Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Telegram Bot API for PHP Change Log

## 0.15 under development

- Chg #187: Change `TransportInterface::downloadFile()` signature — remove `$stream` parameter, method now returns
`resource` (readable stream) instead of `string`.
- Chg #187: `TelegramBotApi::downloadFile()` now returns `DownloadedFile` instead of `string`.
- Chg #187: Remove `TelegramBotApi::downloadFileTo()` method.
- Chg #187: Remove `TransportInterface::downloadFileTo()` method.
- Chg #187: Move `SaveFileException` from `Phptg\BotApi\Transport` to `Phptg\BotApi` namespace.

## 0.14.1 February 9, 2026

- New #186: Add `SetMyProfilePhoto`, `RemoveMyProfilePhoto` and `GetUserProfileAudios` methods.
Expand Down
19 changes: 8 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,26 +119,23 @@ $url = $api->makeFileUrl('photos/file_2');

#### File downloading

Use `TelegramBotApi::downloadFile()` and `TelegramBotApi::downloadFileTo()` methods to download a file from the Telegram
server. For example:
Use `TelegramBotApi::downloadFile()` method to download a file from the Telegram server. The method returns
a `DownloadedFile` instance with `getStream()`, `getBody()` and `saveTo()` methods. For example:

```php
/**
* @var \Phptg\BotApi\TelegramBotApi $api
* @var \Phptg\BotApi\Type\File $file
*/

// Get file content by `File` instance
$fileContent = $api->downloadFile($file);

// Get file content by file path
$fileContent = $api->downloadFile('photos/file_2');
// Get file content as string
$content = $api->downloadFile($file)->getBody();

// Download and save file by `File` instance
$fileContent = $api->downloadFileTo($file, '/local/path/to/file.jpg');
// Get file content as stream
$stream = $api->downloadFile('photos/file_2')->getStream();

// Download and save file by file path
$fileContent = $api->downloadFileTo('photos/file_2', '/local/path/to/file.jpg');
// Download and save file
$api->downloadFile($file)->saveTo('/local/path/to/file.jpg');
```

### Guides
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"phpunit/phpunit": "^11.5.46",
"psr/log": "^3.0.2",
"yiisoft/files": "^2.1",
"yiisoft/test-support": "^3.1.0"
"yiisoft/test-support": "^3.2.0"
},
"suggest": {
"ext-curl": "To use `CurlTransport`.",
Expand Down
3 changes: 2 additions & 1 deletion infection.json.dist
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
]
},
"logs": {
"text": "php:\/\/stderr",
"text": "runtime/infection/run.log",
"stryker": {
"report": "master"
}
},
"tmpDir": "runtime/infection",
"mutators": {
"@default": true
},
Expand Down
64 changes: 64 additions & 0 deletions src/DownloadedFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Phptg\BotApi;

/**
* Represents a downloaded file from the Telegram servers.
*
* @api
*/
final readonly class DownloadedFile
{
/**
* @param resource $stream A `php://temp` or `php://memory` stream with the file content.
*/
public function __construct(
private mixed $stream,
) {}

/**
* Returns the stream with the file content.
*
* @return resource The stream with the file content (`php://temp` or `php://memory` resource).
*/
public function getStream(): mixed
{
return $this->stream;
}

/**
* Saves the file content to the specified path.
*
* @throws SaveFileException If an error occurred while saving the file.
*/
public function saveTo(string $path): void
{
if (file_exists($path)) {
throw new SaveFileException("File already exists: $path");
}

set_error_handler(
static function (int $errorNumber, string $errorString): bool {
throw new SaveFileException($errorString);
},
);
try {
file_put_contents($path, $this->stream);
} finally {
restore_error_handler();
}
}

/**
* Returns the file content as a string.
*/
public function getBody(): string
{
/**
* @var string We expect the stream to be valid, so `stream_get_contents()` returns string.
*/
return stream_get_contents($this->stream);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Phptg\BotApi\Transport;
namespace Phptg\BotApi;

use RuntimeException;

Expand Down
56 changes: 32 additions & 24 deletions src/TelegramBotApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@
use Phptg\BotApi\Transport\CurlTransport;
use Phptg\BotApi\Transport\DownloadFileException;
use Phptg\BotApi\Transport\NativeTransport;
use Phptg\BotApi\Transport\SaveFileException;
use Phptg\BotApi\Transport\TransportInterface;
use Phptg\BotApi\Type\AcceptedGiftTypes;
use Phptg\BotApi\Type\BotCommand;
Expand Down Expand Up @@ -310,38 +309,47 @@ public function makeFileUrl(string|File $file): string
}

/**
* Downloads a file from the Telegram servers and returns its content.
* Downloads a file from the Telegram servers and returns a {@see DownloadedFile} instance
* that provides access to the file content as a stream, a string or allows saving it to a local path.
*
* @param string|File $file File path or {@see File} object.
* @param string|File $file The file path (as returned by the Telegram API) or a {@see File} object.
*
* @return string The file content.
* @return DownloadedFile The {@see DownloadedFile} instance with a seekable stream ready for reading.
*
* @throws DownloadFileException If an error occurred while downloading the file.
* @throws LogicException If the file path is not specified in `File` object.
*/
public function downloadFile(string|File $file): string
public function downloadFile(string|File $file): DownloadedFile
{
return $this->transport->downloadFile(
$stream = $this->transport->downloadFile(
$this->makeFileUrl($file),
);
}

/**
* Downloads a file from the Telegram servers and saves it to a file.
*
* @param string|File $file File path or {@see File} object.
* @param string $savePath The path to save the file.
*
* @throws DownloadFileException If an error occurred while downloading the file.
* @throws SaveFileException If an error occurred while saving the file.
* @throws LogicException If the file path is not specified in `File` object.
*/
public function downloadFileTo(string|File $file, string $savePath): void
{
$this->transport->downloadFileTo(
$this->makeFileUrl($file),
$savePath,
);
$uri = stream_get_meta_data($stream)['uri'] ?? null;
if ($uri === 'php://temp' || $uri === 'php://memory') {
return new DownloadedFile($stream);
}

/**
* @var resource $temp `php://temp` always opens successfully.
*/
$temp = fopen('php://temp', 'r+b');

set_error_handler(
static function (int $errorNumber, string $errorString) use ($temp): never {
fclose($temp);
throw new DownloadFileException($errorString);
},
);
try {
stream_copy_to_stream($stream, $temp);
} finally {
restore_error_handler();
fclose($stream);
}

rewind($temp);

return new DownloadedFile($temp);
}

/**
Expand Down
53 changes: 10 additions & 43 deletions src/Transport/CurlTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,53 +78,16 @@ public function postWithFiles(string $url, array $data, array $files): ApiRespon
return $this->send($options);
}

public function downloadFile(string $url): string
public function downloadFile(string $url): mixed
{
$options = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FAILONERROR => true,
CURLOPT_SHARE => $this->curlShareHandle,
];

try {
$curl = $this->curl->init();
} catch (CurlException $exception) {
throw new DownloadFileException($exception->getMessage(), previous: $exception);
}

try {
$this->curl->setopt_array($curl, $options);

/**
* @var string $result `curl_exec` returns string because `CURLOPT_RETURNTRANSFER` is set to `true`.
*/
$result = $this->curl->exec($curl);
} catch (CurlException $exception) {
throw new DownloadFileException($exception->getMessage(), previous: $exception);
} finally {
$this->curl->close($curl);
}

return $result;
}

public function downloadFileTo(string $url, string $savePath): void
{
set_error_handler(
static function (int $errorNumber, string $errorString): bool {
throw new SaveFileException($errorString);
},
);
try {
$fileHandler = fopen($savePath, 'wb');
} finally {
restore_error_handler();
}
/**
* @var resource $stream `php://temp` always opens successfully.
*/
$stream = fopen('php://temp', 'r+b');

$options = [
CURLOPT_URL => $url,
CURLOPT_FILE => $fileHandler,
CURLOPT_FILE => $stream,
CURLOPT_FAILONERROR => true,
CURLOPT_SHARE => $this->curlShareHandle,
];
Expand All @@ -143,6 +106,10 @@ static function (int $errorNumber, string $errorString): bool {
} finally {
$this->curl->close($curl);
}

rewind($stream);

return $stream;
}

private function send(array $options): ApiResponse
Expand Down
22 changes: 4 additions & 18 deletions src/Transport/NativeTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public function postWithFiles(string $url, array $data, array $files): ApiRespon
);
}

public function downloadFile(string $url): string
public function downloadFile(string $url): mixed
{
set_error_handler(
static function (int $errorNumber, string $errorString): bool {
Expand All @@ -86,28 +86,14 @@ static function (int $errorNumber, string $errorString): bool {
);
try {
/**
* @var string We throw exception on error, so `file_get_contents()` returns string.
* @var resource $stream We throw exception on error, so `fopen()` returns resource.
*/
return file_get_contents($url);
$stream = fopen($url, 'rb');
} finally {
restore_error_handler();
}
}

public function downloadFileTo(string $url, string $savePath): void
{
$content = $this->downloadFile($url);

set_error_handler(
static function (int $errorNumber, string $errorString): bool {
throw new SaveFileException($errorString);
},
);
try {
file_put_contents($savePath, $content);
} finally {
restore_error_handler();
}
return $stream;
}

private function send(string $url, array $options): ApiResponse
Expand Down
Loading