Skip to content
academia.sh

Lesson 02 / 14

Syntax and Tags

A file with a syntax error never starts running, how that differs from an error thrown during execution, and how code and template mix through tags in a PHP file.

Published Updated

In this lesson

  • Shows with the exit code and the server response that a file with a syntax error stops before any line runs
  • Tells an error thrown during execution apart from a syntax error by the presence of earlier output
  • Mixes code and template with opening, closing and short echo tags
  • Measures how a closing tag at the end of a file can add bytes to the output
Contents

The Execution Model lesson left two rows side by side in the core table: halted execution printed one line and exited with 255, while syntax error printed no line and exited with 255. Both belong to the halts channel, but they say two different things at once: one is the cutting of an execution that is already running, the other is an execution that could never start. This lesson measures the difference.

The difference comes from how PHP handles a file. Before the file is run, it is read in full and parsed according to the rules of the language. If parsing fails, not a single line of the file runs. Tags decide where a file begins and ends: PHP counts only the part between the opening and closing tags as code, and writes everything outside them to the output as it is. This lesson measures the two subjects together, because both concern when a file starts running.

A File That Never Starts Running

Two small files carry the same two steps: first print a line, then run into a problem. In the first, the problem appears during execution: a RuntimeException is thrown and not caught. In the second, the problem is against the language itself: the statement $x = ; expects a value and meets a semicolon. The third file is sound and serves as the comparison. All three are given as files only, without output blocks.

<?php
// fatal.php — prints a line first, then throws an uncaught exception
echo "before\n";
throw new RuntimeException('stop');
<?php
// broken.php — syntax error: the statement $x = ; has no value
echo "before\n";
$x = ;
<?php
// sound.php — valid syntax, prints only one line
echo "before\n";

The script below runs each file in a separate execution. display_errors=0 keeps the error text out of the output; only the lines the file printed and the exit code remain. php -l only parses the file without running it and reports the exit code.

# separate.sh — the same two steps, two different stops: thrown during execution and failed in parsing
php -d display_errors=0 fatal.php; echo "fatal exit=$?"
php -d display_errors=0 broken.php; echo "broken exit=$?"
php -l sound.php >/dev/null 2>&1; echo "sound -l=$?"
php -l broken.php >/dev/null 2>&1; echo "broken -l=$?"
before
fatal exit=255
broken exit=255
sound -l=0
broken -l=255

LE6. Both files exited with 255, but fatal.php managed to print its first line and broken.php did not. What is measured is the presence of output, not the text of the error message: the message changes between versions and carries a file path. php -l gave 0 for the sound file and 255 for the broken one; that is, the parsing result can be learned without running the file at all.

The same difference turns into a status code on the built-in server. When broken.php is requested, the file cannot start running, so the server has no response body and the status code is 500. fatal.php first prints the before line; the body has begun, and the error that comes afterward can no longer change the status code. The server starts with -d display_errors=0, so no error text appears in the body.

# server.sh — a file with a syntax error gives 500, a file that throws during execution gives 200 with a partial body
php -S 127.0.0.1:8081 -d display_errors=0 >/dev/null 2>&1 &
pid=$!
for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8081/sound.php && break; sleep 0.1; done
curl -s -w 'status=%{http_code}\n' http://127.0.0.1:8081/sound.php
curl -s -w 'status=%{http_code}\n' http://127.0.0.1:8081/fatal.php
curl -s -w 'status=%{http_code}\n' http://127.0.0.1:8081/broken.php
kill $pid
before
status=200
before
status=200
status=500

LE7. Three requests gave three different results: the sound file gave before and 200, the file that failed during execution gave the same before line and still 200, and the file with the syntax error gave 500 with no body. The 200 on the second line does not mean the error was lost; because the error came after the body had begun, it did not reach the status code. For this reason the status code is not a reliable measure of whether a problem was heard; it reflects only whether the error came before or after the output.

Mixing Code and Template

A PHP file can carry code and plain text interleaved. The opening tag <?php starts a code section and the closing tag ?> ends it; everything between the two is parsed, and everything outside goes to the output unchanged. <?= is the short form used to print a value: <?= $product ?> does the same job as <?php echo $product; ?>. In this way a file can be written as both a script and a template. The example below produces an HTML list with a loop over an array. The foreach (...): and endforeach; form is the notation meant for blocks that spread across tags instead of curly braces.

<?php
// template.php — mixing code and template, with the short echo tag
$products = ['pencil', 'notebook', 'eraser'];
?>
<ul>
<?php foreach ($products as $product): ?>
  <li><?= $product ?></li>
<?php endforeach; ?>
</ul>
<ul>
  <li>pencil</li>
  <li>notebook</li>
  <li>eraser</li>
</ul>

LE8. A single newline directly after each ?> tag is not written to the output; PHP counts it as part of the closing tag. For this reason the output has no blank lines left over from the code lines, and the three <li> lines stack one under another with no gap. This behavior holds only for a single newline; if two newlines follow the tag, the second one enters the output.

This result matters at the very end of a file. In a file that carries only PHP code, no closing tag is written; the file is valid without a closing tag for the opening tag. The reason is measurable: a newline or space that slips in after the closing tag enters the output. The script below writes the same one-line program in three forms and measures how many bytes each produces.

# closing.sh — a newline left after the closing tag at the end of a file adds bytes
printf '<?php\necho "a";\n' > open.php
printf '<?php\necho "a";\n?>\n' > closed1.php
printf '<?php\necho "a";\n?>\n\n' > closed2.php
for d in open closed1 closed2; do
  printf '%s %s\n' "$d" "$(php $d.php | wc -c | tr -d ' ')"
done
open 1
closed1 1
closed2 2

LE9. The file without a tag and the file with a closing tag followed by one newline produced one byte, the echo "a" itself; the file with a closing tag followed by two newlines produced two bytes. The harm of the extra byte is that it is invisible: every byte that enters the output before the response headers are sent makes setting headers too late. For this reason, if the file carries only code, the final tag is not written.

A Line Comment Also Ends at the Closing Tag

Tags have one more consequence that they process before parsing: a closing tag ends the code section even inside a line comment. Everything after the tag, including the rest of the comment, is plain text; the following lines stop being code. No syntax error appears in this case, because there is no code left to parse, and the file runs without trouble.

<?php
// comment.php — a closing tag inside a line comment ends the code section
echo "a\n"; // comment ?> this part is text
echo "b\n";
a
 this part is text
echo "b\n";

LE10. The output holds the a line, then the part of the comment after the tag, and finally the line echo "b\n"; itself, that is, its source text, as plain text; b was never printed. There was no signal at all: no warning, no status code. This observation falls in the silent channel of the table and does not change this lesson’s channel; it only shows that tags are processed before parsing, independently of the meaning of the comment.

Case Sensitivity

In PHP, whether a name is sensitive to upper and lower case depends on the kind of name. Keywords and function names are insensitive; variable names and constant names defined with define() are sensitive. A typo is therefore not noticed in one place and noticed in another. The file below tries four names with upper and lower case differences; the error handler keeps a counter.

<?php
// case.php — function names are case-insensitive, variable and constant names are sensitive
$notices = 0;
set_error_handler(function (int $level) use (&$notices): bool {
    $notices++;
    return true;
});

$name = 'Ada';
define('LIMIT', 5);

ECHO 'keyword: worked in uppercase too', "\n";
echo 'STRLEN:     ', STRLEN('abc'), "\n";
echo 'notices:    ', $notices, "\n";

$seen = $Name;
echo 'notices:    ', $notices, ' (after reading $Name)', "\n";
echo 'defined(limit): ', var_export(defined('limit'), true), "\n";
echo 'defined(LIMIT): ', var_export(defined('LIMIT'), true), "\n";
keyword: worked in uppercase too
STRLEN:     3
notices:    0
notices:    1 (after reading $Name)
defined(limit): false
defined(LIMIT): true

LE11. ECHO and STRLEN worked even though they were written in uppercase, and produced no notice. $Name is a variable separate from $name; when it was read, a warning was heard and the counter rose to one. In constant names too, limit and LIMIT are different: only the exact spelling was found. So the same typo is silent in a function name and a warning in a variable name; the kind of name that was misspelled decides which channel the error is heard through.

Single and Double Quotes

A string is written in two forms in PHP, and the two are not the same. In a single-quoted string, variables and escape sequences are not interpreted; only \' and \\ carry a special meaning. In a double-quoted string a variable is substituted and escape sequences such as \n are resolved. The file below writes the same text in both forms and counts their lengths.

<?php
// strings.php — single quotes resolve neither variables nor escapes, double quotes do
$to = 'Ada';
$single = 'welcome $to\n';
$double = "welcome $to\n";
$braced = "welcome {$to}x";

echo 'single:  ', $single, "|\n";
echo 'double:  ', $double, "|\n";
echo 'braced:  ', $braced, "|\n";
echo 'single length: ', strlen($single), "\n";
echo 'double length: ', strlen($double), "\n";
echo 'backslash: ', strlen('a\\b'), ' ', strlen('a\nb'), ' ', strlen("a\nb"), "\n";
single:  welcome $to\n|
double:  welcome Ada
|
braced:  welcome Adax|
single length: 13
double length: 12
backslash: 3 4 3

LE12. The single-quoted string came out with $to and \n as they are, so its length was thirteen. In the double-quoted string Ada took the place of $to and \n became a single newline character. The curly braces decide where the variable name ends. The last line shows the difference at the character level: in single quotes \n is two characters, in double quotes one. Neither produces a notice; a \n written in single quotes silently fails to give the expected newline.

Comment Forms

PHP recognizes three comment forms: # and // run to the end of the line, and /* */ runs until its closing. Multi-line comments do not nest: the first */ ends the comment. The file below surrounds five echo statements with different comments.

<?php
// comment_kinds.php — measures where each comment form ends
echo "1\n"; # line-end comment
echo "2\n"; // line-end comment
echo "3\n"; /* inline comment */ echo "4\n";
/* outer comment /* inner comment */ echo "5\n";
1
2
3
4
5

LE13. All five lines were printed. When /* opened a second time on the fourth line, the comment did not deepen; the first */ closed the comment and echo "5\n" ran as code. Code that relies on closing a block nested inside another runs whatever comes after the closing. This too is in the silent channel: there is no error or warning, only the amount of running code has grown.

echo and print

PHP has two language constructs that write to the screen, and they are not the same. echo takes several values separated by commas and returns no value. print takes a single value and always returns 1; for this reason it can be used inside an expression. The file below measures the two behaviors and the way an array element is written inside double quotes.

<?php
// output.php — print returns a value, echo takes many arguments, an array element resolves in double quotes
$result = print "a\n";
echo 'print returned: ', $result, "\n";
echo 'one', 'two', "\n";

$items = [10, 20];
echo "array: $items[0]-$items[1]\n";
echo 'in single quotes: $items[0]', "\n";
a
print returned: 1
onetwo
array: 10-20
in single quotes: $items[0]

LE14. print wrote the line and returned 1; echo wrote the two strings one after another with nothing between them. In the double-quoted string $items[0] was replaced by the value of the element, and in the single-quoted one it stayed as it was. None of these produces a notice; the difference can be seen only by looking at the output.

The Bounding Measurement

The rule that a file with a syntax error never runs applies to each file separately. LE15. If a file pulls in another file with include, the faulty one is the pulled-in file and the main file has already started running. Because the pulled-in file is parsed during execution, the error is thrown at that point as a ParseError, and try with catch can catch it. The file below pulls in the broken.php written earlier.

<?php
// main.php — a file with a syntax error is included; the main file has started running
echo "main started\n";
try {
    include 'broken.php';
} catch (ParseError $error) {
    echo 'caught ', get_class($error), "\n";
}
echo "main finished\n";
main started
caught ParseError
main finished

The main file printed all three of its lines, while the before line of broken.php did not appear: the pulled-in file again ran none of its own lines. The rule stayed the same and its scope changed: parsing is done per file, and the failure of one file reaches only as far as the catcher of the file that is pulling it in. If there is no catcher, the ParseError too falls into the halts channel.

Summary

  • The file is parsed in full before it runs; not a single line of a file with a syntax error runs.
  • An error thrown during execution keeps the earlier output: fatal.php printed one line, broken.php printed none; both exited with 255.
  • This lesson’s channel is halts: a syntax error cuts execution before it starts, and an error thrown during execution cuts it after it has started.
  • On the built-in server, a file with a syntax error gives 500 with no body; an error thrown during execution leaves partial output with 200 after the body has begun.
  • The ?> and <?= tags mix code and template; in a file that carries only code, no closing tag is written, because an extra newline after it adds bytes to the output.
  • The rule is per file: a syntax error in an included file is thrown, after the main file has run, as a ParseError that can be caught.

Next Step

It is now clear that the file started running and what is inside the tags. The Variables and Data Types lesson measures, inside the code, whether the type of a value changes without a signal: can a value lose its type?

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close