PHP의 스트림
로컬 파일, HTTP 요청 또는 압축 파일을 처리해야 했던 적이 있거나 스트림을 처리했지만... 실제로 알게 되었나요?
저는 이것이 PHP에서 가장 오해받는 개념 중 하나라고 생각합니다. 그 결과 기본적인 지식이 부족하여 버그가 많이 발생하는 것을 보았습니다.
이 기사에서는 스트림이 실제로 무엇인지, 스트림을 사용하는 방법을 설명하려고 합니다. 우리는 많은 예제와 함께 스트림 작업에 사용되는 많은 함수를 보게 될 것입니다. 그러나 어떤 방식으로든 모든 기능을 "다시 문서화"하려는 의도는 없습니다.
스트림이 무엇인지 알아보기 전에 먼저 리소스에 접근해야 합니다.
자원
리소스는 단순히 파일, 데이터베이스, 네트워크 또는 SSH 연결과 같은 외부 리소스에 대한 참조 또는 포인터입니다.
curl(curl_init()으로 생성됨), process(proc_open으로 생성됨), stream(fopen()과 같은 함수로 생성됨), opendir 등 여러 유형의 리소스가 있습니다.
스트림
스트림은 PHP가 일반적인 동작을 갖는 리소스 유형을 일반화하는 방식입니다. 즉, 리소스는 카세트 테이프처럼 선형적으로 읽고 쓸 수 있습니다(젠장, 이제 늙어가네요). 스트림의 예로는 파일 리소스, HTTP 응답 본문, 압축 파일 등이 있습니다.
스트림은 몇 바이트에서 몇 GB 크기의 리소스로 작업할 수 있게 해주고, 예를 들어 스트림 전체를 읽으려고 하면 사용 가능한 메모리가 소진될 수 있다는 점에서 매우 유용합니다.
fopen을 사용하여 스트림 만들기
fopen( string $filename, string $mode, bool $use_include_path = false, ?resource $context = null ): resource|false
fopen은 첫 번째 매개변수에 제공된 경로에 따라 파일 또는 네트워크 리소스[1]를 엽니다. 이전에 말했듯이 이 리소스는 스트림 유형입니다.
$fileStream = fopen('/tmp/test', 'w'); echo get_resource_type($fileStream); // 'stream'
$filename이 Scheme:// 형식으로 제공되면 URL로 간주되며 PHP는 file://과 같은 경로와 일치하는 지원되는 프로토콜 핸들러/래퍼를 찾으려고 시도합니다. 파일, http:// - 원격 HTTP/S 리소스에 대한 작업, ssh2:// - SSH 연결 처리 또는 php:// - php://stdin과 같은 PHP 자체 입력 및 출력 스트림에 액세스할 수 있게 해줍니다. php://stdout 및 php://stderr.
$mode는 스트림에 필요한 액세스 유형, 즉 읽기 액세스만 필요한지, 쓰기만 필요한지, 읽기 및 쓰기만 필요한지, 스트림 시작 또는 끝에서 읽기/쓰기 등을 정의합니다.
모드는 작업 중인 리소스 유형에 따라 달라집니다. 예:
$fileStream = fopen('/tmp/test', 'w'); $networkStream = fopen('https://google.com', 'r');
예를 들어 래퍼 https://를 사용하여 쓰기 가능한 스트림을 열면 작동하지 않습니다.
fopen('https://google.com', 'w'); // Failed to open stream: HTTP wrapper does not support writeable connections
[1] 네트워크 또는 원격 리소스와 함께 fopen을 사용하는 것은 php.ini에서 허용_url_fopen이 활성화된 경우에만 작동합니다. 자세한 내용은 설명서를 확인하세요.
이제 스트림 리소스가 생겼는데, 그걸로 무엇을 할 수 있을까요?
fwrite를 사용하여 파일 스트림에 쓰기
fwrite(resource $stream, string $data, ?int $length = null): int|false
fwrite를 사용하면 $data에 제공된 콘텐츠를 스트림에 쓸 수 있습니다. $length가 제공되면 제공된 바이트 수만 씁니다. 예를 살펴보겠습니다:
$fileStream = fopen('/tmp/test', 'w'); fwrite($fileStream, "The quick brown fox jumps over the lazy dog", 10);
이 예에서는 $length = 10을 제공했으므로 콘텐츠의 일부만 기록되었습니다("The Quick" - 나머지는 무시함).
$mode = 'w'를 사용하여 파일 스트림을 열었고 이를 통해 파일에 콘텐츠를 쓸 수 있었습니다. 대신 $mode = 'r'을 사용하여 파일을 열었다면 fwrite(): Write of 8192 bytes failed with errno=9 Bad file descriptor와 같은 메시지가 표시됩니다.
이제 전체 콘텐츠를 파일 스트림에 쓰는 또 다른 예를 살펴보겠습니다.
$fileStream = fopen('/tmp/test', 'w'); fwrite($fileStream, "The quick brown fox jumps over the lazy dog");
이제 $length를 제공하지 않았으므로 전체 내용이 파일에 기록되었습니다.
스트림에 쓰면 읽기/쓰기 포인터의 위치가 시퀀스의 끝 부분으로 이동됩니다. 이 경우 스트림에 기록된 문자열은 44자이므로 이제 포인터의 위치는 43이 되어야 합니다.
fwrite는 파일에 쓰는 것 외에도 소켓과 같은 다른 유형의 스트림에 쓸 수 있습니다. 문서에서 추출한 예:
$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로 스트림 읽기
fread(resource $stream, int $length): string|false
fread를 사용하면 현재 읽기 포인터부터 시작하여 스트림에서 최대 $length 바이트를 읽을 수 있습니다. 예제에서 볼 수 있듯이 바이너리 안전하며 로컬 및 네트워크 리소스와 함께 작동합니다.
연속적으로 fread를 호출하면 청크를 읽은 다음 읽기 포인터를 이 청크의 끝으로 이동합니다. 예, 이전 예에서 작성된 파일을 고려하십시오.
# 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
곧 ftell로 다시 돌아오겠지만 그것이 하는 일은 단순히 읽기 포인터의 현재 위치를 반환하는 것뿐입니다.
다음 중 하나가 발생하는 즉시 읽기가 중지됩니다(false 반환)(문서에서 복사됨, 나중에 이해할 수 있음).
- 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++; }
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:
- 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++; }
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.
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
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
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
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:
- 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'
Some important considerations
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.
- We can fseek a resource opened in append mode (a or a+), but the data will always be appended.
rewind
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:
- 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
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:
- 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);
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); }
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);
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
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.
위 내용은 PHP의 스트림의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

Video Face Swap
완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

PHP에서 Password_hash 및 Password_Verify 기능을 사용하여 보안 비밀번호 해싱을 구현해야하며 MD5 또는 SHA1을 사용해서는 안됩니다. 1) Password_hash는 보안을 향상시키기 위해 소금 값이 포함 된 해시를 생성합니다. 2) Password_verify 암호를 확인하고 해시 값을 비교하여 보안을 보장합니다. 3) MD5 및 SHA1은 취약하고 소금 값이 부족하며 현대 암호 보안에는 적합하지 않습니다.

PHP 유형은 코드 품질과 가독성을 향상시키기위한 프롬프트입니다. 1) 스칼라 유형 팁 : PHP7.0이므로 int, float 등과 같은 기능 매개 변수에 기본 데이터 유형을 지정할 수 있습니다. 2) 반환 유형 프롬프트 : 기능 반환 값 유형의 일관성을 확인하십시오. 3) Union 유형 프롬프트 : PHP8.0이므로 기능 매개 변수 또는 반환 값에 여러 유형을 지정할 수 있습니다. 4) Nullable 유형 프롬프트 : NULL 값을 포함하고 널 값을 반환 할 수있는 기능을 포함 할 수 있습니다.

PHP는 주로 절차 적 프로그래밍이지만 객체 지향 프로그래밍 (OOP)도 지원합니다. Python은 OOP, 기능 및 절차 프로그래밍을 포함한 다양한 패러다임을 지원합니다. PHP는 웹 개발에 적합하며 Python은 데이터 분석 및 기계 학습과 같은 다양한 응용 프로그램에 적합합니다.

PHP는 웹 개발 및 빠른 프로토 타이핑에 적합하며 Python은 데이터 과학 및 기계 학습에 적합합니다. 1.PHP는 간단한 구문과 함께 동적 웹 개발에 사용되며 빠른 개발에 적합합니다. 2. Python은 간결한 구문을 가지고 있으며 여러 분야에 적합하며 강력한 라이브러리 생태계가 있습니다.

PHP는 1994 년에 시작되었으며 Rasmuslerdorf에 의해 개발되었습니다. 원래 웹 사이트 방문자를 추적하는 데 사용되었으며 점차 서버 측 스크립팅 언어로 진화했으며 웹 개발에 널리 사용되었습니다. Python은 1980 년대 후반 Guidovan Rossum에 의해 개발되었으며 1991 년에 처음 출시되었습니다. 코드 가독성과 단순성을 강조하며 과학 컴퓨팅, 데이터 분석 및 기타 분야에 적합합니다.

PHP는 현대화 프로세스에서 많은 웹 사이트 및 응용 프로그램을 지원하고 프레임 워크를 통해 개발 요구에 적응하기 때문에 여전히 중요합니다. 1.PHP7은 성능을 향상시키고 새로운 기능을 소개합니다. 2. Laravel, Symfony 및 Codeigniter와 같은 현대 프레임 워크는 개발을 단순화하고 코드 품질을 향상시킵니다. 3. 성능 최적화 및 모범 사례는 응용 프로그램 효율성을 더욱 향상시킵니다.

PHP의 핵심 이점에는 학습 용이성, 강력한 웹 개발 지원, 풍부한 라이브러리 및 프레임 워크, 고성능 및 확장 성, 크로스 플랫폼 호환성 및 비용 효율성이 포함됩니다. 1) 배우고 사용하기 쉽고 초보자에게 적합합니다. 2) 웹 서버와 우수한 통합 및 여러 데이터베이스를 지원합니다. 3) Laravel과 같은 강력한 프레임 워크가 있습니다. 4) 최적화를 통해 고성능을 달성 할 수 있습니다. 5) 여러 운영 체제 지원; 6) 개발 비용을 줄이기위한 오픈 소스.

phphassignificallyimpactedwebdevelopmentandextendsbeyondit
