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:
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.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
.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.Merci beaucoup pour votre temps et votre patience!
<?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"?
*/
//...
<?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);
}
}
<?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() {}
}
<?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 = '') {}
}
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.
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.
StreamInterface
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.
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);
}