Sama ada anda pernah berurusan dengan fail tempatan, permintaan HTTP atau fail mampat, anda berurusan dengan strim tetapi... adakah anda benar-benar mengenalinya?
Saya berpendapat bahawa ini adalah salah satu konsep yang paling disalahfahamkan dalam PHP, dan akibatnya saya telah melihat beberapa pepijat diperkenalkan kerana kekurangan beberapa pengetahuan asas.
Dalam artikel ini saya akan cuba menerangkan apa itu strim dan cara bekerja dengannya. Kita akan melihat banyak fungsi yang digunakan untuk berfungsi dengan aliran serta banyak contoh, tetapi bukan niat saya untuk "mendokumentasikan semula" kesemuanya dalam apa cara sekalipun.
Sebelum mempelajari aliran, kita perlu terlebih dahulu mendekati sumber.
Sumber hanyalah rujukan atau penunjuk kepada sumber luaran, seperti fail, pangkalan data, rangkaian atau sambungan SSH, contohnya.
Terdapat beberapa jenis sumber, seperti curl - dicipta oleh curl_init(), proses, dicipta oleh proc_open dan stream, dicipta oleh fungsi seperti fopen(), opendir antara lain.
Strim ialah cara PHP menyamaratakan jenis sumber yang mempunyai kelakuan yang sama, iaitu, sumber boleh dibaca daripada dan ditulis kepada secara linear, seperti pita kaset (sial, saya semakin tua). Beberapa contoh strim ialah sumber fail, badan respons HTTP dan fail termampat, hanya untuk menamakan beberapa sahaja.
Strim amat berguna kerana ia membolehkan kami bekerja dengan sumber yang bersaiz daripada beberapa bait hingga beberapa GB dan, sebagai contoh, percubaan membacanya sepenuhnya, akan meletihkan memori kami yang tersedia.
fopen( string $filename, string $mode, bool $use_include_path = false, ?resource $context = null ): resource|false
fopen membuka fail atau sumber rangkaian[1], bergantung pada laluan yang diberikan kepada parameter pertamanya. Seperti yang dinyatakan sebelum ini, sumber ini adalah jenis aliran:
$fileStream = fopen('/tmp/test', 'w'); echo get_resource_type($fileStream); // 'stream'
Jika $filename disediakan dalam skema borang://, ia diandaikan sebagai URL dan PHP akan cuba mencari pengendali/pembungkus protokol yang disokong yang sepadan dengan laluan, seperti fail:// - untuk mengendalikan setempat fail, http:// - untuk bekerja pada sumber HTTP/S jauh, ssh2:// - untuk mengendalikan sambungan SSH atau php:// - yang membolehkan kami mengakses aliran input dan output PHP sendiri, seperti php://stdin, php://stdout dan php://stderr.
$mode mentakrifkan jenis akses yang anda perlukan kepada strim, iaitu, sama ada anda hanya memerlukan akses baca, tulis sahaja, baca dan tulis, baca/tulis dari permulaan atau akhir strim dan sebagainya.
Mod juga bergantung pada jenis sumber yang anda sedang usahakan. Contohnya:
$fileStream = fopen('/tmp/test', 'w'); $networkStream = fopen('https://google.com', 'r');
Membuka strim boleh tulis menggunakan pembalut https://, sebagai contoh, tidak berfungsi:
fopen('https://google.com', 'w'); // Failed to open stream: HTTP wrapper does not support writeable connections
[1] Menggunakan fopen dengan rangkaian atau sumber jauh hanya berfungsi apabila allow_url_fopen didayakan pada php.ini. Untuk maklumat lanjut, semak dokumentasi.
Jadi, sekarang kami mempunyai sumber strim, apa yang boleh kami lakukan dengannya?
fwrite(resource $stream, string $data, ?int $length = null): int|false
fwrite membolehkan kami menulis kandungan yang diberikan kepada $data ke dalam strim. Jika $length dibekalkan, ia hanya menulis bilangan bait yang dibekalkan. Mari lihat contoh:
$fileStream = fopen('/tmp/test', 'w'); fwrite($fileStream, "The quick brown fox jumps over the lazy dog", 10);
Dalam contoh ini, seperti yang kami berikan $length = 10, jadi hanya sebahagian daripada kandungan yang ditulis - "The quick " - mengabaikan yang lain.
Perhatikan bahawa kami membuka strim fail dengan $mode = 'w', yang membolehkan kami menulis kandungan ke dalam fail. Sebaliknya, jika kami telah membuka fail dengan $mode = 'r', kami akan mendapat mesej seperti fwrite(): Tulisan sebanyak 8192 bait gagal dengan errno=9 Deskriptor fail buruk.
Mari kita lihat contoh lain, kini menulis keseluruhan kandungan ke dalam strim fail:
$fileStream = fopen('/tmp/test', 'w'); fwrite($fileStream, "The quick brown fox jumps over the lazy dog");
Sekarang, kerana kami tidak memberikan $length, keseluruhan kandungan telah ditulis ke dalam fail.
Menulis ke dalam strim menggerakkan kedudukan penuding baca/tulis ke penghujung jujukan. Dalam kes ini, rentetan yang ditulis ke dalam strim mempunyai 44 aksara, oleh itu, kedudukan penunjuk sekarang hendaklah 43.
Selain menulis ke dalam fail, fwrite boleh menulis dalam jenis strim lain, seperti soket. Contoh diekstrak daripada dokumen:
$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);
fread(resource $stream, int $length): string|false
Dengan fread anda boleh membaca sehingga $length bait daripada strim, bermula dari penuding baca semasa. Ia selamat binari dan ia berfungsi dengan sumber tempatan dan rangkaian, seperti yang akan kita lihat dalam contoh.
Memanggil fread secara berturut-turut akan membaca ketulan dan kemudian mengalihkan penuding baca ke hujung bongkah ini. Contoh, mengambil kira fail yang ditulis dalam contoh sebelumnya:
# 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
Kami akan kembali untuk memberitahu tidak lama lagi, tetapi apa yang dilakukannya hanyalah mengembalikan kedudukan semasa penuding baca.
Bacaan berhenti (mengembalikan palsu), sebaik sahaja salah satu daripada yang berikut berlaku (disalin daripada dokumen, anda akan faham kemudian):
- 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.
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).
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++; }
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
What happened in this case was clear:
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++; }
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
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++; }
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
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++; }
Output:
Iteration: 1. Bytes read: 1048576 Iteration: 2. Bytes read: 1048576 ... Iteration: 208. Bytes read: 1048576 Iteration: 209. Bytes read: 1048576
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.
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(resource $stream): int|false
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
stream_get_meta_data(resource $stream): array
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)):
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" }
This function's documentation is pretty honest describing each value ;)
fseek(resource $stream, int $offset, int $whence = SEEK_SET): int
fseek sets the read/write pointer on the opened stream to the value provided to $offset.
The position will be updated based on $whence:
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'
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.
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
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.
rewind(resource $stream): bool
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:
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
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 ..."
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'
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'
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:
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);
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); }
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?
Ask yourself these (and other questions), make some performance benchmark tests and select the function that suits your needs the most.
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);
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:
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
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.
Atas ialah kandungan terperinci Strim dalam PHP. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!