From 187180fbce1225d5ce68f0eb4f682a1a184d092d Mon Sep 17 00:00:00 2001 From: Valentin Udaltsov Date: Wed, 15 Apr 2026 03:33:51 +0300 Subject: [PATCH 1/6] Hardcode file path or resource in InputFile --- src/Transport/CurlTransport.php | 45 ++++++-- src/Transport/InputFileData.php | 106 ------------------ .../ApacheMimeTypeResolver.php | 6 +- .../CompositeMimeTypeResolver.php | 6 +- .../CustomMimeTypeResolver.php | 6 +- .../MimeTypeResolverInterface.php | 8 +- src/Transport/NativeTransport.php | 41 +++++-- .../ResourceReader/NativeResourceReader.php | 53 --------- .../ResourceReaderInterface.php | 42 ------- src/Type/InputFile.php | 42 +++---- tests/Support/StubResourceReader.php | 31 ----- .../CurlTransport/CurlTransportTest.php | 21 +--- tests/Transport/InputFileDataTest.php | 34 ------ .../ApacheMimeTypeResolverTest.php | 17 +-- .../CompositeMimeTypeResolverTest.php | 16 +-- .../CustomMimeTypeResolverTest.php | 23 +--- .../NativeTransport/NativeTransportTest.php | 11 +- tests/Type/InputFileTest.php | 78 +++++++------ 18 files changed, 165 insertions(+), 421 deletions(-) delete mode 100644 src/Transport/InputFileData.php delete mode 100644 src/Transport/ResourceReader/NativeResourceReader.php delete mode 100644 src/Transport/ResourceReader/ResourceReaderInterface.php delete mode 100644 tests/Support/StubResourceReader.php delete mode 100644 tests/Transport/InputFileDataTest.php diff --git a/src/Transport/CurlTransport.php b/src/Transport/CurlTransport.php index b95bfeaf..f7653378 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,31 @@ 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) { + throw new RuntimeException('Failed to read the stream.'); + } + + 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..bb8b1545 100644 --- a/src/Transport/NativeTransport.php +++ b/src/Transport/NativeTransport.php @@ -4,8 +4,6 @@ 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; @@ -25,13 +23,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 +137,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 +151,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 +169,32 @@ 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) { + throw new RuntimeException("Failed to read the stream"); + } + + 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 @@ -sendName !== null) { + return $this->sendName; } - 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); + } + + 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..14ad96b8 100644 --- a/tests/Transport/CurlTransport/CurlTransportTest.php +++ b/tests/Transport/CurlTransport/CurlTransportTest.php @@ -5,7 +5,7 @@ namespace Phptg\BotApi\Tests\Transport\CurlTransport; use CurlShareHandle; -use CURLStringFile; +use CURLFile; use PHPUnit\Framework\TestCase; use Phptg\BotApi\Tests\Curl\CurlMock; use Phptg\BotApi\Transport\CurlTransport; @@ -101,8 +101,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 +115,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], ); @@ -147,10 +141,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..a5afcdae 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,15 @@ 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(__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, (new ApacheMimeTypeResolver())->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..1c18542d 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,10 @@ 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(__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 +29,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..5d0d47d8 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'), ], ); diff --git a/tests/Type/InputFileTest.php b/tests/Type/InputFileTest.php index c0e88a69..dd8b3beb 100644 --- a/tests/Type/InputFileTest.php +++ b/tests/Type/InputFileTest.php @@ -5,69 +5,67 @@ 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); + assertNull($file->sendName); + assertSame(basename(__FILE__), $file->filename()); + assertSame('php', $file->extension()); } - public function testFilled(): void + public function testFromPathWithSendName(): 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->sendName); + 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); + assertNull($file->sendName); + 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->sendName); + assertNull($file->filename()); + assertNull($file->extension()); - assertIsResource($file->resource); - assertSame('test.php', $file->filename); + fclose($resource); } - public function testFromLocalNotExistsFile(): void + public function testFromResourceWithSendName(): 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->sendName); + assertSame('document.pdf', $file->filename()); + assertSame('pdf', $file->extension()); + + fclose($resource); } } From a62e1b87402c982c40777b51e2da5410e2b32e49 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 18 Apr 2026 13:25:29 +0300 Subject: [PATCH 2/6] improve --- composer.json | 1 + src/Transport/CurlTransport.php | 4 ++-- src/Transport/NativeTransport.php | 10 ++++------ src/Type/InputFile.php | 19 +++++++++++++------ .../ApacheMimeTypeResolverTest.php | 4 +++- tests/Type/InputFileTest.php | 9 ++------- 6 files changed, 25 insertions(+), 22 deletions(-) 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/src/Transport/CurlTransport.php b/src/Transport/CurlTransport.php index f7653378..7ebc3103 100644 --- a/src/Transport/CurlTransport.php +++ b/src/Transport/CurlTransport.php @@ -81,12 +81,12 @@ 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() ?? ''); + 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() ?? ''); + return new CURLFile($metadata['uri'], $mimeType, $file->filename()); } if ($metadata['seekable']) { diff --git a/src/Transport/NativeTransport.php b/src/Transport/NativeTransport.php index bb8b1545..a9788ace 100644 --- a/src/Transport/NativeTransport.php +++ b/src/Transport/NativeTransport.php @@ -9,6 +9,7 @@ use Phptg\BotApi\Transport\MimeTypeResolver\MimeTypeResolverInterface; use Phptg\BotApi\Type\InputFile; +use function file_get_contents; use function is_string; use function json_encode; @@ -176,12 +177,10 @@ private function parseStatusCode(array $headers): int private function readFile(mixed $pathOrResource): string { if (is_string($pathOrResource)) { - $contents = \file_get_contents($pathOrResource); - + $contents = file_get_contents($pathOrResource); if ($contents === false) { - throw new RuntimeException("Failed to read the file {$pathOrResource}"); + throw new RuntimeException("Failed to read the file $pathOrResource."); } - return $contents; } @@ -190,9 +189,8 @@ private function readFile(mixed $pathOrResource): string } $contents = stream_get_contents($pathOrResource); - if ($contents === false) { - throw new RuntimeException("Failed to read the stream"); + throw new RuntimeException("Failed to read the stream."); } return $contents; diff --git a/src/Type/InputFile.php b/src/Type/InputFile.php index bd587d63..94b9d57f 100644 --- a/src/Type/InputFile.php +++ b/src/Type/InputFile.php @@ -4,6 +4,8 @@ namespace Phptg\BotApi\Type; +use function is_string; + /** * @see https://core.telegram.org/bots/api#sending-files * @@ -12,17 +14,21 @@ final readonly class InputFile { /** - * @param string|resource $pathOrResource + * @param string|resource $pathOrResource File path or open resource. + * @param string|null $filename The filename sent to Telegram. */ public function __construct( public mixed $pathOrResource, - public ?string $sendName = null, + private ?string $filename = null, ) {} + /** + * Returns the filename to use when sending the file, or `null` if it cannot be determined. + */ public function filename(): ?string { - if ($this->sendName !== null) { - return $this->sendName; + if ($this->filename !== null) { + return $this->filename; } if (is_string($this->pathOrResource)) { @@ -30,14 +36,15 @@ public function filename(): ?string } $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/Transport/MimeTypeResolver/ApacheMimeTypeResolverTest.php b/tests/Transport/MimeTypeResolver/ApacheMimeTypeResolverTest.php index a5afcdae..b591502b 100644 --- a/tests/Transport/MimeTypeResolver/ApacheMimeTypeResolverTest.php +++ b/tests/Transport/MimeTypeResolver/ApacheMimeTypeResolverTest.php @@ -24,6 +24,8 @@ public static function dataBase(): iterable #[DataProvider('dataBase')] public function testBase(?string $expected, InputFile $file): void { - assertSame($expected, (new ApacheMimeTypeResolver())->resolve($file)); + $resolver = new ApacheMimeTypeResolver(); + + assertSame($expected, $resolver->resolve($file)); } } diff --git a/tests/Type/InputFileTest.php b/tests/Type/InputFileTest.php index dd8b3beb..cfda24a6 100644 --- a/tests/Type/InputFileTest.php +++ b/tests/Type/InputFileTest.php @@ -17,17 +17,15 @@ public function testFromPath(): void $file = new InputFile(__FILE__); assertSame(__FILE__, $file->pathOrResource); - assertNull($file->sendName); assertSame(basename(__FILE__), $file->filename()); assertSame('php', $file->extension()); } - public function testFromPathWithSendName(): void + public function testFromPathWithFilename(): void { $file = new InputFile(__FILE__, 'test.txt'); assertSame(__FILE__, $file->pathOrResource); - assertSame('test.txt', $file->sendName); assertSame('test.txt', $file->filename()); assertSame('txt', $file->extension()); } @@ -38,7 +36,6 @@ public function testFromResource(): void $file = new InputFile($resource); assertSame($resource, $file->pathOrResource); - assertNull($file->sendName); assertSame(basename(__FILE__), $file->filename()); assertSame('php', $file->extension()); @@ -50,19 +47,17 @@ public function testFromVirtualResource(): void $resource = fopen('php://temp', 'r+b'); $file = new InputFile($resource); - assertNull($file->sendName); assertNull($file->filename()); assertNull($file->extension()); fclose($resource); } - public function testFromResourceWithSendName(): void + public function testFromResourceWithFilename(): void { $resource = fopen('php://temp', 'r+b'); $file = new InputFile($resource, 'document.pdf'); - assertSame('document.pdf', $file->sendName); assertSame('document.pdf', $file->filename()); assertSame('pdf', $file->extension()); From 248030ad84a8876892e0a28131d24071548ef948 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 18 Apr 2026 14:20:05 +0300 Subject: [PATCH 3/6] coverage --- src/Transport/CurlTransport.php | 3 +- src/Transport/NativeTransport.php | 3 +- .../CurlTransport/CurlTransportTest.php | 25 ++++++++++ .../ApacheMimeTypeResolverTest.php | 1 + .../CustomMimeTypeResolverTest.php | 1 + .../NativeTransport/NativeTransportTest.php | 50 +++++++++++++++++++ 6 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/Transport/CurlTransport.php b/src/Transport/CurlTransport.php index 7ebc3103..7eaea0e2 100644 --- a/src/Transport/CurlTransport.php +++ b/src/Transport/CurlTransport.php @@ -95,7 +95,8 @@ private function toCurlFile(InputFile $file): CURLFile|CURLStringFile $contents = stream_get_contents($file->pathOrResource); if ($contents === false) { - throw new RuntimeException('Failed to read the stream.'); + // `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'); diff --git a/src/Transport/NativeTransport.php b/src/Transport/NativeTransport.php index a9788ace..9955ca38 100644 --- a/src/Transport/NativeTransport.php +++ b/src/Transport/NativeTransport.php @@ -190,7 +190,8 @@ private function readFile(mixed $pathOrResource): string $contents = stream_get_contents($pathOrResource); if ($contents === false) { - throw new RuntimeException("Failed to read the stream."); + // `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/tests/Transport/CurlTransport/CurlTransportTest.php b/tests/Transport/CurlTransport/CurlTransportTest.php index 14ad96b8..5999c01f 100644 --- a/tests/Transport/CurlTransport/CurlTransportTest.php +++ b/tests/Transport/CurlTransport/CurlTransportTest.php @@ -6,6 +6,7 @@ use CurlShareHandle; use CURLFile; +use CURLStringFile; use PHPUnit\Framework\TestCase; use Phptg\BotApi\Tests\Curl\CurlMock; use Phptg\BotApi\Transport\CurlTransport; @@ -124,6 +125,30 @@ public function testPostWithLocalFiles(): void assertInstanceOf(CurlShareHandle::class, $options[CURLOPT_SHARE]); } + public function testPostWithSeekableVirtualStreamResource(): 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, 'data.bin')], + ); + + fclose($resource); + + $postFields = $curl->getOptions()[CURLOPT_POSTFIELDS]; + assertInstanceOf(CURLStringFile::class, $postFields['file']); + assertSame('stream content', $postFields['file']->data); + } + public function testSeekableResource(): void { $curl = new CurlMock(); diff --git a/tests/Transport/MimeTypeResolver/ApacheMimeTypeResolverTest.php b/tests/Transport/MimeTypeResolver/ApacheMimeTypeResolverTest.php index b591502b..c498c419 100644 --- a/tests/Transport/MimeTypeResolver/ApacheMimeTypeResolverTest.php +++ b/tests/Transport/MimeTypeResolver/ApacheMimeTypeResolverTest.php @@ -15,6 +15,7 @@ final class ApacheMimeTypeResolverTest extends TestCase { public static function dataBase(): iterable { + 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')]; diff --git a/tests/Transport/MimeTypeResolver/CustomMimeTypeResolverTest.php b/tests/Transport/MimeTypeResolver/CustomMimeTypeResolverTest.php index 1c18542d..05930227 100644 --- a/tests/Transport/MimeTypeResolver/CustomMimeTypeResolverTest.php +++ b/tests/Transport/MimeTypeResolver/CustomMimeTypeResolverTest.php @@ -15,6 +15,7 @@ final class CustomMimeTypeResolverTest extends TestCase { public static function dataBase(): iterable { + 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')]; diff --git a/tests/Transport/NativeTransport/NativeTransportTest.php b/tests/Transport/NativeTransport/NativeTransportTest.php index 5d0d47d8..83cab72c 100644 --- a/tests/Transport/NativeTransport/NativeTransportTest.php +++ b/tests/Transport/NativeTransport/NativeTransportTest.php @@ -223,6 +223,56 @@ public function resolve(InputFile $inputFile): ?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(); From bdfc0ef878b54e7339ff6f55b895eecce1b81eb4 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 18 Apr 2026 14:36:53 +0300 Subject: [PATCH 4/6] mutant --- .../CurlTransport/CurlTransportTest.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/Transport/CurlTransport/CurlTransportTest.php b/tests/Transport/CurlTransport/CurlTransportTest.php index 5999c01f..d04403bf 100644 --- a/tests/Transport/CurlTransport/CurlTransportTest.php +++ b/tests/Transport/CurlTransport/CurlTransportTest.php @@ -7,6 +7,7 @@ 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; @@ -125,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 testPostWithSeekableVirtualStreamResource(): void { $curl = new CurlMock( @@ -147,6 +177,8 @@ public function testPostWithSeekableVirtualStreamResource(): void $postFields = $curl->getOptions()[CURLOPT_POSTFIELDS]; assertInstanceOf(CURLStringFile::class, $postFields['file']); assertSame('stream content', $postFields['file']->data); + assertSame('data.bin', $postFields['file']->postname); + assertSame('application/octet-stream', $postFields['file']->mime); } public function testSeekableResource(): void From 2808c310a4324c11081f2d02cfe2bad70ec6655b Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 18 Apr 2026 14:48:25 +0300 Subject: [PATCH 5/6] test --- .../CurlTransport/CurlTransportTest.php | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/tests/Transport/CurlTransport/CurlTransportTest.php b/tests/Transport/CurlTransport/CurlTransportTest.php index d04403bf..e8e167c4 100644 --- a/tests/Transport/CurlTransport/CurlTransportTest.php +++ b/tests/Transport/CurlTransport/CurlTransportTest.php @@ -155,32 +155,6 @@ public function testPostWithResource(string $mimeType, ?string $filename): void assertSame($mimeType, $postFields['file']->mime); } - public function testPostWithSeekableVirtualStreamResource(): 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, 'data.bin')], - ); - - fclose($resource); - - $postFields = $curl->getOptions()[CURLOPT_POSTFIELDS]; - assertInstanceOf(CURLStringFile::class, $postFields['file']); - assertSame('stream content', $postFields['file']->data); - assertSame('data.bin', $postFields['file']->postname); - assertSame('application/octet-stream', $postFields['file']->mime); - } - public function testSeekableResource(): void { $curl = new CurlMock(); From 14315d325a3b35036ccca1f43904f702ad50647b Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 18 Apr 2026 15:03:26 +0300 Subject: [PATCH 6/6] docs --- CHANGELOG.md | 11 +++++-- README.md | 3 +- docs/mime-type-resolvers.md | 61 +++++++++++++++++++++++++++++++++++++ docs/resource-readers.md | 61 ------------------------------------- docs/transport.md | 7 ++--- 5 files changed, 73 insertions(+), 70 deletions(-) create mode 100644 docs/mime-type-resolvers.md delete mode 100644 docs/resource-readers.md 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/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: