Flux en PHP

王林
Libérer: 2024-08-26 06:33:35
original
569 Les gens l'ont consulté

Streams in PHP

Que vous ayez déjà eu affaire à des fichiers locaux, à des requêtes HTTP ou à des fichiers compressés, vous avez eu affaire à des flux mais... avez-vous vraiment appris à les connaître ?

Je pense que c'est l'un des concepts les plus mal compris de PHP et, par conséquent, j'ai vu pas mal de bugs introduits en raison du manque de connaissances fondamentales.

Dans cet article, je vais essayer d'expliquer ce que sont réellement les streams et comment travailler avec eux. Nous verrons de nombreuses fonctions utilisées pour travailler avec des flux ainsi que de nombreux exemples, mais ce n'est pas mon intention de toutes les "redocumenter" de quelque manière que ce soit.

Avant d'apprendre ce que sont les flux, nous devons d'abord aborder les ressources.

Ressources

Les ressources sont simplement des références ou des pointeurs vers des ressources externes, comme un fichier, une base de données, une connexion réseau ou SSH par exemple.

Il existe plusieurs types de ressources, telles que curl - créées par curl_init(), process, créées par proc_open et stream, créées par des fonctions comme fopen(), opendir entre autres.

Flux

Les flux sont la façon dont PHP généralise les types de ressources qui ont un comportement commun, c'est-à-dire que les ressources peuvent être lues et écrites de manière linéaire, comme une cassette (putain, je vieillis). Quelques exemples de flux sont les ressources de fichiers, les corps de réponse HTTP et les fichiers compressés, pour n'en nommer que quelques-uns.

Les flux sont incroyablement utiles car ils nous permettent de travailler avec des ressources dont la taille varie de quelques octets à plusieurs Go et, tenter de les lire entièrement, par exemple, épuiserait notre mémoire disponible.

Créer un flux avec fopen

 fopen(
    string $filename,
    string $mode,
    bool $use_include_path = false,
    ?resource $context = null
): resource|false
Copier après la connexion

fopen ouvre un fichier ou une ressource réseau[1], selon le chemin fourni à son premier paramètre. Comme dit précédemment, cette ressource est de type stream :

$fileStream = fopen('/tmp/test', 'w');
echo get_resource_type($fileStream); // 'stream'
Copier après la connexion

Si $filename est fourni sous la forme schéma://, il est supposé qu'il s'agit d'une URL et PHP essaiera de trouver un gestionnaire/wrappers de protocole pris en charge qui correspond au chemin, tel que fichier:// - pour gérer les paramètres locaux. fichiers, http:// - pour travailler sur des ressources HTTP/S distantes, ssh2:// - pour gérer les connexions SSH ou php:// - qui nous permet d'accéder aux propres flux d'entrée et de sortie de PHP, tels que php://stdin, php://stdout et php://stderr.

$mode définit le type d'accès dont vous avez besoin au flux, c'est-à-dire si vous avez besoin uniquement d'un accès en lecture, uniquement en écriture, en lecture et en écriture, en lecture/écriture depuis le début ou la fin du flux, et ainsi de suite.

Le mode dépend également du type de ressource sur laquelle vous travaillez. Par exemple :

$fileStream = fopen('/tmp/test', 'w');
$networkStream = fopen('https://google.com', 'r');
Copier après la connexion

Ouvrir un flux inscriptible à l'aide du wrapper https://, par exemple, ne fonctionne pas :

fopen('https://google.com', 'w'); // Failed to open stream: HTTP wrapper does not support writeable connections
Copier après la connexion

[1] L'utilisation de fopen avec des ressources réseau ou distantes ne fonctionne que lorsque allow_url_fopen est activé sur php.ini. Pour plus d'informations, consultez la documentation.

Alors, maintenant que nous avons une ressource de flux, que pouvons-nous en faire ?

Écrire dans un flux de fichiers avec fwrite

fwrite(resource $stream, string $data, ?int $length = null): int|false
Copier après la connexion

fwrite nous permet d'écrire le contenu fourni à $data dans un flux. Si $length est fourni, il écrit uniquement le nombre d’octets fourni. Voyons un exemple :

$fileStream = fopen('/tmp/test', 'w');

fwrite($fileStream,  "The quick brown fox jumps over the lazy dog", 10);
Copier après la connexion

Dans cet exemple, comme nous avons fourni $length = 10, seule une partie du contenu a été écrite - "Le rapide" - en ignorant le reste.

Remarquez que nous avons ouvert le flux de fichiers avec $mode = 'w', ce qui nous a permis d'écrire du contenu dans le fichier. Si, à la place, nous avions ouvert le fichier avec $mode = 'r', nous obtiendrions un message tel que fwrite() : L'écriture de 8 192 octets a échoué avec errno=9 Bad file descriptor.

Voyons un autre exemple, en écrivant maintenant tout le contenu dans le flux de fichiers :

$fileStream = fopen('/tmp/test', 'w');

fwrite($fileStream,  "The quick brown fox jumps over the lazy dog");
Copier après la connexion

Maintenant, comme nous n'avons pas fourni $length, tout le contenu a été écrit dans le fichier.

L'écriture dans un flux déplace la position du pointeur de lecture/écriture vers la fin de la séquence. Dans ce cas, la chaîne écrite dans le flux comporte 44 caractères, par conséquent, la position du pointeur devrait maintenant être 43.

En plus d'écrire dans un fichier, fwrite peut écrire dans d'autres types de flux, tels que les sockets. Exemple extrait de la doc :

$sock = fsockopen("ssl://secure.example.com", 443, $errno, $errstr, 30);
if (!$sock) die("$errstr ($errno)\n");

$data = "foo=" . urlencode("Value for Foo") . "&bar=" . urlencode("Value for Bar");

fwrite($sock, "POST /form_action.php HTTP/1.0\r\n");
fwrite($sock, "Host: secure.example.com\r\n");
fwrite($sock, "Content-type: application/x-www-form-urlencoded\r\n");
fwrite($sock, "Content-length: " . strlen($data) . "\r\n");
fwrite($sock, "Accept: */*\r\n");
fwrite($sock, "\r\n");
fwrite($sock, $data);

$headers = "";
while ($str = trim(fgets($sock, 4096)))
$headers .= "$str\n";

echo "\n";

$body = "";
while (!feof($sock))
$body .= fgets($sock, 4096);

fclose($sock);
Copier après la connexion

Lire des flux avec fread

fread(resource $stream, int $length): string|false
Copier après la connexion

Avec fread, vous pouvez lire jusqu'à $length octets à partir d'un flux, à partir du pointeur de lecture actuel. Il est sécurisé pour les binaires et fonctionne avec les ressources locales et réseau, comme nous le verrons dans les exemples.

Appeler fread consécutivement lira un morceau, puis déplacera le pointeur de lecture vers la fin de ce morceau. Exemple, en considérant le fichier écrit dans l'exemple précédent :

# Content: "The quick brown fox jumps over the lazy dog"
$fileStream = fopen('/tmp/test', 'r');

echo fread($fileStream, 10) . PHP_EOL;      // 'The quick '
echo ftell($fileStream); // 10
echo fread($fileStream, 10) . PHP_EOL;      // 'brown fox '
echo ftell($fileStream); // 20
Copier après la connexion

Nous reviendrons bientôt sur ftell, mais ce qu'il fait, c'est simplement renvoyer la position actuelle du pointeur de lecture.

La lecture s'arrête (retournant faux), dès que l'un des événements suivants se produit (copié depuis la doc, vous comprendrez plus tard) :

  • length bytes have been read
  • EOF (end of file) is reached
  • a packet becomes available or the socket timeout occurs (for network streams)
  • if the stream is read buffered and it does not represent a plain file, at most one read of up to a number of bytes equal to the chunk size (usually 8192) is made; depending on the previously buffered data, the size of the returned data may be larger than the chunk size.

I don't know if you had the same felling, but this last part is pretty cryptic, so let's break it down.

"if the stream is read buffered"

Stream reads and writes can be buffered, that is, the content may be stored internally. It is possible to disable/enable the buffering, as well as set their sizes using stream_set_read_buffer and stream-set-write-buffer, but according to this comment on the PHP doc's Github, the description of these functions can be misleading.

This is where things get interesting, as this part of the documentation is really obscure. As per the comment, setting stream_set_read_buffer($stream, 0) would disable the read buffering, whereas stream_set_read_buffer($stream, 1) or stream_set_read_buffer($stream, 42) would simply enable it, ignoring its size (depending on the stream wrapper, which can override this default behaviour).

"... at most one read of up to a number of bytes equal to the chunk size (usually 8192) is made"

The chunk size is usually 8192 bytes or 8 KiB, as we will confirm in a bit. We can change this value using stream_set_chunk_size. Let's see it in action:

$f = fopen('https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-standard-3.20.2-x86_64.iso', 'rb');

$previousPos = 0;
$chunkSize = 1024;
$i = 1;

while ($chunk = fread($f, $chunkSize)) {
    $bytesRead = (ftell($f) - $previousPos);
    $previousPos = ftell($f);

    echo "Iteration: {$i}. Bytes read: {$bytesRead}" . PHP_EOL;

    $i++;
}

Copier après la connexion

Output:

Iteration: 1. Bytes read: 1024
Iteration: 2. Bytes read: 1024
Iteration: 3. Bytes read: 1024
...
Iteration: 214016. Bytes read: 1024
Iteration: 214017. Bytes read: 169
Copier après la connexion

What happened in this case was clear:

  • We wanted up to 1024 bytes in each fread call and that's what we got
  • In the last call there were only 169 bytes remainder, which were returned
  • When there was nothing else to return, that is, EOF was reached fread returned false and the loop finished.

Now let's increase considerably the length provided to fread to 1 MiB and see what happens:

$f = fopen('https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-standard-3.20.2-x86_64.iso', 'rb');

$previousPos = 0;
$chunkSize = 1048576; // 1 MiB
$i = 1;

while ($chunk = fread($f, $chunkSize)) {
    $bytesRead = (ftell($f) - $previousPos);
    $previousPos = ftell($f);

    echo "Iteration: {$i}. Bytes read: {$bytesRead}" . PHP_EOL;

    $i++;
}
Copier après la connexion

Output:

Iteration: 1. Bytes read: 1378
Iteration: 2. Bytes read: 1378
Iteration: 3. Bytes read: 1378
...
Iteration: 24. Bytes read: 1074
Iteration: 25. Bytes read: 8192
Iteration: 26. Bytes read: 8192
...
Iteration: 26777. Bytes read: 8192
Iteration: 26778. Bytes read: 8192
Iteration: 26779. Bytes read: 293
Copier après la connexion

So, even though we tried to read 1 MiB using fread, it read up to 8192 bytes - same value that the docs said it would. Interesting. Let's see another experiment:

$f = fopen('https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-standard-3.20.2-x86_64.iso', 'rb');

$previousPos = 0;
$chunkSize = 1048576; // 1 MiB
$i = 1;

stream_set_chunk_size($f, $chunkSize); // Just added this line

while ($chunk = fread($f, $chunkSize)) {
    $bytesRead = (ftell($f) - $previousPos);
    $previousPos = ftell($f);

    echo "Iteration: {$i}. Bytes read: {$bytesRead}" . PHP_EOL;

    $i++;
}
Copier après la connexion

And the output:

Iteration: 1. Bytes read: 1378
Iteration: 2. Bytes read: 1378
Iteration: 3. Bytes read: 1378
...
Iteration: 12. Bytes read: 533
Iteration: 13. Bytes read: 16384
Iteration: 14. Bytes read: 16384
...
Iteration: 13386. Bytes read: 16384
Iteration: 13387. Bytes read: 16384
Iteration: 13388. Bytes read: 13626
Copier après la connexion

Notice that now fread read up to 16 KiB - not even close to what we wanted, but we've seen that stream_set_chunk_size did work, but there are some hard limits, that I suppose that depends also on the wrapper. Let's put that in practice with another experiment, using a local file this time:

$f = fopen('alpine-standard-3.20.2-x86_64.iso', 'rb');

$previousPos = 0;
$chunkSize = 1048576; // 1 MiB
$i = 1;

while ($chunk = fread($f, $chunkSize)) {
    $bytesRead = (ftell($f) - $previousPos);
    $previousPos = ftell($f);

    echo "Iteration: {$i}. Bytes read: {$bytesRead}" . PHP_EOL;

    $i++;
}
Copier après la connexion

Output:

Iteration: 1. Bytes read: 1048576
Iteration: 2. Bytes read: 1048576
...
Iteration: 208. Bytes read: 1048576
Iteration: 209. Bytes read: 1048576
Copier après la connexion

Aha! So using the local file handler we were able to fread 1 MiB as we wanted, and we did not even need to increase the buffer/chunk size with stream_set_chunk_size.

Wrapping up

I think that now the description is less cryptic, at least. Let's read it again (with some interventions):

if the stream is read buffered ...

and it does not represent a plain file (that is, local, not a network resource), ...

at most one read of up to a number of bytes equal to the chunk size (usually 8192) is made (and in our experiments we could confirm that this is true, at least one read of the chunk size was made); ...

depending on the previously buffered data, the size of the returned data may be larger than the chunk size (we did not experience that, but I assume it may happen depending on the wrapper).

There is definitely some room to play here, but I will challenge you. What would happen if you disable the buffers while reading a file? And a network resource? What if you write into a file?

ftell

ftell(resource $stream): int|false
Copier après la connexion

ftell returns the position of the read/write pointer (or null when the resource is not valid).

# Content: "The quick brown fox jumps over the lazy dog"
$fileStream = fopen('/tmp/test', 'r');

fread($fileStream, 10); # "The quick "
echo ftell($fileStream); 10
Copier après la connexion

stream_get_meta_data

stream_get_meta_data(resource $stream): array
Copier après la connexion

stream_get_meta_data returns information about the stream in form of an array. Let's see an example:

# Content: "The quick brown fox jumps over the lazy dog"
$fileStream = fopen('/tmp/test', 'r');
var_dump(stream_get_meta_data($fileStream)): 
Copier après la connexion

The previous example would return in something like this:

array(9) {
  ["timed_out"]=>
  bool(false)
  ["blocked"]=>
  bool(true)
  ["eof"]=>
  bool(false)
  ["wrapper_type"]=>
  string(9) "plainfile"
  ["stream_type"]=>
  string(5) "STDIO"
  ["mode"]=>
  string(1) "r"
  ["unread_bytes"]=>
  int(0)
  ["seekable"]=>
  bool(true)
  ["uri"]=>
  string(16) "file:///tmp/test"
}
Copier après la connexion

This function's documentation is pretty honest describing each value ;)

fseek

fseek(resource $stream, int $offset, int $whence = SEEK_SET): int
Copier après la connexion

fseek sets the read/write pointer on the opened stream to the value provided to $offset.
The position will be updated based on $whence:

  • SEEK_SET: Position is set to $offset, that is, if you call
  • SEEK_CUR: Position is set based on the current one, that is, current + $offset
  • SEEK_END: Position is set to End Of File + $offset.

Using SEEK_END we can provide a negative value to $offset and go backwards from EOF. Its return value can be used to assess if the position has been set successfully (0) or has failed (-1).

Let's see some examples:

# Content: "The quick brown fox jumps over the lazy dog\n"
$fileStream = fopen('/tmp/test', 'r+');

fseek($fileStream, 4, SEEK_SET);
echo fread($fileStream, 5);         // 'quick'
echo ftell($fileStream);            // 9

fseek($fileStream, 7, SEEK_CUR);
echo ftell($fileStream);            // 16, that is, 9 + 7
echo fread($fileStream, 3);         // 'fox'  

fseek($fileStream, 5, SEEK_END);    // Sets the position past the End Of File
echo ftell($fileStream);            // 49, that is, EOF (at 44th position) + 5
echo fread($fileStream, 3);         // ''  
echo ftell($fileStream);            // 49, nothing to read, so read/write pointer hasn't changed
fwrite($fileStream, 'foo');
ftell($fileStream);                 // 52, that is, previous position + 3
fseek($fileStream, -3, SEEK_END);
ftell($fileStream);                 // 49, that is, 52 - 3
echo fread($fileStream, 3);         // 'foo'  
Copier après la connexion

Some important considerations

  1. As we've seen in this example, it is possible we seek past the End Of File and even read in an unwritten area (which returns 0 bytes), but some types of streams do not support it.

  2. An important consideration is that not all streams can be seeked, for instance, you cannot fseek a remote resource:

$f = fopen('https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-standard-3.20.2-x86_64.iso', 'rb');
fseek($f, 10); WARNING  fseek(): Stream does not support seeking
Copier après la connexion

This obviously makes total sense, as we cannot "fast-forward" and set a position on a remote resource. The stream in this case is only read sequentially, like a cassette tape.
We can determine if the stream is seekable or not via the seekable value returned by stream_get_meta_data that we've seen before.

  1. We can fseek a resource opened in append mode (a or a+), but the data will always be appended.

rewind

rewind(resource $stream): bool
Copier après la connexion

This is a pure analogy of rewinding a videotape before returning it to video store. As expected, rewind sets the position of the read/write pointer to 0, which is basically the same as calling fseek with $offset 0.

The same considerations we've seen for fseek applies for rewind, that is:

  • You cannot rewind an unseekable stream
  • rewind on a resource opened in append mode will still write from the current position - the write pointer is not updated.

How about file_get_contents?

So far we've been working directly with resources. file_get_contents is a bit different, as it accepts the file path and returns the whole file content as a string, that is, it implicitly opens the resource.

file_get_contents(
   string $filename,
   bool $use_include_path = false,
   ?resource $context = null,
   int $offset = 0,
   ?int $length = null
): string|false
Copier après la connexion

Similar to fread, file_get_contents can work on local and remote resources, depending on the $filename we provide:

# Content: "The quick brown fox jumps over the lazy dog"

echo file_get_contents('/tmp/test'); // "The quick brown fox jumps over the lazy dog\n"

echo file_get_contents('https://www.php.net/images/logos/php-logo.svg'); // "<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -1 100 50">\n ..."
Copier après la connexion

With $offset we can set the starting point to read the content, whereas with length we can get a given amount of bytes.

# Content: "The quick brown fox jumps over the lazy dog"
echo file_get_contents('/tmp/test', offset: 16, size: 3); // 'fox'
Copier après la connexion

offset also accepts negative values, which counts from the end of the stream.

# Content: "The quick brown fox jumps over the lazy dog"
echo file_get_contents('/tmp/test', offset: -4, size: 3); // 'dog'
Copier après la connexion

Notice that the same rules that govern fseek are also applied for $offset, that is - you cannot set an $offset while reading remote files, as the function would be basically fseek the stream, and we've seen that it does not work well.

The parameter context makes file_get_contents really flexible, enabling us set, for example:

  • Set a different HTTP method, such as POST instead of the default GET
  • Provide headers and content to a POST or PUT request
  • Disable SSL verification and allow self-signed certificates

We create a context using stream_context_create, example:

$context = stream_context_create(['http' => ['method' => "POST"]]);
file_get_contents('https://a-valid-resource.xyz', context: $context);
Copier après la connexion

You can find the list of options you can provide to stream_context_create in this page.

$networkResource = fopen('https://releases.ubuntu.com/24.04/ubuntu-24.04-desktop-amd64.iso', 'r');

while ($chunk = fread($networkResource, 1024)) {
   doSomething($chunk);
}
Copier après la connexion

Which one to use? fread, file_get_contents, fgets, another one?

The list of functions that we can use to read local or remote contents is lengthy, and each function can be seen as a tool in your tool belt, suitable for a specific purpose.

According to the docs, file_get_contents is the preferred way of reading contents of a file into a string, but, is it appropriate for all purposes?

  • What if you know that the content is large? Will it fit into memory?
  • Do you need it entirely in memory or can you work in chunks?
  • Are you going to work on local or remote files?

Ask yourself these (and other questions), make some performance benchmark tests and select the function that suits your needs the most.

PSR-7's StreamInterface

PSR defines the StreamInterface, which libraries such as Guzzle use to represent request and response bodies. When you send a request, the body is an instance of StreamInterface. Let's see an example, extracted from the Guzzle docs:

$client = new \GuzzleHttp\Client();
$response = $client->request('GET', 'http://httpbin.org/get');

$body = $response->getBody();
$body->seek(0);
$body->read(1024);
Copier après la connexion

I suppose that the methods available on $body look familiar for you now :D

StreamInterface implements methods that resemble a lot the functions we've just seen, such as:

  • seek()
  • tell()
  • eof()
  • read
  • write
  • isSeekable
  • isReadable()
  • isWritable
  • and so on.

Last but not least, we can use GuzzleHttp\Psr7\Utils::streamFor to create streams from strings, resources opened with fopen and instances of StreamInterface:

use GuzzleHttp\Psr7;

$stream = Psr7\Utils::streamFor('string data');
echo $stream;                   // string data
echo $stream->read(3);          // str
echo $stream->getContents();    // ing data
var_export($stream->eof());     // true
var_export($stream->tell());    // 11
Copier après la connexion

Summary

In this article we've seen what streams really are, learned how to create them, read from them, write to them, manipulate their pointers as well as clarified some obscured parts regarding read a write buffers.

If I did a good job, some of the doubts you might have had regarding streams are now a little bit clearer and, from now on, you'll write code more confidently, as you know what you are doing.

Should you noticed any errors, inaccuracies or there is any topic that is still unclear, let me know in the comments and I'd be glad to try to help.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal
À propos de nous Clause de non-responsabilité Sitemap
Site Web PHP chinois:Formation PHP en ligne sur le bien-être public,Aidez les apprenants PHP à grandir rapidement!