Create a stream from a resource
Asked Answered
L

3

16

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

}
Llanes answered 30/3, 2018 at 12:50 Comment(3)
For the users who, maybe, decide to downvote my question: Please let me know the motive of your downvote, so that I can change my question correspondingly. I am open to all your suggestions or critiques, but please be fair and give me the chance of knowing your perspective. This way we can contribute together to the continuous improvement of this website. Thank you.Llanes
can you give me example of what you want to do in a practical wayMickimickie
@ManishDhruw I reedited my question with more details and... a lot of code. I thought it would be better for all users to have a complete example of what I am trying to accomplish. Thanks again for your question.Llanes
E
1

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.

Evangelistic answered 7/4, 2018 at 15:0 Comment(16)
Thank you, Emil. My Stream class works with a stream of type "stream". But it expects either a string (on which I can call fopen to transform it into a stream of type "stream"), or a resouce (a stream of type "stream", or a resource of another type). If the resource is not a stream of type "stream", then I have two options: to reject it (by throwing an exception, or similar), or to transform it, somehow, to a stream of type "stream". This "somehow" word is the key: Is there a way to transform a resource which is not of type "stream" into a stream of type "stream"?Llanes
The story with the StreamFactory is technically easy. If I can handle the above presented situation directly within the Stream class, then I can pass the $resource argument of the factory directly to the new Stream($resource,...) call.Llanes
Guessing is always tricky. I recommend throwing an error because believe me, you won't be able to guess every scenario. Often doing less but doing it in predictable way is the best way to go. I suspect I don't have to mention how crucial documentation and comment are.Evangelistic
I like a lot your perspective. And your advices, Emil. Though, here is nothing to be guessed. If I receive a string as the Stream's constructor argument, I know how to transform it in a stream. And, when I receive a resource, I can find exactly its type. What I don't know is, if there is a way of transforming it in a stream of type "stream". A PHP method, a function, or maybe a casting function.Llanes
You can check for resource with is_resource($a) || ($a !== null && !is_scalar($a) && !is_array($a) && !is_object($a)) check explanation here: php.net/manual/en/function.is-resource.php third comment. I suspect you are aware of the fact that PSR 17 is a draft... I believe StreamFactory::createStreamFromResource is meant as wrapper.Evangelistic
I am a bit confused about this is not already a resource of type "stream". From PHP docs: "a stream is a resource object which exhibits streamable behavior". A resource can not be of type stream. They can be streamable.Evangelistic
Thanks again. Yes, I know that is a draft. But a very good one :-) In its form, actually by its solely existence, it solves a big problem - which I discovered trying to find out how to create a JSON response: the fact that the classes of an app would be tightly coupled with the Stream class without it. After research I found this article presenting the problem. That's how I discovered the PSR-17, actually.Llanes
My pleasure. Not saying it is bad just wanted to make sure you know the state of it. I added another answer since I think this one evolved a little bit in the wrong direction initially.Evangelistic
I am a bit confused too, actually :-) Well, a resource can be of many types: a mysql link, a domxml document, a stream, etc (see Example #1 here). When we speak about a stream (as a construct manifesting a streamable behaviour) we actually speak about a resource of type "stream". If a resource is not of type "stream", then it's not streamable.Llanes
The confusing part to me is the fact, that it's not clear for me, if a resource other than of type "stream" can be transformed to a resource of type "stream" (e.g. to a stream), or not. If there is no way of doing such a transformation/casting, then all the mistery is solved: every resource which is not of type "stream" (or opened) is to be rejected by the Stream object. But, if there is a way of transformation, then I can pass the result of it, e.g. the resource of type "stream", to Stream.Llanes
Regarding your confusing part: When we say: A resource is of type "stream", then we speak about: The result of the function get_resource_type($resource) is the string "stream". E.g. $resource is a resource of type "stream". E.g. $resource is a stream. E.g. $resource exhibits streamable behaviour. P.S: I didn't assume that you were saying that beeing a draft is bad ;-) And I am grateful to your efforts for helping me find the solution.Llanes
No, it is not possible to convert resource types: php.net/manual/en/language.types.resource.phpEvangelistic
Oh :-) Emil, you just made my day. I missed it by miles, even if I read it. Would you mind changing/extending your answer, so it reflects the solution? Again: thank you very much for everything. Tomorrow I will transfer the bounty to your answer.Llanes
Done. Glad I could help. Thank you for the bounty. :-) Take care.Evangelistic
You too :-) It was my pleasure. Good luck!Llanes
You could extract the full URL/path from the original resource with stream_get_meta_data($stream) and then simply close it and open up a new resource with new Stream($extracted_old_resource_path, 'w+b'); - which would ensure that the read/write mode is applied correctly. But you never mentioned any error, so hard to really grasp what you need. I did something similar using the decorator-pattern. My base class just had a (public/protected) resource property and basic IO-methods while the extending classes each use a unique constructor for file/resource/string input.Clarino
M
2

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.

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.

Muddler answered 3/4, 2018 at 0:23 Comment(3)
Thank you very much for your answer, ztate. These links are good, indeed.Llanes
@dakis See my updated answer. Hope it would help you.Muddler
Hi, zstate. I accepted Emil's answer, but I wanted to thank you again for the great links and code proposals! Good luck.Llanes
C
1

You can implement it any way you like but this method is essentially just a wrapper for a pre-generated resource.

In most cases, your Stream is probably going to take in a string and possibly a settings/options array and create a stream from the information (possibly with an fopen('http://...') somewhere along the way.

The createStreamFromResource($resource) would take in a pre-generated resource (e.g. the return resource value from an fopen rather than the data to execute the 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);
    }

}

Your factory method could be something as simple as:

public function createStreamFromResource($resource) {
    return Stream::fromResource($resource);
}
Cardenas answered 31/3, 2018 at 3:40 Comment(1)
I accepted Emil's answer, but I wanted to thank you again, Jim, for a good answer. I really appreciate it! Good luck.Llanes
E
1

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.

Evangelistic answered 7/4, 2018 at 15:0 Comment(16)
Thank you, Emil. My Stream class works with a stream of type "stream". But it expects either a string (on which I can call fopen to transform it into a stream of type "stream"), or a resouce (a stream of type "stream", or a resource of another type). If the resource is not a stream of type "stream", then I have two options: to reject it (by throwing an exception, or similar), or to transform it, somehow, to a stream of type "stream". This "somehow" word is the key: Is there a way to transform a resource which is not of type "stream" into a stream of type "stream"?Llanes
The story with the StreamFactory is technically easy. If I can handle the above presented situation directly within the Stream class, then I can pass the $resource argument of the factory directly to the new Stream($resource,...) call.Llanes
Guessing is always tricky. I recommend throwing an error because believe me, you won't be able to guess every scenario. Often doing less but doing it in predictable way is the best way to go. I suspect I don't have to mention how crucial documentation and comment are.Evangelistic
I like a lot your perspective. And your advices, Emil. Though, here is nothing to be guessed. If I receive a string as the Stream's constructor argument, I know how to transform it in a stream. And, when I receive a resource, I can find exactly its type. What I don't know is, if there is a way of transforming it in a stream of type "stream". A PHP method, a function, or maybe a casting function.Llanes
You can check for resource with is_resource($a) || ($a !== null && !is_scalar($a) && !is_array($a) && !is_object($a)) check explanation here: php.net/manual/en/function.is-resource.php third comment. I suspect you are aware of the fact that PSR 17 is a draft... I believe StreamFactory::createStreamFromResource is meant as wrapper.Evangelistic
I am a bit confused about this is not already a resource of type "stream". From PHP docs: "a stream is a resource object which exhibits streamable behavior". A resource can not be of type stream. They can be streamable.Evangelistic
Thanks again. Yes, I know that is a draft. But a very good one :-) In its form, actually by its solely existence, it solves a big problem - which I discovered trying to find out how to create a JSON response: the fact that the classes of an app would be tightly coupled with the Stream class without it. After research I found this article presenting the problem. That's how I discovered the PSR-17, actually.Llanes
My pleasure. Not saying it is bad just wanted to make sure you know the state of it. I added another answer since I think this one evolved a little bit in the wrong direction initially.Evangelistic
I am a bit confused too, actually :-) Well, a resource can be of many types: a mysql link, a domxml document, a stream, etc (see Example #1 here). When we speak about a stream (as a construct manifesting a streamable behaviour) we actually speak about a resource of type "stream". If a resource is not of type "stream", then it's not streamable.Llanes
The confusing part to me is the fact, that it's not clear for me, if a resource other than of type "stream" can be transformed to a resource of type "stream" (e.g. to a stream), or not. If there is no way of doing such a transformation/casting, then all the mistery is solved: every resource which is not of type "stream" (or opened) is to be rejected by the Stream object. But, if there is a way of transformation, then I can pass the result of it, e.g. the resource of type "stream", to Stream.Llanes
Regarding your confusing part: When we say: A resource is of type "stream", then we speak about: The result of the function get_resource_type($resource) is the string "stream". E.g. $resource is a resource of type "stream". E.g. $resource is a stream. E.g. $resource exhibits streamable behaviour. P.S: I didn't assume that you were saying that beeing a draft is bad ;-) And I am grateful to your efforts for helping me find the solution.Llanes
No, it is not possible to convert resource types: php.net/manual/en/language.types.resource.phpEvangelistic
Oh :-) Emil, you just made my day. I missed it by miles, even if I read it. Would you mind changing/extending your answer, so it reflects the solution? Again: thank you very much for everything. Tomorrow I will transfer the bounty to your answer.Llanes
Done. Glad I could help. Thank you for the bounty. :-) Take care.Evangelistic
You too :-) It was my pleasure. Good luck!Llanes
You could extract the full URL/path from the original resource with stream_get_meta_data($stream) and then simply close it and open up a new resource with new Stream($extracted_old_resource_path, 'w+b'); - which would ensure that the read/write mode is applied correctly. But you never mentioned any error, so hard to really grasp what you need. I did something similar using the decorator-pattern. My base class just had a (public/protected) resource property and basic IO-methods while the extending classes each use a unique constructor for file/resource/string input.Clarino

© 2022 - 2024 — McMap. All rights reserved.