web-dev-qa-db-fra.com

Créer un flux à partir d'une ressource

Je sais que je peux créer un flux PHP à partir d'un nom de fichier (un vrai, ou une URL), en utilisant la fonction fopen :

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

Le flux résultant ($stream) est alors un ressource de type "stream", créé à partir de l'URL php://temp.

Mais comment puis-je créer un flux comme ci-dessus à partir d'une ressource?


Pourquoi je demande ça?

Je travaille sur une bibliothèque PSR-7 et j’ai implémenté la classe PSR-7 StreamInterface avec une classe Stream. Afin de créer des instances Stream, j'ai décidé d'implémenter aussi une StreamFactory. Son interface, StreamFactoryInterface, est définie dans la proposition PSR-17 (Factories HTTP) .

La StreamFactoryInterface définit une méthode nommée createStreamFromResource qui, conformément à ses commentaires officiels, devrait:

Créez un nouveau flux à partir d'une ressource existante.

Le flux DOIT être lisible et peut être accessible en écriture.

Donc, la méthode factory reçoit une ressource en argument. Et, dans son implémentation concrète, un nouvel objet Stream est créé - qui reçoit également une ressource en tant qu'argument.

Voici le problème:

Par souci de simplicité, supposons que la classe Stream ne fonctionne qu'avec un flux, par exemple. avec une ressource de type "stream". S'il reçoit une ressource qui n'est pas de type "stream" _, il la rejette.

Alors, que se passe-t-il si l'argument de ressource createStreamFromResource n'est pas déjà une ressource de type "stream"? Comment puis-je le transformer en un flux, par exemple dans une ressource de type "stream" _, afin que je puisse la passer plus loin, à l'appel pour la création d'un nouvel objet Stream avec lui? Existe-t-il un moyen (une méthode PHP, une fonction ou peut-être une fonction de transtypage) de réaliser cette tâche?

Remarques:

  • Par souci de clarté, j’ai préparé un exemple complet (testStream.php) de la création d’un flux, par exemple. une instance Stream, de trois manières: une fois directement et deux fois à l'aide de la fabrique de flux.
  • Je publie également l'implémentation concrète de l'interface usine: la classe StreamFactory avec la méthode createStreamFromResource. Un appel à cette méthode devrait être ma quatrième façon de créer un flux dans testStream.php.
  • De plus, je présente les classes Stream et Response, afin que vous puissiez tout tester directement si vous le souhaitez Les deux classes sont une version très simplifiée de mon vrai code.
  • Dans mes codes, j'ai étiqueté les deux points d'interrogation avec "@asking".

Merci beaucoup pour votre temps et votre patience!


testStream.php (la page de test)

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

La classe StreamFactory (comme je l'ai, donc pas simplifiée)

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

}

La classe Stream (très simplifiée)

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

}

La classe de réponse (très simplifiée)

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

}
9
dakis

La façon dont vous gérez l'argument passé dépend de votre implémentation finale. Si votre code attend un argument de flux, il devrait s'arrêter lorsqu'il ne détecte rien de tel. Mais si votre code est censé gérer le problème, vous pouvez essayer de créer un flux.

Modifier

Je ne l'ai pas compris depuis le début, mais il semble que la question était de savoir s'il était possible de convertir des variables de ressources. Selon la documentation ce n'est pas possible et n'a pas de sens.

1
Emil

Il existe quelques très bonnes implémentations du PSR-7 StreamInterface que je recommanderais de regarder en premier. Vous aurez peut-être une idée du type de validation et de logique que vous devez faire.

Mise à jour: Après avoir examiné tous ces liens, j'ai relevé quelques problèmes avec votre code actuel:

  • Vous devez vérifier le type de ressource dans votre constructeur. Ce peut être une ressource MySQL par exemple et vous ne voulez pas écrire dessus:

    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;
    }
    
  • Lorsque vous écrivez dans un flux, vérifiez si le flux est réellement accessible en écriture. Vous devez d'abord implémenter la méthode isWritable et l'appeler dans votre fonction write. Cet exemple est tiré de la bibliothèque 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, '+')
        );
    }
    
  • Idem avec les fonctions read et seek, vous devez d'abord implémenter isSeekable et isReadable.

  • __toString devrait également vérifier si le flux est lisible et consultable:

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

J'espère que cela t'aides. Bonne chance avec votre nouvelle bibliothèque.

1
zstate

Vous pouvez l'implémenter comme bon vous semble, mais cette méthode est essentiellement un wrapper pour une ressource pré-générée.

Dans la plupart des cas, votre flux va probablement prendre une chaîne et éventuellement un tableau de paramètres/options et créer un flux à partir des informations (éventuellement avec un fopen('http://...') quelque part en cours de route).

CreateStreamFromResource ($ resource) prendrait une ressource pré-générée (par exemple, la valeur de ressource de retour d'un fopen plutôt que les données pour exécuter le 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);
    }

}

Votre méthode d'usine pourrait être quelque chose d'aussi simple que:

public function createStreamFromResource($resource) {
    return Stream::fromResource($resource);
}
1
Jim