Создать поток из ресурса

Я знаю, что могу создать поток PHP из имени файла (реального или URL), используя функцию fopen:

$stream = fopen('php://temp', 'r');

Результирующий поток ($stream) является ресурсом типа "поток", созданным из URL php://temp,

Но как я могу создать поток, подобный выше, из ресурса?


Почему я спрашиваю это?

Я работаю над библиотекой PSR-7 и реализовал StreamInterface PSR-7 с Stream учебный класс. Для того, чтобы создать Stream случаи, когда я решил реализовать StreamFactory тоже. Его интерфейс, StreamFactoryInterface, определено в предложении PSR-17 (фабрики HTTP).

StreamFactoryInterface определяет метод с именем createStreamFromResourceкоторый - согласно его официальным комментариям - должен:

Создать новый поток из существующего ресурса.

Поток ДОЛЖЕН быть читаемым и может быть доступен для записи.

Таким образом, фабричный метод получает ресурс в качестве аргумента. И, в его конкретной реализации, новый Stream объект создан - который также получает ресурс в качестве аргумента.

Вот проблема:

Для простоты, скажем, что Stream Класс работает только с потоком, например, с ресурсом типа "поток". Если он получает ресурс, который не имеет типа "поток", он отклоняет его.

Итак, что если ресурсный аргумент createStreamFromResource это уже не ресурс типа "поток"? Как я могу преобразовать его в поток, например, в ресурс типа "поток", чтобы я мог передать его дальше, на вызов для создания нового Stream возразить с этим? Есть ли способ (метод PHP, функция или, возможно, функция приведения) для достижения этой задачи?

Заметки:

  • Для наглядности подготовил полный пример (testStream.php) как я создаю поток, например Stream Например, тремя способами: один раз напрямую и дважды с помощью фабрики потоков.
  • Я также публикую конкретную реализацию фабричного интерфейса: класс StreamFactory с методом createStreamFromResource, Вызов этого метода должен быть моим четвертым способом создания потока в testStream.php,
  • Кроме того, я представляю классы Stream а также Response, так что вы можете напрямую проверить все, если хотите. Эти два класса - очень упрощенная версия моего реального кода.
  • В своих кодах я пометил два места для опроса "@asking".

Большое спасибо за ваше время и терпение!


testStream.php (тестовая страница)

<?php

use Tests\Stream;
use Tests\Response;
use Tests\StreamFactory;

/*
 * ================================================
 * Option 1: Create a stream by a stream name
 * (like "php://temp") with read and write rights.
 * ================================================
 */
$stream = new Stream('php://temp', 'w+b');

$response = new Response($stream);
$response->getBody()->write(
        'Stream 1: Created directly.<br/><br/>'
);
echo $response->getBody();

/*
 * ================================================
 * Option 2: Create a stream by a stream name
 * (like "php://temp"), using a stream factory.
 * ================================================
 */
$streamFactory = new StreamFactory();
$stream = $streamFactory->createStreamFromFile('php://temp', 'w+b');

$response = new Response($stream);
$response->getBody()->write(
        'Stream 2: Created by a stream name, with a stream factory.<br/><br/>'
);
echo $response->getBody();

/*
 * ================================================
 * Option 3: Create a stream from a string, using a
 * stream factory.
 * ================================================
 */
$streamFactory = new StreamFactory();
$stream = $streamFactory->createStream(
        'Stream 3: Created from a string, with a stream factory.<br/><br/>'
);

$response = new Response($stream);
echo $response->getBody();

/*
 * ================================================
 * Option 4: Create a stream from an existing
 * resource, using a stream factory.
 * ================================================
 * 
 * @asking How can I create a stream by calling the
 * the factory method ServerFactory::createStreamFromResource
 * with a resource which is not of type "stream"?
 */
//...

Класс StreamFactory (как он у меня есть, поэтому не упрощенный)

<?php

namespace Tests;

use Tests\Stream;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\StreamFactoryInterface;

class StreamFactory implements StreamFactoryInterface {

    /**
     * Create a new stream from an existing resource.
     *
     * The stream MUST be readable and may be writable.
     *
     * @param resource $resource
     *
     * @return StreamInterface
     * @throws \InvalidArgumentException
     */
    public function createStreamFromResource($resource) {
        /*
         * @asking What if $resource is not already a resource of type *"stream"*? 
         * How can I transform it into a stream, e.g. into a resource of type *"stream"*, 
         * so that I can pass it further, to the call for creating a new `Stream` object 
         * with it? Is there a way (a PHP method, a function, or maybe a casting function) 
         * of achieving this task?
         */
         //...

        return new Stream($resource, 'w+b');
    }

    /**
     * Create a new stream from a string.
     *
     * The stream SHOULD be created with a temporary resource.
     *
     * @param string $content
     *
     * @return StreamInterface
     * @throws \InvalidArgumentException
     */
    public function createStream($content = '') {
        if (!isset($content) || !is_string($content)) {
            throw new \InvalidArgumentException('For creating a stream, a content string must be provided!');
        }

        $stream = $this->createStreamFromFile('php://temp', 'w+b');

        $stream->write($content);

        return $stream;
    }

    /**
     * Create a stream from an existing file.
     *
     * The file MUST be opened using the given mode, which may be any mode
     * supported by the `fopen` function.
     *
     * The `$filename` MAY be any string supported by `fopen()`.
     *
     * @param string $filename
     * @param string $mode
     *
     * @return StreamInterface
     */
    public function createStreamFromFile($filename, $mode = 'r') {
        return new Stream($filename, $mode);
    }

}

Класс Stream (очень упрощенный)

<?php

namespace Tests;

use Psr\Http\Message\StreamInterface;

class Stream implements StreamInterface {

    /**
     * Stream (resource).
     *
     * @var resource
     */
    private $stream;

    /**
     *
     * @param string|resource $stream Stream name, or resource.
     * @param string $accessMode (optional) Access mode.
     * @throws \InvalidArgumentException
     */
    public function __construct($stream, string $accessMode = 'r') {
        if (
                !isset($stream) ||
                (!is_string($stream) && !is_resource($stream))
        ) {
            throw new \InvalidArgumentException(
                'The provided stream must be a filename, or an opened resource of type "stream"!'
            );
        }

        if (is_string($stream)) {
            $this->stream = fopen($stream, $accessMode);
        } elseif (is_resource($stream)) {
            if ('stream' !== get_resource_type($stream)) {
                throw new \InvalidArgumentException('The provided resource must be of type "stream"!');
            }

            $this->stream = $stream;
        }
    }

    /**
     * Write data to the stream.
     *
     * @param string $string The string that is to be written.
     * @return int Returns the number of bytes written to the stream.
     * @throws \RuntimeException on failure.
     */
    public function write($string) {
        return fwrite($this->stream, $string);
    }

    /**
     * Reads all data from the stream into a string, from the beginning to end.
     *
     * @return string
     */
    public function __toString() {
        try {
            // Rewind the stream.
            fseek($this->stream, 0);

            // Get the stream contents as string.
            $contents = stream_get_contents($this->stream);

            return $contents;
        } catch (\RuntimeException $exc) {
            return '';
        }
    }

    public function close() {}
    public function detach() {}
    public function eof() {}
    public function getContents() {}
    public function getMetadata($key = null) {}
    public function getSize() {}
    public function isReadable() {}
    public function isSeekable() {}
    public function isWritable() {}
    public function read($length) {}
    public function rewind() {}
    public function seek($offset, $whence = SEEK_SET) {}
    public function tell() {}

}

Класс Response (очень упрощенный)

<?php

namespace Tests;

use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\ResponseInterface;

class Response implements ResponseInterface {

    /**
     *
     * @param StreamInterface $body Message body.
     */
    public function __construct(StreamInterface $body) {
        $this->body = $body;
    }

    /**
     * Gets the body of the message.
     *
     * @return StreamInterface Returns the body as a stream.
     */
    public function getBody() {
        return $this->body;
    }

    public function getHeader($name) {}
    public function getHeaderLine($name) {}
    public function getHeaders() {}
    public function getProtocolVersion() {}
    public function hasHeader($name) {}
    public function withAddedHeader($name, $value) {}
    public function withBody(StreamInterface $body) {}
    public function withHeader($name, $value) {}
    public function withProtocolVersion($version) {}
    public function withoutHeader($name) {}
    public function getReasonPhrase() {}
    public function getStatusCode() {}
    public function withStatus($code, $reasonPhrase = '') {}

}

3 ответа

Решение

То, как вы обрабатываете переданный аргумент, зависит от вашей окончательной реализации. Если ваш код ожидает потоковый аргумент, он должен остановиться, когда не обнаружит такой вещи. Но если ваш код должен решить проблему, то вы можете попытаться создать поток.

редактировать

Не получил его с самого начала, но похоже, что вопрос был в том, возможно ли преобразовать переменные ресурса. Согласно документации это невозможно и не имеет смысла.

Есть немало очень хороших реализаций PSR-7 StreamInterface Я бы порекомендовал посмотреть сначала. Вы можете получить некоторые идеи о том, какую проверку и логику вам нужно сделать.

Обновление: просмотрев все эти ссылки, я обнаружил некоторые проблемы с вашим текущим кодом:

  • Вы должны проверить тип ресурса в вашем конструкторе. Например, это может быть ресурс MySQL, и вы не хотите писать в него:

    public function __construct($stream, string $accessMode = 'r') {
    
        if (is_string($stream)) {
            $stream = fopen($stream, $accessMode);
        }
    
        if (! is_resource($stream) || 'stream' !== get_resource_type($stream)) {
            throw new InvalidArgumentException(
                'Invalid stream provided; must be a string stream identifier or stream resource'
            );
        }
    
        $this->stream = $stream;
    }
    
  • При записи в поток проверьте, действительно ли поток доступен для записи. Вы должны реализовать isWritable метод первым и вызовите его в своем write функция. Этот пример взят из библиотеки zend-diactoros:

    public function isWritable()
    {
        if (! $this->resource) {
            return false;
        }
        $meta = stream_get_meta_data($this->resource);
        $mode = $meta['mode'];
        return (
            strstr($mode, 'x')
            || strstr($mode, 'w')
            || strstr($mode, 'c')
            || strstr($mode, 'a')
            || strstr($mode, '+')
        );
    }
    
  • То же самое с read а также seek функции, которые вы должны реализовать isSeekable а также isReadable первый.

  • __toString Также следует проверить, доступен ли поток для чтения и поиска:

    public function __toString()
    {
        if (! $this->isReadable()) {
            return '';
        }
        try {
            if ($this->isSeekable()) {
                $this->rewind();
            }
            return $this->getContents();
        } catch (RuntimeException $e) {
            return '';
        }
    } 
    

Надеюсь это поможет. Удачи в вашей новой библиотеке.

Вы можете реализовать его любым удобным вам способом, но этот метод по сути является просто оболочкой для предварительно сгенерированного ресурса.

В большинстве случаев ваш поток, вероятно, будет принимать строку и, возможно, массив настроек / опций и создавать поток из информации (возможно, с fopen('http://...') где-то по пути.

CreateStreamFromResource($resource) будет принимать предварительно сгенерированный ресурс (например, возвращаемое значение ресурса из fopen а не данные для выполнения fopen):

class Stream implements StreamInterface {

    // ...

    public function __construct($url, $opt = null) {

        // ...

        if( is_resource( $url ) ) {

            /*
             * Check that the $resource is a valid resource
             * (e.g. an http request from an fopen call vs a mysql resource.)
             * or possibly a stream context that still needs to create a
             * request...
             */

            if( !$isValid ) {
                return false;
            }

            $this->resource = $resource;

        } else {

            // ...

            $this->resource = fopen($url, $modifiedOpt);

            // ...

        }

    }

    // ...

    /* createStreamFromResource would call Stream::fromResource($r)
     * or possibly Stream($resource) directly, your call.
     */
    static function fromResource($resource) {
        return new static($resource);
    }

}

Ваш фабричный метод может быть таким простым:

public function createStreamFromResource($resource) {
    return Stream::fromResource($resource);
}
Другие вопросы по тегам