D
P
0
← All articles Baca dalam Bahasa Indonesia

WordPress & PHP in Production

`php -l` Says Unmatched '}' on Line 466 but the Braces Balance: A `?>` Inside a `//` Comment

· · 4 min read
`php -l` Says Unmatched '}' on Line 466 but the Braces Balance: A `?>` Inside a `//` Comment

Every PHP module I write for a client's membership directory site goes through the same gate before it touches the server: a local php -l lint, a check that the file is pure ASCII, a check for functions declared twice, then the file is written to /tmp on the server, linted there once more with php -l, and only then swapped into place with cat > so the owner stays www-data. The gate is boring, and that is the point. One day the local lint rejected a module file with a message that looked very specific:

Unmatched '}' on line 466

I opened line 466. An ordinary closing brace. I counted the brace pairs around it, balanced. I folded the file block by block from the top, still balanced. No function left open, no if with one closer too many. A whole work cycle went into hunting a brace that, as it turned out, was never missing.

The line that was actually broken sat near line 150, nowhere near the number in the error. It held a one-line documentation comment. The comment explained how the templates render a marker, and to make that clear I wrote the example markup verbatim inside the comment. The minimal shape looks roughly like this:

<?php
function example_render_marker( $mark ) {
    // templates render it like this: "<?php echo $mark; ?> SEEN"
    return $mark . ' SEEN';
}

To a human eye that is a comment. To PHP, the comment stops exactly at ?>.

Why this happens

The PHP tokenizer treats ?> as a hard token, even when it shows up in the middle of a comment. A single-line comment with // or # only runs until ?> or until the end of the line, whichever comes first. This is not a bug, it is documented behavior: the PHP closing tag always wins over a single-line comment.

So the moment the tokenizer passes the ?> inside that comment, PHP mode ends. The rest of that line and everything after it becomes raw HTML output, and the parser reads the remainder of the file as text. The brace that should have closed the function on the next line gets swallowed as text, the brace count shifts by one, and the parser only gets to complain when it meets a } far further down. That is why the message points at line 466 while the culprit sits near line 150. The number is honest from the parser's point of view and misleading from the point of view of someone looking for a brace.

What makes it hard to see is that the surrounding code is all correct. No missing bracket, no forgotten semicolon. The only thing wrong is two characters on a line that my editor paints grey as a comment, and grey by itself says "ignore me".

The fix

The fix in the file is trivial: the example markup inside the comment becomes a plain sentence describing the result, without ever writing <?php or ?> inside the comment.

<?php
function example_render_marker( $mark ) {
    // templates append " SEEN" after the echoed value
    return $mark . ' SEEN';
}

php -l passed immediately, and the file went up to the server through the usual gate.

The more valuable part is the diagnostic rule. When php -l points at an unmatched } in a file whose braces look balanced, do not start by counting braces. Grep for ?> inside comments first:

grep -nE '(//|#).*\?>' inc/*.php

That pattern catches any // or # comment that still contains a PHP closing tag and points straight at the line. In my case this one command would have saved a full cycle.

The writing rule I have kept since then: never write ?> or <?php … ?> inside a // or # comment. If the markup needs explaining, describe it in prose. A /* … */ block is fine only if it contains no ?> either.

This was not the first time a comment tripped the lint on the same project. Not long before, during the session that built the membership module, a */ sequence appeared inside a block comment because I wrote a name pattern like this:

/* patterns skipped: *_extra_*/raw */

The */ in the middle of that pattern quietly closed the comment early, the remainder was parsed as code, and a parse error followed. php -l caught it. Since then the lint step in the work plan explicitly reads php -l, ASCII check, and watch for */. The ?> case just adds one more item to the same list.

Lessons

Comments are not fully inert. Two character sequences are still read by PHP even inside a comment: ?> in a single-line comment, and */ in a block comment. Both get caught by php -l, but the error message does not necessarily point at the right place, and an unmatched } reported more than three hundred lines away from its source is the clearest example.

This lesson is a cousin of the ASCII-only rule I have long kept for PHP source: watch the literal characters in the file, not just the logic. Smart quotes, closing tags, comment terminators, all one family. Code that looks correct and a parser complaining on the wrong line almost always means some character carries more meaning than I assumed.