Mã PHP nên được thực thi ở đâu?

Bài viết này đã được bình duyệt bởi Younes Rafie. Cảm ơn tất cả những người đánh giá ngang hàng của SitePoint đã làm cho nội dung SitePoint trở nên tốt nhất có thể


Lấy cảm hứng từ một bài viết gần đây về cách mã Ruby thực thi, bài viết này đề cập đến quy trình thực thi mã PHP

Mã PHP nên được thực thi ở đâu?

Giới thiệu

Có rất nhiều thứ đang diễn ra khi chúng ta thực thi một đoạn mã PHP. Nói chung, trình thông dịch PHP trải qua bốn giai đoạn khi thực thi mã

  1. lexing
  2. phân tích cú pháp
  3. biên soạn
  4. Diễn dịch

Bài viết này sẽ lướt qua các giai đoạn này và chỉ ra cách chúng ta có thể xem đầu ra từ mỗi giai đoạn để thực sự biết điều gì đang diễn ra. Lưu ý rằng mặc dù một số tiện ích mở rộng được sử dụng phải là một phần trong quá trình cài đặt PHP của bạn (chẳng hạn như mã thông báo và OPcache), những tiện ích mở rộng khác sẽ cần được cài đặt và kích hoạt thủ công (chẳng hạn như php-ast và VLD)

Giai đoạn 1 – Lexing

Lexing (hoặc tokenizing) là quá trình biến một chuỗi (trong trường hợp này là mã nguồn PHP) thành một chuỗi các token. Mã thông báo chỉ đơn giản là một mã định danh được đặt tên cho giá trị mà nó đã khớp. PHP sử dụng re2c để tạo từ vựng của nó từ zend_language_scanner. tôi tập tin định nghĩa

Chúng ta có thể thấy đầu ra của giai đoạn lexing thông qua tiện ích mở rộng mã thông báo

$code = <<<'code'

$a = 1;
code;

$tokens = token_get_all($code);

foreach ($tokens as $token) {
    if (is_array($token)) {
        echo "Line {$token[2]}: ", token_name($token[0]), " ('{$token[1]}')", PHP_EOL;
    } else {
        var_dump($token);
    }
}

đầu ra

Line 1: T_OPEN_TAG ('

Có một vài điểm đáng chú ý từ đầu ra trên. Điểm đầu tiên là không phải tất cả các phần của mã nguồn đều được đặt tên là mã thông báo. Thay vào đó, một số biểu tượng được coi là mã thông báo trong và của chính chúng (chẳng hạn như

Line 1: T_OPEN_TAG ('
2,
Line 1: T_OPEN_TAG ('
0,
Line 1: T_OPEN_TAG ('
1,
Line 1: T_OPEN_TAG ('
2, v.v.). Điểm thứ hai là lexer thực sự làm được nhiều việc hơn là chỉ xuất ra một luồng mã thông báo. Trong hầu hết các trường hợp, nó cũng lưu trữ từ vựng (giá trị khớp với mã thông báo) và số dòng của mã thông báo khớp (được sử dụng cho những thứ như dấu vết ngăn xếp)

Giai đoạn 2 – Phân tích cú pháp

Trình phân tích cú pháp cũng được tạo, lần này là với Bison thông qua tệp ngữ pháp BNF. PHP sử dụng ngữ pháp phi ngữ cảnh LALR(1) (nhìn về phía trước, từ trái sang phải). Phần xem trước chỉ đơn giản có nghĩa là trình phân tích cú pháp có thể xem trước mã thông báo

Line 1: T_OPEN_TAG ('
3 (1, trong trường hợp này) để giải quyết những điểm mơ hồ mà nó có thể gặp phải trong khi phân tích cú pháp. Phần từ trái sang phải có nghĩa là nó phân tích luồng mã thông báo từ trái sang phải

Giai đoạn trình phân tích cú pháp được tạo lấy luồng mã thông báo từ lexer làm đầu vào và có hai công việc. Đầu tiên, nó xác minh tính hợp lệ của thứ tự mã thông báo bằng cách cố gắng khớp chúng với bất kỳ một trong các quy tắc ngữ pháp được xác định trong tệp ngữ pháp BNF của nó. Điều này đảm bảo rằng các cấu trúc ngôn ngữ hợp lệ đang được hình thành bởi các mã thông báo trong luồng mã thông báo. Công việc thứ hai của trình phân tích cú pháp là tạo cây cú pháp trừu tượng (AST) – dạng xem dạng cây của mã nguồn sẽ được sử dụng trong giai đoạn tiếp theo (biên dịch)

Chúng ta có thể xem một dạng AST do trình phân tích cú pháp tạo ra bằng tiện ích mở rộng php-ast. AST nội bộ không được hiển thị trực tiếp vì nó không đặc biệt "sạch" để làm việc với (về tính nhất quán và khả năng sử dụng chung), và do đó, tiện ích mở rộng php-ast thực hiện một số chuyển đổi trên nó để làm cho nó hoạt động tốt hơn

Chúng ta hãy xem AST để biết một đoạn mã thô sơ

$code = <<<'code'

$a = 1;
code;

print_r(ast\parse_code($code, 30));

đầu ra

ast\Node Object (
    [kind] => 132
    [flags] => 0
    [lineno] => 1
    [children] => Array (
        [0] => ast\Node Object (
            [kind] => 517
            [flags] => 0
            [lineno] => 2
            [children] => Array (
                [var] => ast\Node Object (
                    [kind] => 256
                    [flags] => 0
                    [lineno] => 2
                    [children] => Array (
                        [name] => a
                    )
                )
                [expr] => 1
            )
        )
    )
)

Các nút cây (thường thuộc loại

Line 1: T_OPEN_TAG ('
4) có một số thuộc tính

  • Line 1: T_OPEN_TAG ('
    5 – Một giá trị số nguyên để mô tả loại nút; . g.
    Line 1: T_OPEN_TAG ('
    6 => 132,
    Line 1: T_OPEN_TAG ('
    7 => 517,
    Line 1: T_OPEN_TAG ('
    8 => 256)
  • Line 1: T_OPEN_TAG ('
    9 – Một số nguyên xác định hành vi quá tải (e. g. một nút
    $code = <<<'code'
    
    $a = 1;
    code;
    
    print_r(ast\parse_code($code, 30));
    
    0 sẽ có các cờ để phân biệt hoạt động nhị phân nào đang xảy ra)
  • $code = <<<'code'
    
    $a = 1;
    code;
    
    print_r(ast\parse_code($code, 30));
    
    1 – Số dòng, như đã thấy từ thông tin mã thông báo trước đó
  • $code = <<<'code'
    
    $a = 1;
    code;
    
    print_r(ast\parse_code($code, 30));
    
    2 – các nút con, điển hình là các phần của nút được chia nhỏ hơn nữa (e. g. một nút chức năng sẽ có con. tham số, kiểu trả về, nội dung, v.v.)

Đầu ra AST của giai đoạn này rất thuận tiện để sử dụng cho các công cụ như máy phân tích mã tĩnh (e. g. Phan)

Giai đoạn 3 – Tổng hợp

Giai đoạn biên dịch sử dụng AST, nơi nó phát ra mã lệnh bằng cách duyệt qua cây một cách đệ quy. Giai đoạn này cũng thực hiện một vài tối ưu hóa. Chúng bao gồm việc giải quyết một số lệnh gọi hàm với các đối số theo nghĩa đen (chẳng hạn như

$code = <<<'code'

$a = 1;
code;

print_r(ast\parse_code($code, 30));
3 đến
$code = <<<'code'

$a = 1;
code;

print_r(ast\parse_code($code, 30));
4) và gấp các biểu thức toán học không đổi (chẳng hạn như
$code = <<<'code'

$a = 1;
code;

print_r(ast\parse_code($code, 30));
5 đến
$code = <<<'code'

$a = 1;
code;

print_r(ast\parse_code($code, 30));
6)

Chúng ta có thể kiểm tra đầu ra opcode ở giai đoạn này theo một số cách, bao gồm với OPcache, VLD và PHPDBG. Tôi sẽ sử dụng VLD cho việc này, vì tôi cảm thấy đầu ra thân thiện hơn khi nhìn vào

Hãy xem đầu ra của tệp sau là gì. tập lệnh php

Line 1: T_OPEN_TAG ('
1

Thực hiện lệnh sau

Line 1: T_OPEN_TAG ('
2

đầu ra của chúng tôi là

Line 1: T_OPEN_TAG ('
3

Các opcode giống với mã nguồn ban đầu, đủ để thực hiện theo các thao tác cơ bản. (Tôi sẽ không đi sâu vào chi tiết về opcodes trong bài viết này, vì điều đó sẽ chiếm toàn bộ một số bài viết. ) Không có tối ưu hóa nào được áp dụng ở cấp opcode trong tập lệnh trên – nhưng như chúng ta có thể thấy, giai đoạn biên dịch đã thực hiện một số bằng cách giải quyết điều kiện không đổi (

$code = <<<'code'

$a = 1;
code;

print_r(ast\parse_code($code, 30));
7) thành
$code = <<<'code'

$a = 1;
code;

print_r(ast\parse_code($code, 30));
8

OPcache không chỉ đơn giản là lưu trữ mã opcode (do đó bỏ qua các giai đoạn từ vựng, phân tích cú pháp và biên dịch). Nó cũng đi kèm với nhiều cấp độ tối ưu hóa khác nhau. Hãy tăng mức độ tối ưu hóa lên bốn lượt để xem điều gì sẽ xảy ra

Chỉ huy

Line 1: T_OPEN_TAG ('
6

đầu ra

Line 1: T_OPEN_TAG ('
7

Chúng ta có thể thấy rằng điều kiện không đổi đã bị loại bỏ và hai lệnh

$code = <<<'code'

$a = 1;
code;

print_r(ast\parse_code($code, 30));
9 đã được nén thành một lệnh duy nhất. Đây chỉ là một phần của nhiều tối ưu hóa mà OPcache áp dụng khi thực hiện chuyển qua các opcode của tập lệnh. Tuy nhiên, tôi sẽ không đi qua các cấp độ tối ưu hóa khác nhau trong bài viết này, vì đó cũng là một bài viết.

Giai đoạn 4 – Phiên dịch

Giai đoạn cuối cùng là giải thích các opcodes. Đây là nơi mã lệnh được chạy trên máy ảo Zend Engine (ZE). Thực sự có rất ít điều để nói về giai đoạn này (ít nhất là từ góc độ cấp cao). Đầu ra hầu như là bất cứ thứ gì tập lệnh PHP của bạn xuất ra thông qua các lệnh như

ast\Node Object (
    [kind] => 132
    [flags] => 0
    [lineno] => 1
    [children] => Array (
        [0] => ast\Node Object (
            [kind] => 517
            [flags] => 0
            [lineno] => 2
            [children] => Array (
                [var] => ast\Node Object (
                    [kind] => 256
                    [flags] => 0
                    [lineno] => 2
                    [children] => Array (
                        [name] => a
                    )
                )
                [expr] => 1
            )
        )
    )
)
0,
ast\Node Object (
    [kind] => 132
    [flags] => 0
    [lineno] => 1
    [children] => Array (
        [0] => ast\Node Object (
            [kind] => 517
            [flags] => 0
            [lineno] => 2
            [children] => Array (
                [var] => ast\Node Object (
                    [kind] => 256
                    [flags] => 0
                    [lineno] => 2
                    [children] => Array (
                        [name] => a
                    )
                )
                [expr] => 1
            )
        )
    )
)
1,
ast\Node Object (
    [kind] => 132
    [flags] => 0
    [lineno] => 1
    [children] => Array (
        [0] => ast\Node Object (
            [kind] => 517
            [flags] => 0
            [lineno] => 2
            [children] => Array (
                [var] => ast\Node Object (
                    [kind] => 256
                    [flags] => 0
                    [lineno] => 2
                    [children] => Array (
                        [name] => a
                    )
                )
                [expr] => 1
            )
        )
    )
)
2, v.v.

Vì vậy, thay vì đào sâu vào bất cứ điều gì phức tạp ở giai đoạn này, đây là một sự thật thú vị. PHP yêu cầu chính nó như một phần phụ thuộc khi tạo VM của riêng nó. Điều này là do VM được tạo bởi một tập lệnh PHP, do nó đơn giản hơn để viết và dễ bảo trì hơn

Phần kết luận

Chúng ta đã xem qua bốn giai đoạn mà trình thông dịch PHP trải qua khi chạy mã PHP. Điều này liên quan đến việc sử dụng các tiện ích mở rộng khác nhau (bao gồm mã thông báo, php-ast, OPcache và VLD) để thao tác và xem đầu ra của từng giai đoạn

Tôi hy vọng bài viết này đã giúp cung cấp cho bạn sự hiểu biết toàn diện hơn về trình thông dịch của PHP, cũng như cho thấy tầm quan trọng của tiện ích mở rộng OPcache (đối với cả khả năng lưu trữ và tối ưu hóa của nó)

Chia sẻ bài viết này

Mã PHP nên được thực thi ở đâu?

Thomas Punt

Thomas là một sinh viên Công nghệ Web mới tốt nghiệp từ Vương quốc Anh. Anh ấy rất quan tâm đến lập trình, đặc biệt tập trung vào các công nghệ phát triển web phía máy chủ (cụ thể là PHP và Elixir). Anh ấy đóng góp cho PHP và các dự án mã nguồn mở khác trong thời gian rảnh rỗi, cũng như viết về các chủ đề mà anh ấy thấy thú vị

Mã PHP được thực thi như thế nào?

Nói chung, trình thông dịch PHP trải qua bốn giai đoạn khi thực thi mã. Lexing . Phân tích cú pháp . Biên dịch .

PHP được thực thi trên máy chủ hay máy khách?

PHP là ngôn ngữ kịch bản phía máy chủ phổ biến trong phát triển web. PHP được thực thi trên máy chủ , sau đó được dịch sang phía máy khách bằng mã HTML. Trình duyệt web của bạn sau đó sẽ tạo một đầu ra.

Mã PHP thường được chạy ở đâu?

Tất cả mã PHP chỉ được thực thi trên máy chủ web , không phải trên máy tính cục bộ của bạn. Ví dụ: nếu bạn hoàn thành một biểu mẫu trên một trang web và gửi biểu mẫu đó hoặc nhấp vào liên kết đến một trang web được viết bằng PHP, thì không có mã PHP thực nào chạy trên máy tính của bạn.

PHP thực thi và xử lý tập lệnh ở đâu?

Đó là máy chủ web —chứ không phải trình duyệt web—có thể tương tác với trình thông dịch PHP. Trình duyệt của bạn có thể tự xử lý HTML, nhưng nó phải yêu cầu máy chủ web xử lý các tập lệnh PHP. Máy chủ đó có thể lấy các tập lệnh PHP của bạn và chạy chúng, sau đó nhận phản hồi và gửi lại cho trình duyệt của bạn.