diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c06e158..110f8af2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,15 @@ # Telegram Bot API for PHP Change Log -## 0.18.2 under construction - +## 0.19 April 18, 2026 + +- New #195: Add `InputFile::filename()` and `InputFile::extension()` methods. +- Chg #195: Remove `InputFile::fromLocalFile()` method, pass a file path directly to `InputFile` constructor. +- Chg #195: Remove `ResourceReaderInterface` and `NativeResourceReader`. +- Chg #195: Remove `InputFileData`. +- Chg #195: `MimeTypeResolverInterface::resolve()` now accepts `InputFile` instead of `InputFileData`. +- Chg #195: Remove `$resourceReaders` constructor parameter from `CurlTransport` and `NativeTransport`. - Enh #194: Remove deprecated `curl_close()` call in `CurlTransport`. +- Enh #195: `CurlTransport` now uses `CURLFile` for file-path resources, avoiding loading file content into memory. ## 0.18.1 April 5, 2026 diff --git a/README.md b/README.md index deb2cc43..85ba2438 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ $api->sendMessage( // Send local photo $api->sendPhoto( chatId: 22351, - photo: InputFile::fromLocalFile('/path/to/photo.jpg'), + photo: new InputFile('/path/to/photo.jpg'), ); ``` @@ -143,7 +143,6 @@ $api->downloadFile($file)->saveTo('/local/path/to/file.jpg'); ### Guides - [Transport](docs/transport.md) -- [Resource readers](docs/resource-readers.md) - [Logging](docs/logging.md) - [Webhook handling](docs/webhook-handling.md) - [Custom requests](docs/custom-requests.md) diff --git a/composer.json b/composer.json index f2d901d6..a84170c2 100644 --- a/composer.json +++ b/composer.json @@ -72,6 +72,7 @@ } }, "scripts": { + "coverage": "phpunit --coverage-html=runtime/coverage", "cs-fix": "php-cs-fixer fix", "dependency-analyser": "composer-dependency-analyser", "infection": "infection --threads=max", diff --git a/docs/mime-type-resolvers.md b/docs/mime-type-resolvers.md new file mode 100644 index 00000000..bb756cf0 --- /dev/null +++ b/docs/mime-type-resolvers.md @@ -0,0 +1,61 @@ +# MIME type resolvers + +MIME type resolvers determine the content type of files sent to the Telegram Bot API. The transport uses a resolver +to set the correct `Content-Type` for each file. + +## Built-in resolvers + +- `ApacheMimeTypeResolver` — resolves MIME type by file extension based on + [Apache's `mime.types` file](https://svn.apache.org/repos/asf/httpd/httpd/tags/2.4.9/docs/conf/mime.types). Used by + default. +- `CustomMimeTypeResolver` — resolves MIME type by file extension using a custom map. +- `CompositeMimeTypeResolver` — combines multiple resolvers, returning the first non-null result. + +## Custom MIME type resolvers + +You can create a custom resolver by implementing `MimeTypeResolverInterface`: + +```php +use Phptg\BotApi\Transport\MimeTypeResolver\MimeTypeResolverInterface; +use Phptg\BotApi\Type\InputFile; + +final class MyMimeTypeResolver implements MimeTypeResolverInterface +{ + public function resolve(InputFile $inputFile): ?string + { + // Return MIME type or null if it cannot be determined + return null; + } +} +``` + +Pass it to the transport constructor: + +```php +use Phptg\BotApi\TelegramBotApi; +use Phptg\BotApi\Transport\NativeTransport; + +$transport = new NativeTransport( + mimeTypeResolver: new MyMimeTypeResolver(), +); + +$api = new TelegramBotApi($token, transport: $transport); +``` + +## Combining resolvers + +To combine multiple resolvers use `CompositeMimeTypeResolver`: + +```php +use Phptg\BotApi\Transport\MimeTypeResolver\ApacheMimeTypeResolver; +use Phptg\BotApi\Transport\MimeTypeResolver\CompositeMimeTypeResolver; +use Phptg\BotApi\Transport\MimeTypeResolver\CustomMimeTypeResolver; +use Phptg\BotApi\Transport\NativeTransport; + +$transport = new NativeTransport( + mimeTypeResolver: new CompositeMimeTypeResolver( + new CustomMimeTypeResolver(['webp' => 'image/webp']), + new ApacheMimeTypeResolver(), + ), +); +``` diff --git a/docs/resource-readers.md b/docs/resource-readers.md deleted file mode 100644 index ed377ff3..00000000 --- a/docs/resource-readers.md +++ /dev/null @@ -1,61 +0,0 @@ -# Resource readers - -Resource readers are components that handle reading content from different types of resources stored in `InputFile` -objects. When you send a file using `InputFile`, the transport internally iterates through the provided resource readers -and uses the first one that supports the resource type. - -## Built-in readers - -The package provides one built-in resource reader: - -- `NativeResourceReader` — handles native PHP stream resources created by functions like `fopen()`, `tmpfile()`, etc. - -## Additional readers - -- `StreamResourceReader` — handles PSR-7 `StreamInterface` resources. Provided by the [phptg/transport-psr](https://github.com/phptg/transport-psr) package. - -## Custom resource readers - -You can create custom resource readers by implementing the `ResourceReaderInterface`. This is useful when you need -to support custom resource types. - -Example: - -```php -use Phptg\BotApi\Transport\ResourceReader\ResourceReaderInterface; - -final class MyCustomResourceReader implements ResourceReaderInterface -{ - public function supports(mixed $resource): bool - { - return $resource instanceof MyCustomResource; - } - - public function read(mixed $resource): string - { - return $resource->getContent(); - } - - public function getUri(mixed $resource): string - { - return $resource->getPath(); - } -} -``` - -Then pass it to the transport: - -```php -use Phptg\BotApi\TelegramBotApi; -use Phptg\BotApi\Transport\CurlTransport; -use Phptg\BotApi\Transport\ResourceReader\NativeResourceReader; - -$transport = new CurlTransport( - resourceReaders: [ - new NativeResourceReader(), - new MyCustomResourceReader(), - ], -); - -$api = new TelegramBotApi($token, transport: $transport); -``` diff --git a/docs/transport.md b/docs/transport.md index b509fa2d..6b8c2d17 100644 --- a/docs/transport.md +++ b/docs/transport.md @@ -31,8 +31,7 @@ $api = new TelegramBotApi($token, transport: $transport); Constructor parameters: -- `$resourceReaders` — list of [resource readers](resource-readers.md) to handle different resource types. By default, - includes `NativeResourceReader`. +- `$mimeTypeResolver` — [MIME type resolver](mime-type-resolvers.md) for determining file types. Defaults to `ApacheMimeTypeResolver`. ## Native @@ -59,9 +58,7 @@ $api = new TelegramBotApi($token, transport: $transport); Constructor parameters: -- `$mimeTypeResolver` — MIME type resolver for determining file types. Defaults to `ApacheMimeTypeResolver`. -- `$resourceReaders` — List of [resource readers](resource-readers.md) to handle different resource types. By default, - includes `NativeResourceReader`. +- `$mimeTypeResolver` — [MIME type resolver](mime-type-resolvers.md) for determining file types. Defaults to `ApacheMimeTypeResolver`. Available MIME type resolvers: diff --git a/src/Transport/CurlTransport.php b/src/Transport/CurlTransport.php index b95bfeaf..7eaea0e2 100644 --- a/src/Transport/CurlTransport.php +++ b/src/Transport/CurlTransport.php @@ -5,14 +5,18 @@ namespace Phptg\BotApi\Transport; use CurlShareHandle; +use CURLFile; use CURLStringFile; +use RuntimeException; use Phptg\BotApi\Curl\Curl; use Phptg\BotApi\Curl\CurlException; use Phptg\BotApi\Curl\CurlInterface; -use Phptg\BotApi\Transport\ResourceReader\NativeResourceReader; -use Phptg\BotApi\Transport\ResourceReader\ResourceReaderInterface; +use Phptg\BotApi\Transport\MimeTypeResolver\ApacheMimeTypeResolver; +use Phptg\BotApi\Transport\MimeTypeResolver\MimeTypeResolverInterface; +use Phptg\BotApi\Type\InputFile; use function is_int; +use function is_string; /** * @api @@ -22,13 +26,12 @@ private CurlShareHandle $curlShareHandle; /** - * @param ResourceReaderInterface[] $resourceReaders List of resource readers to handle different resource types. + * @param MimeTypeResolverInterface $mimeTypeResolver MIME type resolver for determining file types. Defaults + * to {@see ApacheMimeTypeResolver}. * @param CurlInterface $curl cURL interface implementation for making HTTP requests. */ public function __construct( - private array $resourceReaders = [ - new NativeResourceReader(), - ], + private MimeTypeResolverInterface $mimeTypeResolver = new ApacheMimeTypeResolver(), private CurlInterface $curl = new Curl(), ) { $this->curlShareHandle = $this->createCurlShareHandle(); @@ -62,10 +65,7 @@ public function post(string $url, string $body, array $headers): ApiResponse public function postWithFiles(string $url, array $data, array $files): ApiResponse { foreach ($files as $key => $file) { - $data[$key] = new CURLStringFile( - (new InputFileData($file, $this->resourceReaders))->read(), - $file->filename ?? '', - ); + $data[$key] = $this->toCurlFile($file); } $options = [ @@ -76,6 +76,32 @@ public function postWithFiles(string $url, array $data, array $files): ApiRespon return $this->send($options); } + private function toCurlFile(InputFile $file): CURLFile|CURLStringFile + { + $mimeType = $this->mimeTypeResolver->resolve($file); + + if (is_string($file->pathOrResource)) { + return new CURLFile($file->pathOrResource, $mimeType, $file->filename()); + } + + $metadata = stream_get_meta_data($file->pathOrResource); + if (!str_contains($metadata['uri'], '://')) { + return new CURLFile($metadata['uri'], $mimeType, $file->filename()); + } + + if ($metadata['seekable']) { + rewind($file->pathOrResource); + } + + $contents = stream_get_contents($file->pathOrResource); + if ($contents === false) { + // `stream_get_contents()` can return false only on error, but we can't trigger it in tests. + throw new RuntimeException('Failed to read the stream.'); // @codeCoverageIgnore + } + + return new CURLStringFile($contents, $file->filename() ?? '', $mimeType ?? 'application/octet-stream'); + } + public function downloadFile(string $url): mixed { /** diff --git a/src/Transport/InputFileData.php b/src/Transport/InputFileData.php deleted file mode 100644 index f5cd1109..00000000 --- a/src/Transport/InputFileData.php +++ /dev/null @@ -1,106 +0,0 @@ -reader = $this->resolveReader($readers); - } - - /** - * Reads the content of the input file. - * - * @return string The file content. - */ - public function read(): string - { - return $this->reader->read($this->inputFile->resource); - } - - /** - * Returns the file extension. - * - * @return string|null The file extension, or `null` if it cannot be determined. - */ - public function extension(): ?string - { - $filepath = $this->filepath(); - return $filepath === null - ? null - : pathinfo($filepath, PATHINFO_EXTENSION); - } - - /** - * Returns the base name of the file. - * - * @return string|null The file base name, or `null` if it cannot be determined. - */ - public function basename(): ?string - { - $filepath = $this->filepath(); - return $filepath === null - ? null - : basename($filepath); - } - - /** - * Returns the file path. - * - * @return string|null The file path, or `null` if it cannot be determined. - */ - private function filepath(): ?string - { - if ($this->inputFile->filename !== null) { - return $this->inputFile->filename; - } - - $uri = $this->reader->getUri($this->inputFile->resource); - - if (str_contains($uri, '://')) { - return null; - } - - return $uri; - } - - /** - * Resolves the appropriate reader for the input file resource. - * - * @param ResourceReaderInterface[] $readers Available readers. - * - * @return ResourceReaderInterface The resolved reader. - * - * @throws LogicException If no suitable reader is found. - */ - private function resolveReader(array $readers): ResourceReaderInterface - { - foreach ($readers as $reader) { - if ($reader->supports($this->inputFile->resource)) { - return $reader; - } - } - - throw new LogicException('No suitable resource reader found.'); - } -} diff --git a/src/Transport/MimeTypeResolver/ApacheMimeTypeResolver.php b/src/Transport/MimeTypeResolver/ApacheMimeTypeResolver.php index d0ec24e6..f6281115 100644 --- a/src/Transport/MimeTypeResolver/ApacheMimeTypeResolver.php +++ b/src/Transport/MimeTypeResolver/ApacheMimeTypeResolver.php @@ -4,7 +4,7 @@ namespace Phptg\BotApi\Transport\MimeTypeResolver; -use Phptg\BotApi\Transport\InputFileData; +use Phptg\BotApi\Type\InputFile; /** * @see https://svn.apache.org/repos/asf/httpd/httpd/tags/2.4.9/docs/conf/mime.types @@ -997,9 +997,9 @@ 'ice' => 'x-conference/x-cooltalk', ]; - public function resolve(InputFileData $fileData): ?string + public function resolve(InputFile $inputFile): ?string { - $extension = $fileData->extension(); + $extension = $inputFile->extension(); if ($extension === null) { return null; } diff --git a/src/Transport/MimeTypeResolver/CompositeMimeTypeResolver.php b/src/Transport/MimeTypeResolver/CompositeMimeTypeResolver.php index a3dbc8dd..7a0c55d8 100644 --- a/src/Transport/MimeTypeResolver/CompositeMimeTypeResolver.php +++ b/src/Transport/MimeTypeResolver/CompositeMimeTypeResolver.php @@ -4,7 +4,7 @@ namespace Phptg\BotApi\Transport\MimeTypeResolver; -use Phptg\BotApi\Transport\InputFileData; +use Phptg\BotApi\Type\InputFile; /** * @api @@ -24,10 +24,10 @@ public function __construct(MimeTypeResolverInterface ...$resolvers) $this->resolvers = $resolvers; } - public function resolve(InputFileData $fileData): ?string + public function resolve(InputFile $inputFile): ?string { foreach ($this->resolvers as $resolver) { - $result = $resolver->resolve($fileData); + $result = $resolver->resolve($inputFile); if ($result !== null) { return $result; } diff --git a/src/Transport/MimeTypeResolver/CustomMimeTypeResolver.php b/src/Transport/MimeTypeResolver/CustomMimeTypeResolver.php index 3aa6a77a..fe832560 100644 --- a/src/Transport/MimeTypeResolver/CustomMimeTypeResolver.php +++ b/src/Transport/MimeTypeResolver/CustomMimeTypeResolver.php @@ -4,7 +4,7 @@ namespace Phptg\BotApi\Transport\MimeTypeResolver; -use Phptg\BotApi\Transport\InputFileData; +use Phptg\BotApi\Type\InputFile; /** * @api @@ -18,9 +18,9 @@ public function __construct( private array $map, ) {} - public function resolve(InputFileData $fileData): ?string + public function resolve(InputFile $inputFile): ?string { - $extension = $fileData->extension(); + $extension = $inputFile->extension(); if ($extension === null) { return null; } diff --git a/src/Transport/MimeTypeResolver/MimeTypeResolverInterface.php b/src/Transport/MimeTypeResolver/MimeTypeResolverInterface.php index c05c57cd..4839cca2 100644 --- a/src/Transport/MimeTypeResolver/MimeTypeResolverInterface.php +++ b/src/Transport/MimeTypeResolver/MimeTypeResolverInterface.php @@ -4,7 +4,7 @@ namespace Phptg\BotApi\Transport\MimeTypeResolver; -use Phptg\BotApi\Transport\InputFileData; +use Phptg\BotApi\Type\InputFile; /** * @api @@ -12,11 +12,11 @@ interface MimeTypeResolverInterface { /** - * Resolves the MIME type for the given input file data. + * Resolves the MIME type for the given file. * - * @param InputFileData $fileData The input file data to resolve MIME type for. + * @param InputFile $inputFile The file to resolve MIME type for. * * @return string|null The resolved MIME type, or `null` if it cannot be determined. */ - public function resolve(InputFileData $fileData): ?string; + public function resolve(InputFile $inputFile): ?string; } diff --git a/src/Transport/NativeTransport.php b/src/Transport/NativeTransport.php index abcb7d9c..9955ca38 100644 --- a/src/Transport/NativeTransport.php +++ b/src/Transport/NativeTransport.php @@ -4,13 +4,12 @@ namespace Phptg\BotApi\Transport; -use Phptg\BotApi\Transport\ResourceReader\NativeResourceReader; -use Phptg\BotApi\Transport\ResourceReader\ResourceReaderInterface; use RuntimeException; use Phptg\BotApi\Transport\MimeTypeResolver\ApacheMimeTypeResolver; use Phptg\BotApi\Transport\MimeTypeResolver\MimeTypeResolverInterface; use Phptg\BotApi\Type\InputFile; +use function file_get_contents; use function is_string; use function json_encode; @@ -25,13 +24,9 @@ /** * @param MimeTypeResolverInterface $mimeTypeResolver MIME type resolver for determining file types. Defaults * to {@see ApacheMimeTypeResolver}. - * @param ResourceReaderInterface[] $resourceReaders List of resource readers to handle different resource types. */ public function __construct( private MimeTypeResolverInterface $mimeTypeResolver = new ApacheMimeTypeResolver(), - private array $resourceReaders = [ - new NativeResourceReader(), - ], ) {} public function get(string $url): ApiResponse @@ -143,9 +138,8 @@ private function buildMultipartFormData(array $data, array $files, string $bound } foreach ($files as $key => $file) { - $fileData = new InputFileData($file, $this->resourceReaders); - $mimeType = $this->mimeTypeResolver->resolve($fileData); - $filename = $fileData->basename(); + $mimeType = $this->mimeTypeResolver->resolve($file); + $filename = $file->filename(); $contentDisposition = "Content-Disposition: form-data; name=\"$key\""; if ($filename !== null) { @@ -158,7 +152,7 @@ private function buildMultipartFormData(array $data, array $files, string $bound $result[] = "Content-Type: $mimeType"; } $result[] = ''; - $result[] = $fileData->read(); + $result[] = $this->readFile($file->pathOrResource); } $result[] = "--$boundary--"; @@ -176,4 +170,30 @@ private function parseStatusCode(array $headers): int ? (int) $matches[1] : 0; } + + /** + * @param string|resource $pathOrResource + */ + private function readFile(mixed $pathOrResource): string + { + if (is_string($pathOrResource)) { + $contents = file_get_contents($pathOrResource); + if ($contents === false) { + throw new RuntimeException("Failed to read the file $pathOrResource."); + } + return $contents; + } + + if (stream_get_meta_data($pathOrResource)['seekable']) { + rewind($pathOrResource); + } + + $contents = stream_get_contents($pathOrResource); + if ($contents === false) { + // `stream_get_contents()` can return false only on error, but we can't trigger it in tests. + throw new RuntimeException('Failed to read the stream.'); // @codeCoverageIgnore + } + + return $contents; + } } diff --git a/src/Transport/ResourceReader/NativeResourceReader.php b/src/Transport/ResourceReader/NativeResourceReader.php deleted file mode 100644 index b970ac22..00000000 --- a/src/Transport/ResourceReader/NativeResourceReader.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * @api - */ -final class NativeResourceReader implements ResourceReaderInterface -{ - /** - * @param resource $resource The native PHP resource to read from. - * - * @return string The content of the resource. - */ - public function read(mixed $resource): string - { - $metadata = stream_get_meta_data($resource); - if ($metadata['seekable']) { - rewind($resource); - } - - /** - * @var string We assume that `$resource` is correct, so `stream_get_contents()` never returns `false`. - */ - return stream_get_contents($resource); - } - - /** - * @param resource $resource The native PHP resource to get URI from. - * - * @return string The resource URI. - */ - public function getUri(mixed $resource): string - { - return stream_get_meta_data($resource)['uri']; - } - - public function supports(mixed $resource): bool - { - return is_resource($resource); - } -} diff --git a/src/Transport/ResourceReader/ResourceReaderInterface.php b/src/Transport/ResourceReader/ResourceReaderInterface.php deleted file mode 100644 index e9903bf4..00000000 --- a/src/Transport/ResourceReader/ResourceReaderInterface.php +++ /dev/null @@ -1,42 +0,0 @@ -filename !== null) { + return $this->filename; } - return new self($resource, $filename); + + if (is_string($this->pathOrResource)) { + return basename($this->pathOrResource); + } + + $uri = stream_get_meta_data($this->pathOrResource)['uri']; + return str_contains($uri, '://') ? null : basename($uri); + } + + /** + * Returns the file extension derived from the filename, or `null` if it cannot be determined. + */ + public function extension(): ?string + { + $filename = $this->filename(); + return $filename === null ? null : pathinfo($filename, PATHINFO_EXTENSION); } } diff --git a/tests/Support/StubResourceReader.php b/tests/Support/StubResourceReader.php deleted file mode 100644 index 295551ba..00000000 --- a/tests/Support/StubResourceReader.php +++ /dev/null @@ -1,31 +0,0 @@ -content; - } - - public function getUri(mixed $resource): string - { - return $this->uri; - } - - public function supports(mixed $resource): bool - { - return $this->supports; - } -} diff --git a/tests/Transport/CurlTransport/CurlTransportTest.php b/tests/Transport/CurlTransport/CurlTransportTest.php index 8a9db0be..e8e167c4 100644 --- a/tests/Transport/CurlTransport/CurlTransportTest.php +++ b/tests/Transport/CurlTransport/CurlTransportTest.php @@ -5,7 +5,9 @@ namespace Phptg\BotApi\Tests\Transport\CurlTransport; use CurlShareHandle; +use CURLFile; use CURLStringFile; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Phptg\BotApi\Tests\Curl\CurlMock; use Phptg\BotApi\Transport\CurlTransport; @@ -101,8 +103,8 @@ public function testPostWithLocalFiles(): void '//url/sendPhoto', [], [ - 'photo1' => InputFile::fromLocalFile(__DIR__ . '/photo.png'), - 'photo2' => InputFile::fromLocalFile(__DIR__ . '/photo.png', 'photo.png'), + 'photo1' => new InputFile(__DIR__ . '/photo.png'), + 'photo2' => new InputFile(__DIR__ . '/photo.png', 'photo.png'), ], ); @@ -115,14 +117,8 @@ public function testPostWithLocalFiles(): void assertSame('//url/sendPhoto', $options[CURLOPT_URL]); assertEquals( [ - 'photo1' => new CURLStringFile( - file_get_contents(__DIR__ . '/photo.png'), - '', - ), - 'photo2' => new CURLStringFile( - file_get_contents(__DIR__ . '/photo.png'), - 'photo.png', - ), + 'photo1' => new CURLFile(__DIR__ . '/photo.png', 'image/png', 'photo.png'), + 'photo2' => new CURLFile(__DIR__ . '/photo.png', 'image/png', 'photo.png'), ], $options[CURLOPT_POSTFIELDS], ); @@ -130,6 +126,35 @@ public function testPostWithLocalFiles(): void assertInstanceOf(CurlShareHandle::class, $options[CURLOPT_SHARE]); } + #[TestWith(['text/plain', 'test.txt'])] + #[TestWith(['application/octet-stream', 'data.bin'])] + #[TestWith(['application/octet-stream', null])] + public function testPostWithResource(string $mimeType, ?string $filename): void + { + $curl = new CurlMock( + execResult: '{"ok":true,"result":[]}', + getinfoResult: [CURLINFO_HTTP_CODE => 200], + ); + $transport = new CurlTransport(curl: $curl); + + $resource = fopen('php://memory', 'r+'); + fwrite($resource, 'stream content'); + + $transport->postWithFiles( + '//url/method', + [], + ['file' => new InputFile($resource, $filename)], + ); + + fclose($resource); + + $postFields = $curl->getOptions()[CURLOPT_POSTFIELDS]; + assertInstanceOf(CURLStringFile::class, $postFields['file']); + assertSame('stream content', $postFields['file']->data); + assertSame($filename ?? '', $postFields['file']->postname); + assertSame($mimeType, $postFields['file']->mime); + } + public function testSeekableResource(): void { $curl = new CurlMock(); @@ -147,10 +172,7 @@ public function testSeekableResource(): void assertEquals( [ - 'photo' => new CURLStringFile( - file_get_contents(__DIR__ . '/photo.png'), - '', - ), + 'photo' => new CURLFile(__DIR__ . '/photo.png', 'image/png', 'photo.png'), ], $curl->getOptions()[CURLOPT_POSTFIELDS] ?? null, ); diff --git a/tests/Transport/InputFileDataTest.php b/tests/Transport/InputFileDataTest.php deleted file mode 100644 index 8a0fea68..00000000 --- a/tests/Transport/InputFileDataTest.php +++ /dev/null @@ -1,34 +0,0 @@ -expectException(LogicException::class); - $this->expectExceptionMessage('No suitable resource reader found.'); - new InputFileData($file, []); - } - - public function testExtensionAndBasenameAreNullForUri(): void - { - $reader = new StubResourceReader(uri: 'https://example.com/file.txt'); - - $file = new InputFile('resource'); - $data = new InputFileData($file, [$reader]); - - $this->assertNull($data->extension()); - $this->assertNull($data->basename()); - } -} diff --git a/tests/Transport/MimeTypeResolver/ApacheMimeTypeResolverTest.php b/tests/Transport/MimeTypeResolver/ApacheMimeTypeResolverTest.php index 8751dd4f..c498c419 100644 --- a/tests/Transport/MimeTypeResolver/ApacheMimeTypeResolverTest.php +++ b/tests/Transport/MimeTypeResolver/ApacheMimeTypeResolverTest.php @@ -4,8 +4,6 @@ namespace Phptg\BotApi\Tests\Transport\MimeTypeResolver; -use Phptg\BotApi\Transport\InputFileData; -use Phptg\BotApi\Transport\ResourceReader\NativeResourceReader; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Phptg\BotApi\Transport\MimeTypeResolver\ApacheMimeTypeResolver; @@ -17,20 +15,18 @@ final class ApacheMimeTypeResolverTest extends TestCase { public static function dataBase(): iterable { - yield [null, InputFile::fromLocalFile(__DIR__ . '/files/test.unknown')]; - yield ['text/plain', InputFile::fromLocalFile(__DIR__ . '/files/test.txt')]; - yield ['text/css', InputFile::fromLocalFile(__DIR__ . '/files/test.txt', 'test.css')]; - yield ['text/css', InputFile::fromLocalFile(__DIR__ . '/files/test.txt', 'test.CSS')]; + yield [null, new InputFile(fopen('php://memory', 'rb'))]; + yield [null, new InputFile(__DIR__ . '/files/test.unknown')]; + yield ['text/plain', new InputFile(__DIR__ . '/files/test.txt')]; + yield ['text/css', new InputFile(__DIR__ . '/files/test.txt', 'test.css')]; + yield ['text/css', new InputFile(__DIR__ . '/files/test.txt', 'test.CSS')]; } #[DataProvider('dataBase')] public function testBase(?string $expected, InputFile $file): void { $resolver = new ApacheMimeTypeResolver(); - $fileData = new InputFileData($file, [new NativeResourceReader()]); - $result = $resolver->resolve($fileData); - - assertSame($expected, $result); + assertSame($expected, $resolver->resolve($file)); } } diff --git a/tests/Transport/MimeTypeResolver/CompositeMimeTypeResolverTest.php b/tests/Transport/MimeTypeResolver/CompositeMimeTypeResolverTest.php index 8663d6d4..1bf6fff6 100644 --- a/tests/Transport/MimeTypeResolver/CompositeMimeTypeResolverTest.php +++ b/tests/Transport/MimeTypeResolver/CompositeMimeTypeResolverTest.php @@ -2,13 +2,11 @@ declare(strict_types=1); -namespace Transport\MimeTypeResolver; +namespace Phptg\BotApi\Tests\Transport\MimeTypeResolver; use Generator; -use Phptg\BotApi\Transport\ResourceReader\NativeResourceReader; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use Phptg\BotApi\Transport\InputFileData; use Phptg\BotApi\Transport\MimeTypeResolver\ApacheMimeTypeResolver; use Phptg\BotApi\Transport\MimeTypeResolver\CompositeMimeTypeResolver; use Phptg\BotApi\Transport\MimeTypeResolver\CustomMimeTypeResolver; @@ -20,30 +18,28 @@ final class CompositeMimeTypeResolverTest extends TestCase { public static function dataResolve(): Generator { - $readers = [new NativeResourceReader()]; - yield 'custom resolver takes priority' => [ 'text/my-plain', - new InputFileData(InputFile::fromLocalFile(__DIR__ . '/files/test.txt'), $readers), + new InputFile(__DIR__ . '/files/test.txt'), ]; yield 'fallback to apache resolver' => [ 'image/png', - new InputFileData(InputFile::fromLocalFile(__DIR__ . '/files/dot.png'), $readers), + new InputFile(__DIR__ . '/files/dot.png'), ]; yield 'unknown extension returns null' => [ null, - new InputFileData(InputFile::fromLocalFile(__DIR__ . '/files/test.unknown'), $readers), + new InputFile(__DIR__ . '/files/test.unknown'), ]; } #[DataProvider('dataResolve')] - public function testResolve(?string $expected, InputFileData $inputFileData): void + public function testResolve(?string $expected, InputFile $file): void { $resolver = new CompositeMimeTypeResolver( new CustomMimeTypeResolver(['txt' => 'text/my-plain']), new ApacheMimeTypeResolver(), ); - assertSame($expected, $resolver->resolve($inputFileData)); + assertSame($expected, $resolver->resolve($file)); } } diff --git a/tests/Transport/MimeTypeResolver/CustomMimeTypeResolverTest.php b/tests/Transport/MimeTypeResolver/CustomMimeTypeResolverTest.php index 7150f3ad..05930227 100644 --- a/tests/Transport/MimeTypeResolver/CustomMimeTypeResolverTest.php +++ b/tests/Transport/MimeTypeResolver/CustomMimeTypeResolverTest.php @@ -4,9 +4,6 @@ namespace Phptg\BotApi\Tests\Transport\MimeTypeResolver; -use Phptg\BotApi\Tests\Support\StubResourceReader; -use Phptg\BotApi\Transport\InputFileData; -use Phptg\BotApi\Transport\ResourceReader\NativeResourceReader; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Phptg\BotApi\Transport\MimeTypeResolver\CustomMimeTypeResolver; @@ -18,11 +15,11 @@ final class CustomMimeTypeResolverTest extends TestCase { public static function dataBase(): iterable { - yield [null, new InputFile(null)]; - yield [null, InputFile::fromLocalFile(__DIR__ . '/files/test.unknown')]; - yield ['text/plain', InputFile::fromLocalFile(__DIR__ . '/files/test.txt')]; - yield ['text/css', InputFile::fromLocalFile(__DIR__ . '/files/test.txt', 'test.css')]; - yield ['text/css', InputFile::fromLocalFile(__DIR__ . '/files/test.txt', 'test.CSS')]; + yield [null, new InputFile(fopen('php://memory', 'rb'))]; + yield [null, new InputFile(__DIR__ . '/files/test.unknown')]; + yield ['text/plain', new InputFile(__DIR__ . '/files/test.txt')]; + yield ['text/css', new InputFile(__DIR__ . '/files/test.txt', 'test.css')]; + yield ['text/css', new InputFile(__DIR__ . '/files/test.txt', 'test.CSS')]; } #[DataProvider('dataBase')] @@ -33,16 +30,7 @@ public function testBase(?string $expected, InputFile $file): void 'txt' => 'text/plain', 'css' => 'text/css', ]); - $fileData = new InputFileData( - $file, - [ - new NativeResourceReader(), - new StubResourceReader(uri: 'custom://test'), - ], - ); - $result = $resolver->resolve($fileData); - - assertSame($expected, $result); + assertSame($expected, $resolver->resolve($file)); } } diff --git a/tests/Transport/NativeTransport/NativeTransportTest.php b/tests/Transport/NativeTransport/NativeTransportTest.php index 5358ddd1..83cab72c 100644 --- a/tests/Transport/NativeTransport/NativeTransportTest.php +++ b/tests/Transport/NativeTransport/NativeTransportTest.php @@ -4,7 +4,6 @@ namespace Phptg\BotApi\Tests\Transport\NativeTransport; -use Phptg\BotApi\Transport\InputFileData; use PHPUnit\Framework\TestCase; use RuntimeException; use Phptg\BotApi\Tests\Transport\NativeTransport\StreamMock\StreamMock; @@ -114,8 +113,8 @@ public function testPostWithLocalFiles(): void 'age' => 19, ], [ - 'photo1' => InputFile::fromLocalFile(__DIR__ . '/photo.png'), - 'photo2' => InputFile::fromLocalFile(__DIR__ . '/photo.png', 'face.png'), + 'photo1' => new InputFile(__DIR__ . '/photo.png'), + 'photo2' => new InputFile(__DIR__ . '/photo.png', 'face.png'), ], ); @@ -161,7 +160,7 @@ public function testPostWithFiles(): void 'ages' => [23, 45], ], [ - 'file1' => InputFile::fromLocalFile(__DIR__ . '/test.txt'), + 'file1' => new InputFile(__DIR__ . '/test.txt'), ], ); @@ -188,7 +187,7 @@ public function testCustomMimeTypeResolver(): void { $transport = new NativeTransport( new class implements MimeTypeResolverInterface { - public function resolve(InputFileData $fileData): ?string + public function resolve(InputFile $inputFile): ?string { return 'text/custom'; } @@ -207,7 +206,7 @@ public function resolve(InputFileData $fileData): ?string 'http://url/method', [], [ - 'file1' => InputFile::fromLocalFile(__DIR__ . '/test.txt'), + 'file1' => new InputFile(__DIR__ . '/test.txt'), ], ); @@ -224,6 +223,56 @@ public function resolve(InputFileData $fileData): ?string ); } + public function testReadNonExistentFileThrowsException(): void + { + $transport = new NativeTransport(); + + $nonExistentPath = __DIR__ . '/non-existent-file.txt'; + $inputFile = new InputFile($nonExistentPath); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("Failed to read the file $nonExistentPath."); + + set_error_handler(static fn() => true, E_WARNING); + try { + $transport->postWithFiles( + 'http://url/method', + [], + ['file' => $inputFile], + ); + } finally { + restore_error_handler(); + } + } + + public function testPostWithSeekableStreamResource(): void + { + $transport = new NativeTransport(); + + StreamMock::enable( + responseHeaders: [ + 'HTTP/1.1 200 OK', + 'Content-Type: text/json', + ], + responseBody: '{"ok":true,"result":[]}', + ); + + $resource = fopen('php://temp', 'r+'); + fwrite($resource, 'stream content'); + + $transport->postWithFiles( + 'http://url/method', + [], + ['file' => new InputFile($resource, 'data.bin')], + ); + + fclose($resource); + + $request = StreamMock::disable(); + + assertStringContainsString('stream content', $request['options']['http']['content']); + } + public function testErrorOnSend(): void { $transport = new NativeTransport(); diff --git a/tests/Type/InputFileTest.php b/tests/Type/InputFileTest.php index c0e88a69..cfda24a6 100644 --- a/tests/Type/InputFileTest.php +++ b/tests/Type/InputFileTest.php @@ -5,69 +5,62 @@ namespace Phptg\BotApi\Tests\Type; use PHPUnit\Framework\TestCase; -use RuntimeException; -use Throwable; use Phptg\BotApi\Type\InputFile; -use function PHPUnit\Framework\assertInstanceOf; -use function PHPUnit\Framework\assertIsResource; use function PHPUnit\Framework\assertNull; use function PHPUnit\Framework\assertSame; final class InputFileTest extends TestCase { - public function testBase(): void + public function testFromPath(): void { - $file = new InputFile('test'); + $file = new InputFile(__FILE__); - assertSame('test', $file->resource); - assertNull($file->filename); + assertSame(__FILE__, $file->pathOrResource); + assertSame(basename(__FILE__), $file->filename()); + assertSame('php', $file->extension()); } - public function testFilled(): void + public function testFromPathWithFilename(): void { - $file = new InputFile('test', 'file.txt'); + $file = new InputFile(__FILE__, 'test.txt'); - assertSame('test', $file->resource); - assertSame('file.txt', $file->filename); + assertSame(__FILE__, $file->pathOrResource); + assertSame('test.txt', $file->filename()); + assertSame('txt', $file->extension()); } - public function testFromLocalFile(): void + public function testFromResource(): void { - $file = InputFile::fromLocalFile(__FILE__); + $resource = fopen(__FILE__, 'rb'); + $file = new InputFile($resource); - assertIsResource($file->resource); - assertNull($file->filename); + assertSame($resource, $file->pathOrResource); + assertSame(basename(__FILE__), $file->filename()); + assertSame('php', $file->extension()); + + fclose($resource); } - public function testFromLocalFileWithName(): void + public function testFromVirtualResource(): void { - $file = InputFile::fromLocalFile(__FILE__, 'test.php'); + $resource = fopen('php://temp', 'r+b'); + $file = new InputFile($resource); + + assertNull($file->filename()); + assertNull($file->extension()); - assertIsResource($file->resource); - assertSame('test.php', $file->filename); + fclose($resource); } - public function testFromLocalNotExistsFile(): void + public function testFromResourceWithFilename(): void { - $errorMessage = null; - set_error_handler( - static function (int $code, string $message) use (&$errorMessage): bool { - $errorMessage = $message; - return true; - }, - ); - - $exception = null; - try { - InputFile::fromLocalFile('not-exists'); - } catch (Throwable $exception) { - } - - restore_error_handler(); - - assertSame('fopen(not-exists): Failed to open stream: No such file or directory', $errorMessage); - assertInstanceOf(RuntimeException::class, $exception); - assertSame('Unable to open file "not-exists".', $exception->getMessage()); + $resource = fopen('php://temp', 'r+b'); + $file = new InputFile($resource, 'document.pdf'); + + assertSame('document.pdf', $file->filename()); + assertSame('pdf', $file->extension()); + + fclose($resource); } }