Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create a stream from a resource

I know that I can create a PHP stream from a filename (a real one, or an URL), by using the fopen function:

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

The resulting stream ($stream) is then a resource of type "stream", created from the URL php://temp.

But how I can create a stream like the above from a resource?


Why am I asking this?

I am working on a PSR-7 library and I implemented the PSR-7 StreamInterface with a Stream class. In order to create Stream instances, I decided to implement a StreamFactory too. Its interface, StreamFactoryInterface, is defined in PSR-17: HTTP Factories.

The StreamFactoryInterface defines a method named createStreamFromResource, which - conform to its official comments - should:

Create a new stream from an existing resource.

The stream MUST be readable and may be writable.

So the factory method receives a resource as argument. And, in its concrete implementation, a new Stream object is created - which receives a resource as argument, too.

Here is the problem:

For the sake of simplicity, let's say that the Stream class works only with a stream, e.g. with a resource of type "stream". If it receives a resource which is not of type "stream", it rejects it.

So, what if the resource argument of createStreamFromResource 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?

Notes:

  • For clarity, I prepared a complete example (testStream.php) of how I create a stream, e.g. a Stream instance, in three ways: once directly, and twice using the stream factory.
  • I also post the concrete implementation of the factory interface: the class StreamFactory with the method createStreamFromResource. A call to this method should be my fourth way of creating a stream in testStream.php.
  • Furthermore I present the classes Stream and Response, so that you can directly test all, if you wish. The two classes are a very simplified version of my real code.
  • In my codes I tagged the two questioning places with "@asking".

Thank you very much for your time and patience!


testStream.php (the testing page):

<?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"?
 */
//...

The StreamFactory class (as I have it, so not simplified):

<?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);
    }

}

The Stream class (very simplified):

<?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() {}

}

The Response class (very simplified):

<?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 = '') {}

}
like image 915
dakis Avatar asked Mar 30 '18 12:03

dakis


People also ask

What is a stream resource?

Streams are a way of generalising file, network, compression resources and a few other things in a way that allows them to share a common set of features. I stream is a resource object that has streamable behaviour. It can be read or written to in a linear fashion, but not necessarily from the beginning of the stream.

What is a PHP stream?

PHP Stream Introduction Streams are the way of generalizing file, network, data compression, and other operations which share a common set of functions and uses. In its simplest definition, a stream is a resource object which exhibits streamable behavior.


2 Answers

How you handle the passed argument depends on your final implementation. If your code expects a stream argument then it should stop when it detects no such thing. But if your code is expected to handle the issue then you can try to create a stream.

Edit

Didn't get it from the start but it looks like the question was if it is possible to convert resource variables. According to the documentation that is not possible and doesn't make sense.

like image 155
EmilCataranciuc Avatar answered Sep 22 '22 07:09

EmilCataranciuc


There are quite a few very good implementation of the PSR-7 StreamInterface I would recommend to look at first. You might get some ideas of what kind of validation and logic you need to do.

  • guzzle/psr7 - Guzzle implementation of the PSR-7 StreamInterface
  • reactphp/stream - This one doesn't implement PSR-7, but the guys put a lot of thoughts into their implementation and code is very well documented. Look at ReadableResourceStream and WritableResourceStream.
  • zendframework/zend-diactoros
  • slimphp/Slim

Update: After looking at all these links I spotted some issues with your current code:

  • You have to check for the resource type in your constructor. It might be a MySQL resource for example and you don't want to write to it:

    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;
    }
    
  • When you writing to a stream check if the stream is actually writable. You have to implement isWritable method first and call it in your write function. This example is taken from zend-diactoros library:

    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, '+')
        );
    }
    
  • Same with read and seek functions you have to implement isSeekable and isReadable first.

  • __toString should also check if the stream is readable and seekable:

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

Hope this helps. Good luck with your new library.

like image 20
zstate Avatar answered Sep 26 '22 07:09

zstate