fix!: handle php errors with custom error handling
This commit changes the api of the stream classes, since indication of success or failure is no longer necessary.
This commit is contained in:
@@ -50,6 +50,10 @@ final class Child
|
||||
*/
|
||||
public function __construct(mixed $process, array $pipes)
|
||||
{
|
||||
if (get_resource_type($process) !== 'process') {
|
||||
throw new ChildException('invalid resource type: not a process');
|
||||
}
|
||||
|
||||
$this->process = $process;
|
||||
|
||||
$status = proc_get_status($this->process);
|
||||
@@ -94,10 +98,6 @@ final class Child
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
if (!is_resource($this->process)) {
|
||||
throw new ChildException('Resource was already closed');
|
||||
}
|
||||
|
||||
// Avoid possible deadlock before waiting.
|
||||
$this->stdin?->close();
|
||||
|
||||
@@ -165,7 +165,7 @@ final class Child
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if (is_resource($this->process)) {
|
||||
if (get_resource_type($this->process) === 'process') {
|
||||
proc_close($this->process);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use Override;
|
||||
|
||||
final class ChildStderr implements StdioInterface
|
||||
{
|
||||
use StreamReadable;
|
||||
use StreamReadTrait;
|
||||
|
||||
#[Override]
|
||||
public function getDescriptionSpec(int $fd): mixed
|
||||
|
||||
@@ -8,7 +8,7 @@ use Override;
|
||||
|
||||
final class ChildStdin implements StdioInterface
|
||||
{
|
||||
use StreamWritable;
|
||||
use StreamWriteTrait;
|
||||
|
||||
#[Override]
|
||||
public function getDescriptionSpec(int $fd): mixed
|
||||
|
||||
@@ -8,7 +8,7 @@ use Override;
|
||||
|
||||
final class ChildStdout implements StdioInterface
|
||||
{
|
||||
use StreamReadable;
|
||||
use StreamReadTrait;
|
||||
|
||||
#[Override]
|
||||
public function getDescriptionSpec(int $fd): mixed
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Nih\CommandBuilder;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Override;
|
||||
use Stringable;
|
||||
use ValueError;
|
||||
@@ -202,10 +203,19 @@ final class Command implements Stringable
|
||||
*
|
||||
* Defaults to inherit when used with spawn or status, and defaults to piped
|
||||
* when used with output.
|
||||
*
|
||||
* @param StdioInterface|resource $stdin
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* When the provided stdin handle is neither a StdioInterface nor a live
|
||||
* stream resource.
|
||||
*/
|
||||
public function stdin(StdioInterface $stdin): static
|
||||
public function stdin(mixed $stdin): static
|
||||
{
|
||||
$this->stdin = $stdin;
|
||||
$this->stdin = $stdin instanceof StdioInterface
|
||||
? $stdin
|
||||
: Stdio::stream($stdin);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -214,10 +224,19 @@ final class Command implements Stringable
|
||||
*
|
||||
* Defaults to inherit when used with spawn or status, and defaults to piped
|
||||
* when used with output.
|
||||
*
|
||||
* @param StdioInterface|resource $stdout
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* When the provided stdout handle is neither a StdioInterface nor a live
|
||||
* stream resource.
|
||||
*/
|
||||
public function stdout(StdioInterface $stdout): static
|
||||
public function stdout(mixed $stdout): static
|
||||
{
|
||||
$this->stdout = $stdout;
|
||||
$this->stdout = $stdout instanceof StdioInterface
|
||||
? $stdout
|
||||
: Stdio::stream($stdout);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -226,10 +245,19 @@ final class Command implements Stringable
|
||||
*
|
||||
* Defaults to inherit when used with spawn or status, and defaults to piped
|
||||
* when used with output.
|
||||
*
|
||||
* @param StdioInterface|resource $stderr
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* When the provided stderr handle is neither a StdioInterface nor a live
|
||||
* stream resource.
|
||||
*/
|
||||
public function stderr(StdioInterface $stderr): static
|
||||
public function stderr(mixed $stderr): static
|
||||
{
|
||||
$this->stderr = $stderr;
|
||||
$this->stderr = $stderr instanceof StdioInterface
|
||||
? $stderr
|
||||
: Stdio::stream($stderr);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -385,6 +413,8 @@ final class Command implements Stringable
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
set_error_handler(CommandException::handleError(...));
|
||||
$proc = proc_open(
|
||||
[$program, ...$this->args],
|
||||
$descriptorSpec,
|
||||
@@ -392,14 +422,12 @@ final class Command implements Stringable
|
||||
$this->cwd,
|
||||
$environment,
|
||||
);
|
||||
|
||||
if ($proc === false) {
|
||||
throw new CommandException(sprintf(
|
||||
'Program "%s" failed to start',
|
||||
$this->program,
|
||||
));
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
assert($proc !== false);
|
||||
|
||||
return new Child($proc, $pipes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,13 @@ namespace Nih\CommandBuilder;
|
||||
use RuntimeException;
|
||||
|
||||
class CommandException extends RuntimeException
|
||||
{}
|
||||
{
|
||||
public static function handleError(int $errno, string $errstr, string $file, int $line): never
|
||||
{
|
||||
$exception = new self($errstr);
|
||||
$exception->file = $file;
|
||||
$exception->line = $line;
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,22 +100,12 @@ abstract class Stdio
|
||||
*
|
||||
* @param resource $stream
|
||||
*
|
||||
* @throws InvalidArgumentException When stream is not a live stream resource
|
||||
* @throws InvalidArgumentException When stream is not a stream resource
|
||||
*/
|
||||
public static function stream($stream): StdioInterface
|
||||
{
|
||||
$isResource = is_resource($stream)
|
||||
|| ($stream !== null
|
||||
&& !is_scalar($stream)
|
||||
&& !is_array($stream)
|
||||
&& !is_object($stream));
|
||||
|
||||
if (!$isResource) {
|
||||
throw new InvalidArgumentException('not a resource');
|
||||
}
|
||||
|
||||
if (get_resource_type($stream) !== 'stream') {
|
||||
throw new InvalidArgumentException('resource is not a stream or was closed');
|
||||
throw new InvalidArgumentException('resource is not a stream');
|
||||
}
|
||||
|
||||
return new class($stream) implements StdioInterface {
|
||||
|
||||
@@ -14,7 +14,7 @@ use Stringable;
|
||||
interface StdioInterface
|
||||
{
|
||||
/**
|
||||
* @return resource|array{0: 'file', 1: string, 2: string}|array{0: 'piped', 1: string}
|
||||
* @return resource|array{0: 'file', 1: string, 2: string}|array{0: 'pipe', 1: string}
|
||||
*/
|
||||
public function getDescriptionSpec(int $fd): mixed;
|
||||
}
|
||||
|
||||
10
src/StreamException.php
Normal file
10
src/StreamException.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nih\CommandBuilder;
|
||||
|
||||
/**
|
||||
* An error prevented reading from or writing to a stream resource.
|
||||
*/
|
||||
class StreamException extends CommandException {}
|
||||
52
src/StreamReadTrait.php
Normal file
52
src/StreamReadTrait.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nih\CommandBuilder;
|
||||
|
||||
trait StreamReadTrait
|
||||
{
|
||||
use StreamTrait;
|
||||
|
||||
/**
|
||||
* @throws StreamException
|
||||
*/
|
||||
public function read(int $length): ?string
|
||||
{
|
||||
if (get_resource_type($this->stream) !== 'stream') {
|
||||
throw new StreamException('the stream was closed');
|
||||
}
|
||||
|
||||
try {
|
||||
set_error_handler(StreamException::handleError(...));
|
||||
$bytes = fread($this->stream, $length);
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
assert($bytes !== false);
|
||||
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StreamException
|
||||
*/
|
||||
public function getContents(?int $length = null, int $offset = -1): string
|
||||
{
|
||||
if (get_resource_type($this->stream) !== 'stream') {
|
||||
throw new StreamException('the stream was closed');
|
||||
}
|
||||
|
||||
try {
|
||||
set_error_handler(StreamException::handleError(...));
|
||||
$contents = stream_get_contents($this->stream, $length, $offset);
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
assert($contents !== false);
|
||||
|
||||
return $contents;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nih\CommandBuilder;
|
||||
|
||||
trait StreamReadable
|
||||
{
|
||||
/**
|
||||
* @param resource $stream
|
||||
*/
|
||||
public function __construct(public readonly mixed $stream)
|
||||
{
|
||||
}
|
||||
|
||||
public function read(int $length): ?string
|
||||
{
|
||||
if (!is_resource($this->stream)) {
|
||||
throw new CommandException('Cannot read from closed stream');
|
||||
}
|
||||
|
||||
return fread($this->stream, $length) ?: null;
|
||||
}
|
||||
|
||||
public function getContents(?int $length = null, int $offset = -1): ?string
|
||||
{
|
||||
if (!is_resource($this->stream)) {
|
||||
throw new CommandException('Cannot read from closed stream');
|
||||
}
|
||||
|
||||
$contents = stream_get_contents($this->stream, $length, $offset);
|
||||
return $contents === false
|
||||
? null
|
||||
: $contents;
|
||||
}
|
||||
|
||||
public function close(): bool
|
||||
{
|
||||
return is_resource($this->stream)
|
||||
? fclose($this->stream)
|
||||
: true;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
}
|
||||
52
src/StreamTrait.php
Normal file
52
src/StreamTrait.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nih\CommandBuilder;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
trait StreamTrait
|
||||
{
|
||||
/**
|
||||
* @param resource $stream
|
||||
*
|
||||
* @throws InvalidArgumentException When stream is not a stream
|
||||
*/
|
||||
public function __construct(private readonly mixed $stream)
|
||||
{
|
||||
if (get_resource_type($stream) !== 'stream') {
|
||||
throw new InvalidArgumentException('resource is not a stream');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the stream.
|
||||
*
|
||||
* Noop if the stream was already closed.
|
||||
*
|
||||
* @throws StreamException
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
if (get_resource_type($this->stream) !== 'stream') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
set_error_handler(StreamException::handleError(...));
|
||||
$success = fclose($this->stream);
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
assert($success);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if (get_resource_type($this->stream) === 'stream') {
|
||||
fclose($this->stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nih\CommandBuilder;
|
||||
|
||||
trait StreamWritable
|
||||
{
|
||||
/**
|
||||
* @param resource $stream
|
||||
*/
|
||||
public function __construct(public readonly mixed $stream)
|
||||
{
|
||||
}
|
||||
|
||||
public function write(string $data, ?int $length = null): ?int
|
||||
{
|
||||
if (!is_resource($this->stream)) {
|
||||
throw new CommandException('Cannot write to closed stream');
|
||||
}
|
||||
|
||||
$bytes = fwrite($this->stream, $data, $length) ?: null;
|
||||
return $bytes === false ? null : $bytes;
|
||||
}
|
||||
|
||||
public function flush(): bool
|
||||
{
|
||||
if (!is_resource($this->stream)) {
|
||||
throw new CommandException('Cannot flush closed stream');
|
||||
}
|
||||
|
||||
return fflush($this->stream);
|
||||
}
|
||||
|
||||
public function close(): bool
|
||||
{
|
||||
return is_resource($this->stream)
|
||||
? fclose($this->stream)
|
||||
: true;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
}
|
||||
50
src/StreamWriteTrait.php
Normal file
50
src/StreamWriteTrait.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nih\CommandBuilder;
|
||||
|
||||
trait StreamWriteTrait
|
||||
{
|
||||
use StreamTrait;
|
||||
|
||||
/**
|
||||
* @throws StreamException
|
||||
*/
|
||||
public function write(string $data, ?int $length = null): int
|
||||
{
|
||||
if (get_resource_type($this->stream) !== 'stream') {
|
||||
throw new StreamException('the stream was closed');
|
||||
}
|
||||
|
||||
try {
|
||||
set_error_handler(StreamException::handleError(...));
|
||||
$bytes = fwrite($this->stream, $data, $length);
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
assert($bytes !== false);
|
||||
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StreamException
|
||||
*/
|
||||
public function flush(): void
|
||||
{
|
||||
if (get_resource_type($this->stream) !== 'stream') {
|
||||
throw new StreamException('the stream was closed');
|
||||
}
|
||||
|
||||
try {
|
||||
set_error_handler(StreamException::handleError(...));
|
||||
$success = fflush($this->stream);
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
assert($success);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user