src/UploadedFile.php
<?php
declare(strict_types=1);
namespace Furious\Psr7;
use Furious\Psr7\Exception\InvalidArgumentException;
use Furious\Psr7\Exception\InvalidUploadErrorException;
use Furious\Psr7\Exception\RuntimeException;
use Furious\Psr7\Exception\StreamAlreadyMovedException;
use Furious\Psr7\Exception\UploadErrorException;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
use function fopen;
use function is_resource;
use function is_string;
use function move_uploaded_file;
use function rename;
use function sprintf;
use const PHP_SAPI;
use const UPLOAD_ERR_CANT_WRITE;
use const UPLOAD_ERR_EXTENSION;
use const UPLOAD_ERR_FORM_SIZE;
use const UPLOAD_ERR_INI_SIZE;
use const UPLOAD_ERR_NO_FILE;
use const UPLOAD_ERR_NO_TMP_DIR;
use const UPLOAD_ERR_OK;
use const UPLOAD_ERR_PARTIAL;
class UploadedFile implements UploadedFileInterface
{
private int $error;
private ?int $size;
private bool $moved = false;
private ?string $file = null;
private ?string $clientFileName = null;
private ?string $clientMediaType = null;
private ?StreamInterface $stream = null;
private const ERROR_MESSAGES = [
UPLOAD_ERR_OK => 'The file successfully uploaded.',
UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.',
UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded.',
UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
UPLOAD_ERR_NO_TMP_DIR => 'Missing a temp folder.',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload.'
];
public function __construct(
$streamOrFile, ?int $size, int $error,
string $clientFileName = null, string $clientMediaType = null
)
{
if ($error < 0 or $error > 8) {
throw new InvalidUploadErrorException('Invalid error status for UploadedFile');
}
$this->error = $error;
if (UPLOAD_ERR_OK !== $error) {
throw new UploadErrorException(self::ERROR_MESSAGES[$this->error]);
}
$this->size = $size;
$this->clientFileName = $clientFileName;
$this->clientMediaType = $clientMediaType;
if (UPLOAD_ERR_OK === $this->error) {
$this->initializeStream($streamOrFile);
}
}
private function initializeStream($streamOrFile): void
{
if (is_string($streamOrFile)) {
$this->file = $streamOrFile;
} elseif (is_resource($streamOrFile)) {
$this->stream = Stream::new($streamOrFile);
} elseif ($streamOrFile instanceof StreamInterface) {
$this->stream = $streamOrFile;
} else {
throw new InvalidArgumentException('Invalid stream or file provided for UploadedFile');
}
}
// Get
public function getSize(): ?int
{
return $this->size;
}
public function getError(): int
{
return $this->error;
}
public function getClientFilename(): ?string
{
return $this->clientFileName;
}
public function getClientMediaType(): ?string
{
return $this->clientMediaType;
}
public function getStream(): StreamInterface
{
$this->validateActive();
if ($this->stream instanceof StreamInterface) {
return $this->stream;
}
$resource = fopen($this->file, 'r');
return Stream::new($resource);
}
// Move
public function moveTo($targetPath): void
{
$this->validateActive();
if (!is_string($targetPath) or '' === $targetPath) {
throw new InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
}
if (null !== $this->file) {
$this->setIsMovedByFile($targetPath);
} else {
$stream = $this->getStream();
if ($stream->isSeekable()) {
$stream->rewind();
}
$this->copyStreamContent($stream, $targetPath);
$this->moved = true;
}
$this->validateIsMoved($targetPath);
}
/**
* @param bool $moved
*/
public function setMoved(bool $moved): void
{
$this->moved = $moved;
}
// Validate
private function validateActive(): void
{
if (UPLOAD_ERR_OK !== $this->error) {
throw new UploadErrorException('Cannot retrieve stream due to upload error');
}
if ($this->moved) {
throw new StreamAlreadyMovedException('Cannot retrieve stream after it has already been moved');
}
}
private function validateIsMoved(string $targetPath): void
{
if (false === $this->moved) {
throw new RuntimeException(sprintf('Uploaded file could not be moved to %s', $targetPath));
}
}
// Copy
private function copyStreamContent(StreamInterface $stream, string $targetPath): void
{
$dest = Stream::new(fopen($targetPath, 'w'));
while (!$stream->eof()) {
if (!$dest->write($stream->read(1048576))) {
break;
}
}
}
private function setIsMovedByFile($targetPath): void
{
if ('cli' === PHP_SAPI) {
$this->moved = rename($this->file, $targetPath);
} else {
$this->moved = move_uploaded_file($this->file, $targetPath);
}
}
}