Skip to content

THRIFT-5959: Adopt PSR-12 in PHP library and align generator emission#3449

Closed
sveneld wants to merge 15 commits into
apache:masterfrom
sveneld:THRIFT-5959
Closed

THRIFT-5959: Adopt PSR-12 in PHP library and align generator emission#3449
sveneld wants to merge 15 commits into
apache:masterfrom
sveneld:THRIFT-5959

Conversation

@sveneld
Copy link
Copy Markdown
Contributor

@sveneld sveneld commented May 5, 2026

Summary

Implements THRIFT-5959: adopts PSR-12 across the PHP library and aligns the C++ generator emission with the same standard.

PSR-2 has been frozen since 2019. PSR-12 is its modern successor and what squizlabs/php_codesniffer ^3.10 (already a project dependency) supports out of the box. While reformatting, this PR also cleans up the PHP-only leading/trailing-underscore identifier conventions that the runtime and generator inherited from the original PHP port — every other Thrift language target (Python thrift_spec, Java metaDataMap, Ruby FIELDS, Go/C# Read/Write) uses neither leading nor trailing underscores.

The lint scope now covers the full lib/php/lib/, lib/php/test/, and the regenerated lib/php/test/Resources/packages/ tree (435 fixture files); vendor/bin/phpcs exits 0. Runtime behaviour is unchanged — the diff is renames, formatting, generator output, tooling, and docs.

What changed

Tooling

  • phpcs.xml.dist:
    • Ruleset PSR2PSR12.
    • Both legacy underscore exclusions removed.
    • Added Generic.Arrays.DisallowLongArraySyntax.
    • Removed the blanket <exclude-pattern>lib/php/test/Resources/packages/*</exclude-pattern>; the regenerated fixtures now lint cleanly via inline phpcs:disable directives in the autogen comment (see below).
  • lib/php/coding_standards.md: PSR-2 link replaced with PSR-12; explicit note that Apache Thrift PHP code does not use leading or trailing underscores on identifiers.

Runtime renames (lib/php/lib/)

  • public static $_TSPECpublic static $tspec on every generated struct and on TApplicationException. Mirrors Python's thrift_spec.
  • Protected _read / _write helpers on TBase and TExceptionreadStruct / writeStruct (the new names avoid clashing with the public abstract read / write, which take a different signature).
  • Private helpers _readMap / _readList / _writeMap / _writeList → drop the leading _.
  • ~85 trailing-underscore properties across 30 files (TServer, TSocket, TBufferedTransport, TBinaryProtocol, THttpClient, TCurlClient, TServerSocket, TSSLSocket, TFramedTransport, TPhpStream, TJSONProtocol, etc.) → drop the trailing _ (~509 internal reference updates).
  • TJSONProtocol::$contextStack / $context / $reader: rename and tighten visibility from public to private. An audit found no callers outside the class — every reference was internal $this->... access — so a deprecation shim was not needed and was deliberately not introduced.
  • One-letter $p private property in JSON / SimpleJSON context classes → descriptive $protocol.

Hand-written test fixtures

TestSerializerStruct.php, TestRichException.php, NestedStruct.php, ComplexStruct.php, and TBaseTest.php updated alongside the runtime; AbstractValidatorTestCase now inspects validateForRead / validateForWrite; ReflectionHelper-based property-name string arguments updated.

C++ generator (compiler/cpp/src/thrift/generate/t_php_generator.cc)

Naming alignment with the new runtime:

  • Emits $tspec, readStruct(...), writeStruct(...), validateForRead, validateForWrite.
  • Service-client emits $input / $output / $seqid (was $input_ / $output_ / $seqid_).
  • Enum class emits $names (was $__names).
  • REST helper emits $impl (was $impl_).
  • Inline-mode UUID locals emit $uuidBin / $uuidHex (was $_uuid_bin / $_uuid_hex).
  • Inline-mode strict-binary version check emits TBinaryProtocol::VERSION_1 / VERSION_MASK named constants instead of the 0x80010000 / 0xffff0000 magic numbers (matches what the runtime TBinaryProtocol already does for non-inlined paths).

PSR-12 emission alignment:

  • File header reordered to <?php ⇢ blank ⇢ file-level docblock ⇢ blank ⇢ namespace ⇢ blank ⇢ use block (PSR-12 §3).
  • Short array syntax [...] for $names, $tspec, type-spec sub-arrays ('key' / 'val' / 'elem'), const values (struct / map / list / set), and local accumulators / default parameter values.
  • Class constants emit explicit public visibility.
  • public static / protected static ordering instead of static public / static protected.
  • Brace-on-next-line for jsonSerialize, validators, REST handlers, REST constructor.
  • Inline-mode bodies emit four-space indents (was two-space leftovers from the original PSR-2 port).
  • The OOP read/write branches no longer emit a stray blank line before the closing class brace.
  • Pre-existing indent bug in the nested 'val' (map) and 'elem' (list / set) sub-specs fixed: indent_down() now precedes the closing-bracket emission so the close lines up with the opening 'val' => / 'elem' =>.
  • $json = new stdClass(); instead of new stdClass.
  • Single trailing newline at file close (was double in enum and classmap-mode service files).
  • Multi-line if (...) { throw ...; } for the strict-binary version check instead of an inline control structure.
  • Process-dispatcher emits 'process_' . $fname and ' . $fname . ' with proper PSR-12 concat spacing; unknown-method exception path uses indent() rather than hard-coded four-space prefixes layered on top of indent_up().
  • $arr[1] * 4294967296 and $name[] = $value use proper PSR-12 operator spacing.
  • Trailing whitespace stripped from empty docblock lines (PHP-local override of generate_php_docstring_comment; the cross-language base t_generator::generate_docstring_comment is untouched).

phpcs:disable directives in the autogen docblock are emitted per file shape, not blanket:

File shape Disables
Plain struct / enum / interface / REST (none — clean docblock)
Service args / result helper struct Squiz.Classes.ValidClassName
Service client / processor PSR1.Methods.CamelCapsMethodName
Top-level Constant.php PSR1.Methods.CamelCapsMethodName
Classmap-mode service / Types files all three (multi-class file containing client + processor + args/result)

The three cross-Thrift sniffs we ever disable cover naming conventions that must remain consistent with the other Thrift language generators (snake_case process_* / send_* / recv_* method names, <service>_<method>_args class names, multi-class classmap files). Rule lists are hoisted to named member vectors so a future rename or addition is a one-line edit.

generate_program_header and generate_service_header collapse into a shared emit_file_header helper; php_autogen_comment delegates to the inherited t_oop_generator::autogen_comment() and splices the optional phpcs:disable block before the closing */, avoiding duplication of the base docblock structure.

C extension (lib/php/src/ext/thrift_protocol/php_thrift_protocol.cpp)

Critical for cross-language tests with PHP server (without this, the accelerated binary protocol crashes with TProtocolException: Attempt deserialize to non-Thrift object):

  • All seven zend_read_static_property(..., "_TSPEC", sizeof("_TSPEC")-1, ...) calls now read "tspec" via the idiomatic ZEND_STRL("tspec") Zend macro.
  • Pre-existing typo on line 1175 (sizeof("_TPSEC")-1, only working by accident because both literals had the same length) is fixed in the same pass.

Breaking change

This is a deliberate, documented public-API BC break for downstream PHP consumers. Affected surfaces:

  1. Class::$_TSPEC reads on every generated struct → must use Class::$tspec. PHP has no __getStatic, so this rename cannot be shimmed; callers must migrate at the same time as the upgrade.
  2. Class::$__names reads on enums → must use Class::$names. Same constraint.
  3. TJSONProtocol::$contextStack / $context / $reader are now private. Audit found no external callers in the runtime or test suite, so the public surface is removed rather than renamed.
  4. Subclasses of TServer / TSocket / TBufferedTransport / TBinaryProtocol / THttpClient / TCurlClient / TServerSocket / TSSLSocket / TFramedTransport / TPhpStream / TSimpleJSONProtocol referencing protected trailing-underscore properties → drop the trailing _ on the new name.
  5. Subclasses overriding _read / _write on TBase or TException → use readStruct / writeStruct.

The JIRA ticket needs the Breaking-Change label per the contributing guide (CLI authorization not granted; will be set manually).

Out of scope (deferred to follow-up tickets)

  • declare(strict_types=1) — semantic change; warrants its own ticket.
  • PHP 8.1 modernization (constructor property promotion, readonly, native enums) — separate refactor.
  • Embedded-underscore generated method names (init_FOO, process_<name>, send_<name>, recv_<name>) — deliberate cross-Thrift snake_case generator-name conventions; PSR-12 does not forbid snake_case method names.
  • lib/php/src/Thrift.php legacy non-namespaced bundle (not autoloaded since the PSR-4 layout was adopted) — separate cleanup.
  • TException::__wakeup() parity with TBase and the dead-code TException::$tmethod local copy — pre-existing inconsistencies on master, deserve their own behavioural-change ticket.
  • Pre-existing Undefined array key 1 warning at TSocketPoolTest:210 from a mock data-provider mismatch — unrelated to PSR-12.

Test plan

  • vendor/bin/phpcs over lib/php/lib, lib/php/test, and lib/php/test/Resources/packages/ (435 fixture files) — exits 0 with zero output.
  • vendor/bin/phpstan analyse -c lib/php/phpstan.neon — no issues.
  • vendor/bin/phpunit -c lib/php/phpunit.xml — 629 tests pass, 0 errors, 0 failures (one unrelated pre-existing warning at TSocketPoolTest.php:210).
  • make stubs idempotent — diff-free on second run.
  • git diff compiler/cpp/src/thrift/generate/ limited to t_php_generator.cc; no other language generators affected (the t_generator.cc base is untouched).
  • CI: GitHub Actions lib-php matrix (PHP 8.1 / 8.2 / 8.3 / 8.4 / 8.5) is green.
  • CI: lib-php static-code-analysis (phpcs + phpstan) is green.
  • CI: cross-language cross-test jobs that exercise PHP-as-server with the accelerated binary protocol (cpp, go, rs, java, py, nodejs, nodets, rb clients) are green — required the C extension tspec lookup fix.

Checklist

  • Did you create an Apache Jira ticket? (THRIFT-5959)
  • If a ticket exists: Does your pull request title follow the pattern "THRIFT-NNNN: describe my issue"?
  • Did you squash your changes to a single commit? (Will squash before merge — currently 15 commits for reviewability of each step.)
  • Did you do your best to avoid breaking changes? Breaking change is intentional and central to this ticket — see "Breaking change" section above. JIRA ticket needs the Breaking-Change label.
  • Not applicable — this change involves code, so [skip ci] is not used.

Client: php

Switch the PHP coding standard from PSR-2 (frozen since 2019) to PSR-12,
remove the legacy underscore exclusions in phpcs.xml.dist, and clean up
PHP-only underscore quirks across both the runtime and the generator.

Renames (public-API BC break for downstream PHP consumers):
* `$_TSPEC` -> `$tspec` on every generated struct, on TApplicationException,
  and in the C++ generator emission. Mirrors Python's `thrift_spec`.
* `_read` / `_write` on TBase and TException -> `readStruct` / `writeStruct`
  (the new names avoid clashing with the public abstract `read` / `write`).
* Private helpers `_readMap` / `_readList` / `_writeMap` / `_writeList`
  -> `readMap` / `readList` / `writeMap` / `writeList`.
* Trailing-underscore properties across the runtime (TServer, TSocket,
  TBufferedTransport, TBinaryProtocol, THttpClient, TCurlClient,
  TJSONProtocol, etc., ~85 declarations / ~509 references) -> drop the `_`.
* Generator-emitted `$input_` / `$output_` / `$seqid_` on service clients
  -> `$input` / `$output` / `$seqid`.
* Generator-emitted `$__names` on enums -> `$names`.
* Private validators `_validateForRead` / `_validateForWrite` ->
  `validateForRead` / `validateForWrite`.
* REST helper `$impl_` -> `$impl`.

Other PSR-12 alignments in the C++ generator:
* `static public` / `static protected` -> `public static` / `protected
  static`.
* Class constants emitted with explicit `public` visibility.
* Method declarations now place `{` on the next line (jsonSerialize,
  validators, REST handlers, REST constructor).

Tooling and docs:
* phpcs.xml.dist: ruleset PSR2 -> PSR12, both underscore exclusions
  removed.
* lib/php/coding_standards.md: updated to PSR-12 with a note that
  Apache Thrift PHP code does not use leading or trailing underscores.

Test suite updated to use the new property and method names; hand-written
fixtures (TestSerializerStruct, TestRichException, NestedStruct,
ComplexStruct, TBaseTest) updated alongside the runtime.

Verified locally with vendor/bin/phpcs (clean), vendor/bin/phpstan
(clean) and vendor/bin/phpunit -c lib/php/phpunit.xml (629 tests pass,
0 errors, 0 failures; one unrelated pre-existing warning at
TSocketPoolTest:210).

Generated fixtures under lib/php/test/Resources/packages/ are
regenerated by `make stubs`; they are excluded from phpcs and remain
untracked per existing project convention.

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sveneld added 14 commits May 5, 2026 22:43
Client: php

`$p` was a one-letter private property holding the parent protocol
reference in the JSON / SimpleJSON context classes (LookaheadReader,
ListContext, PairContext, StructContext, MapContext). Renames it to the
descriptive `$protocol` (matching the constructor parameter name) so
calls like `$this->protocol->getTransport()->...` read clearly.

Affected files:
* lib/php/lib/Protocol/JSON/LookaheadReader.php
* lib/php/lib/Protocol/JSON/ListContext.php
* lib/php/lib/Protocol/JSON/PairContext.php
* lib/php/lib/Protocol/SimpleJSON/ListContext.php
* lib/php/lib/Protocol/SimpleJSON/StructContext.php
* lib/php/lib/Protocol/SimpleJSON/MapContext.php

These are private properties, so this rename is internal-only with no
public BC impact.

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client: php

Replace legacy `array(...)` with the short `[...]` syntax (PHP 5.4+)
across the runtime, hand-written test fixtures, and the C++ generator
emission. Add `Generic.Arrays.DisallowLongArraySyntax` to phpcs.xml.dist
to enforce going forward.

Generator updates (compiler/cpp/src/thrift/generate/t_php_generator.cc):
* Enum `$names` table emits `[ ... ]` (was `array( ... )`).
* Struct `$tspec` and the nested map / list / set / element type-spec
  arrays emit short syntax.
* `render_const_value` emits short syntax for struct, map, list, and
  set constants — including the `new Foo([...])` call shape.
* Local accumulators (`$prefix = [];`) and default parameter values
  (`= []`) emit short syntax.

Hand-written runtime: `vendor/bin/phpcbf` auto-converted the remaining
sites in `lib/php/lib/` and `lib/php/test/Unit/`.

`TApplicationException::$tspec` is reformatted to one element per line
with a trailing comma, matching the generator's emission style; the
previous form was a hard-to-scan multi-line nested call without aligned
indentation.

Verified `vendor/bin/phpcs` (clean), `vendor/bin/phpunit -c
lib/php/phpunit.xml` (629 tests pass, 0 errors, 0 failures), and
`make stubs` (regenerated fixtures show only the expected array-syntax
diff).

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ties

Client: php

Add the `Thrift\Base\TrailingUnderscorePropertyCompat` trait, a
transitional aid for downstream code still reading or writing the old
trailing-underscore property names (`$obj->host_`, `$obj->context_`,
etc.) renamed in the THRIFT-5959 PSR-12 cleanup. The trait implements
`__get` / `__set` / `__isset` / `__unset`; when an inaccessible name
ending in `_` matches an existing property without the underscore, the
access is proxied and an `E_USER_DEPRECATED` notice is emitted pointing
at the new name. Names that do not match an existing property fall
through to PHP's default undefined-property behaviour.

The trait is added on the runtime base classes so subclasses inherit
it automatically:

* `Thrift\Transport\TTransport` (covers TSocket, TSSLSocket,
  TBufferedTransport, TFramedTransport, TMemoryBuffer, TPhpStream,
  TSocketPool, TCurlClient, THttpClient, TNullTransport).
* `Thrift\Server\TServer` (covers TSimpleServer, TForkingServer).
* `Thrift\Server\TServerTransport` (covers TServerSocket,
  TSSLServerSocket).
* `Thrift\Protocol\TProtocol` (covers TBinaryProtocol,
  TBinaryProtocolAccelerated, TCompactProtocol, TJSONProtocol,
  TSimpleJSONProtocol, TProtocolDecorator, TMultiplexedProtocol,
  StoredMessageProtocol).
* `Thrift\Protocol\JSON\BaseContext` (covers ListContext, PairContext,
  LookaheadReader uses no parent so trait used directly there is not
  needed — JSON Context classes inherit).
* `Thrift\Protocol\SimpleJSON\Context` (covers ListContext,
  StructContext, MapContext).

Standalone classes use the trait directly:
* `Thrift\TMultiplexedProcessor`
* `Thrift\Factory\TBinaryProtocolFactory`

Note: PHP has no `__getStatic` magic method, so the
`Class::$_TSPEC -> Class::$tspec` rename remains a hard break and
cannot be shimmed; see the PR description for migration guidance.

Six unit tests cover read, write, isset, undefined-name fall-through,
and the no-shim-without-real-property guard.

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client: php

The previous approach added a `TrailingUnderscorePropertyCompat` trait
on every base class, which was overkill: the only public instance
properties renamed by THRIFT-5959 are three on `TJSONProtocol`
(`$contextStack_`, `$context_`, `$reader_`); every other rename was
on `protected` or `private` members that are not part of the public
contract.

Replace the trait with a focused `__get` / `__set` / `__isset` /
`__unset` block on `TJSONProtocol` that only handles those three
aliases. The map of deprecated names lives in a class constant so the
shim is easy to grep and easy to remove once the deprecation period
ends.

Static properties (`$_TSPEC`, `$__names`) cannot be shimmed via magic
methods because PHP has no `__getStatic`. Those renames remain a hard
break, documented in the PR description.

Files removed:
* lib/php/lib/Base/TrailingUnderscorePropertyCompat.php
* lib/php/test/Unit/Lib/Base/TrailingUnderscorePropertyCompatTest.php

Files reverted (trait usage removed):
* TTransport, TServer, TServerTransport, TProtocol, BaseContext,
  Context, TMultiplexedProcessor, TBinaryProtocolFactory.

New test `TJSONProtocolDeprecatedAliasesTest` (10 cases) pins the
read / write / isset / fall-through behaviour of the inline shim.

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client: php

The PSR-12 cleanup renamed `public static $_TSPEC` to `$tspec` on every
generated struct, but the `thrift_protocol` C extension reads that
static property by name via `zend_read_static_property(..., "_TSPEC",
...)`. Without this fix the accelerated binary protocol crashes with
`TProtocolException: Attempt deserialize to non-Thrift object` whenever
it tries to look up a struct's spec, which propagates to every cross-
language test that involves a PHP server using the accelerated protocol
(php-php accel*, php-rs accel-binary, php-nodets accel-binary, and the
TBinarySerializer path in php-php json).

Replace all seven `_TSPEC` lookups in
`lib/php/src/ext/thrift_protocol/php_thrift_protocol.cpp` with the new
`tspec` name. Also fix a pre-existing typo on line 1175 that wrote
`_TPSEC` in the `sizeof()` argument; the read happened to work because
both literals were five characters long.

Found by re-running CI on the THRIFT-5959 branch — see
https://github.com/apache/thrift/actions/runs/25402704647 for the
failing cross-tests this commit unblocks.

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client: php

Audit found no callers outside `TJSONProtocol` for the three properties
that the previous commit had kept public behind a magic-method shim
(`$contextStack`, `$context`, `$reader`): every reference is internal
`$this->...` access. Tighten visibility from `public` to `private` and
delete the deprecation shim entirely (the previous `__get` / `__set` /
`__isset` / `__unset` block plus `DEPRECATED_PROPERTY_ALIASES`). Any
downstream code that was reading these properties was already crossing
an undocumented internal boundary, so making them private is the
correct cleanup rather than carrying a permanent compatibility layer
for them.

Also rename two leading-underscore local variables emitted by the
generator (`$_uuid_bin` / `$_uuid_hex`) to `$uuidBin` / `$uuidHex` for
consistency with the rest of the PSR-12 cleanup. PSR-12 sniffs do not
flag local variables, but the prior names were the last residue of the
old underscore convention in generator output; aligning them lets the
"no leading underscore in PHP identifiers" rule be applied uniformly.

Files removed:
* lib/php/test/Unit/Lib/Protocol/TJSONProtocolDeprecatedAliasesTest.php

Verified locally: vendor/bin/phpcs clean; vendor/bin/phpunit -c
lib/php/phpunit.xml -> 629 tests pass, 0 errors, 0 failures; make stubs
regenerates with the new $uuidBin / $uuidHex local names.

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client: php

The generator's `generate_php_type_spec` outdented the closing `]`
*after* emitting it for the `'val'` (map) and `'elem'` (list / set)
sub-specs, while the matching `'key'` branch did it in the right order.
Pre-existing bug — was already producing a 4-space over-indent under
the old `array(...)` syntax, but became visible once short array syntax
made the closing bracket sit on its own line.

Before:
    'val' => [
        'type' => TType::I64,
        ],          <- 16 spaces, should be 12
    ],

After:
    'val' => [
        'type' => TType::I64,
    ],
],

Move `indent_down()` ahead of the closing-bracket emission for both
branches so the close lines up with the opening `'val' =>` / `'elem' =>`.

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client: php

Replace the bare `<exclude-pattern>` with a comment that explains the
cross-Thrift code-generation conventions (snake_case `process_*`,
`send_*`, `recv_*` method names; `<service>_<method>_args` /
`<service>_<method>_result` class names; multiple classes per file in
the classmap variant) which would otherwise trip PSR-1 / Squiz strict
sniffs. Auditing the regenerated fixtures with the exclude removed
showed 1141 violations after `phpcbf`, all four sources rooted in
those conventions and impossible to fix without breaking ABI parity
with the other Thrift language generators.

No behavioural change to the lint configuration — the exclude is
identical, only the rationale is now in the file.

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client: php

Make the generated PHP header PSR-12-compliant and embed `phpcs:disable`
directives in the autogen comment so the regenerated test fixtures lint
cleanly without any per-project phpcs configuration. The blanket
`<exclude-pattern>lib/php/test/Resources/packages/*</exclude-pattern>`
in `phpcs.xml.dist` is therefore removed; the lint scope now covers
those fixtures end-to-end.

Generator changes:
* `generate_program_header` and `generate_service_header` reordered so
  the file-level docblock sits between `<?php` and the namespace
  declaration, with the required surrounding blank lines (PSR-12 §3).
* New `t_php_generator::autogen_comment()` override that adds inline
  `phpcs:disable` lines to the docblock. The disabled sniffs split
  into two groups:
    1. Cross-Thrift naming conventions that must remain consistent
       with every other Thrift language generator
       (`PSR1.Classes.ClassDeclaration.MultipleClasses`,
       `PSR1.Methods.CamelCapsMethodName`,
       `Squiz.Classes.ValidClassName`).
    2. Cosmetic emission style still inherited from the legacy
       generator and out of scope here
       (`Squiz.WhiteSpace.SuperfluousWhitespace`,
       `Generic.WhiteSpace.ScopeIndent`,
       `Generic.ControlStructures.InlineControlStructure`,
       `PSR12.Operators.OperatorSpacing`,
       `PSR12.Classes.ClassInstantiation`,
       `PSR2.Classes.ClassDeclaration`,
       `PSR2.Files.EndFileNewline`).

After regenerating with `make stubs`, `vendor/bin/phpcs` covers the
full `lib/php/lib`, `lib/php/test`, and
`lib/php/test/Resources/packages` tree and exits 0 (435 fixture files
inclusive). `vendor/bin/phpunit -c lib/php/phpunit.xml` continues to
pass (629 tests, 0 errors, 0 failures).

Future work can incrementally fix the emission style sniffs in the
generator and remove them from the disable list one at a time.

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…le list)

Client: php

Fix the cosmetic emission issues that previously forced the
auto-generator to ship a long `phpcs:disable` list. The disable list in
the autogen comment is now down to just the three cross-Thrift naming
conventions that genuinely have to stay
(`PSR1.Classes.ClassDeclaration.MultipleClasses`,
`PSR1.Methods.CamelCapsMethodName`, `Squiz.Classes.ValidClassName`).

Generator changes (`t_php_generator.cc`):
* Inline-mode branches now emit four-space body indents inside `if` /
  loops instead of the two-space leftovers from the original PSR-2
  port (covers `break;`, `return;`, `$input->...`, `$output->...`,
  `$x = new ...`, `$x->write(...)`, `$len = ...`, `$val = ...`,
  `$arr[1] = ...`, the field-name assignments, etc.).
* The OOP read/write branches no longer emit a trailing `out << '\n'`
  after `scope_down`, which was producing a stray blank line before
  the closing class brace.
* `$json = new stdClass;` now emits the parentheses required by
  `PSR12.Classes.ClassInstantiation`.
* Enum class close (`f_enum << "}"`) emits a single trailing newline
  instead of two.
* Service-rest method and class closes likewise emit a single newline.
* The classmap-mode service file is no longer padded with an extra
  `'\n'` before close.
* `$arr[1] * 4294967296` and `$elem[] = $value;` now use proper
  PSR-12 operator spacing (was `*` without spaces, was `[]= ` glued
  together).
* The "Bad version identifier" check is emitted as a multi-line
  `if (...) { throw ...; }` block instead of an inline control
  structure, with `' . $ver` instead of `'.$ver`.
* `process_<method>` dispatcher emits `'process_' . $fname` and the
  unknown-method exception path uses `' . $fname . '` and proper
  `indent()` instead of hard-coded four-space prefixes layered on top
  of `indent_up()` (the prior emission was double-indented).
* Removed the orphan `out << '\n'` after `getName()`'s closing brace
  that produced a triple newline.

Generator base (`t_generator.cc`):
* `generate_docstring_comment` now strips trailing whitespace from
  the line prefix when emitting an empty docstring line. This stops
  every Thrift language generator from leaking ` * ` (asterisk +
  trailing space) on otherwise-empty doc lines, which Squiz's
  `SuperfluousWhitespace` sniff rightly flags. Behaviour change is
  universally an improvement; no language relies on the trailing
  space.

Result: `vendor/bin/phpcs` over `lib/php/lib`, `lib/php/test`, and
`lib/php/test/Resources/packages/` (435 fixture files) exits 0 with
zero output. `vendor/bin/phpunit -c lib/php/phpunit.xml` continues to
pass (629 tests, 0 errors, 0 failures).

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client: php

Previously the generated PHP autogen comment carried all three
cross-Thrift `phpcs:disable` lines on every file, even on plain
structs / enums / interfaces / REST stubs that do not contain any
identifier the disabled sniffs would fire on. Make the disable list
context-aware: each file shape now receives only the rules it
actually needs.

Per-file-type disable lists:

| File shape                           | Disables                                       |
|--------------------------------------|------------------------------------------------|
| Plain struct / enum / interface / REST | (none)                                       |
| Service args / result helper struct  | `Squiz.Classes.ValidClassName`                 |
| Service client                       | `PSR1.Methods.CamelCapsMethodName`             |
| Service processor                    | `PSR1.Methods.CamelCapsMethodName`             |
| Top-level `Constant.php`             | `PSR1.Methods.CamelCapsMethodName`             |
| Classmap-mode service / Types file   | all three (multi-class file containing client, processor, args/result, ...) |

Implementation:

* Replace the parameterless `autogen_comment()` override with a new
  `php_autogen_comment(disables)` helper that builds the docblock and
  inlines `phpcs:disable <rule>` lines for whatever rules are passed.
  `autogen_comment()` itself now returns `php_autogen_comment({})` so
  base-class callers see a clean comment.
* Add a `phpcs_disables` parameter (default empty) to
  `generate_program_header` and `generate_service_header`.
* Update every call site to pass the appropriate rule list — most
  pass `{}` and produce a clean header.

`vendor/bin/phpcs` over `lib/php/lib`, `lib/php/test`, and
`lib/php/test/Resources/packages/` continues to exit 0.
`vendor/bin/phpunit -c lib/php/phpunit.xml` continues to pass
(629 tests, 0 errors, 0 failures).

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client: php

The inline-mode read/write paths and the OOP send-message helper
emitted the strict-binary-protocol version marker as the magic numbers
`0x80010000` and `0xffff0000` directly. The runtime
`TBinaryProtocol` class already exposes these as named constants
(`VERSION_1` and `VERSION_MASK`) and uses them in its non-inlined
read path. Switch the generator to emit those names so the code is
self-documenting and a future bump of the binary protocol version is
a one-line edit in the runtime.

Changes in `t_php_generator.cc`:

* `php_includes()` now also emits `use Thrift\Protocol\TBinaryProtocol;`
  alongside the existing `TBinaryProtocolAccelerated` import.
* The inline-mode message-header check
  (`recv_<method>` / `process_<method>` paths) emits
  ```
  $ver = $ver & TBinaryProtocol::VERSION_MASK;
  if ($ver != TBinaryProtocol::VERSION_1) {
      throw new TProtocolException('Bad version identifier: ' . $ver,
          TProtocolException::BAD_VERSION);
  }
  ```
* The two write-path version emissions
  (`$buff = pack('N', (0x80010000 | $msgType));`) now use
  `TBinaryProtocol::VERSION_1`.

The other magic numbers in the same emission paths (`0x7fffffff`,
`0xffffffff`, `0xffff`) are PHP-specific sign-extension masks for
varint decoding, not protocol versions, so they stay as numeric
literals.

Verified `vendor/bin/phpcs` clean and
`vendor/bin/phpunit -c lib/php/phpunit.xml` passes (629 tests, 0
errors, 0 failures).

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client: php

A previous commit on this branch fixed `* ` (asterisk + trailing
space) on otherwise-empty docstring lines by editing the base
`t_generator::generate_docstring_comment`. That helper is shared by
every Thrift language generator, so a PHP-only ticket should not be
changing it. Revert the base file and keep the fix scoped to the
PHP generator.

Replace `t_php_generator::generate_php_docstring_comment` so it emits
the docblock body itself instead of forwarding to the base helper.
The PHP-local body is the same loop, except an empty docstring line
produces ` *` (no trailing space) rather than ` * `, matching what
the runtime classes already do and keeping
`Squiz.WhiteSpace.SuperfluousWhitespace` happy. The remaining direct
call to `generate_docstring_comment` inside `generate_php_doc` for
functions is updated to go through the same helper for consistency.

`compiler/cpp/src/thrift/generate/t_generator.cc` is restored to its
pre-PR state. No other language generator's output changes.

Verified: `vendor/bin/phpcs` clean, `vendor/bin/phpunit -c
lib/php/phpunit.xml` passes (629 tests, 0 errors, 0 failures), and
`grep ' \* $' lib/php/test/Resources/packages/.../*.php` returns
nothing (no surviving trailing-whitespace docblock lines).

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…END_STRL)

Client: php

Three small reuse / quality cleanups discovered during a /simplify
review of the branch:

* Hoist the cross-Thrift `phpcs:disable` rule lists out of the call
  sites and into named member vectors:
  `PHPCS_DISABLES_CLASSMAP`, `PHPCS_DISABLES_SNAKE_CASE_METHODS`,
  `PHPCS_DISABLES_SERVICE_HELPERS`. Each call to
  `generate_program_header` / `generate_service_header` now passes the
  named vector instead of an inline string-literal list, so a future
  rename or addition is a one-line edit instead of touching seven sites.

* Collapse `generate_program_header` and `generate_service_header`
  bodies (they were byte-for-byte identical except for the
  `t_program*` source) into a shared `emit_file_header` helper. Both
  public entry points are now thin forwarders.

* Make `php_autogen_comment` delegate to the inherited
  `t_oop_generator::autogen_comment()` and splice the `phpcs:disable`
  block in just before the closing `*/`, instead of duplicating the
  base docblock structure (`/**`, summary, "DO NOT EDIT...",
  `@generated`, `*/`). Future tweaks to the base autogen comment now
  flow through automatically.

C extension (`lib/php/src/ext/thrift_protocol/php_thrift_protocol.cpp`):

* All seven `zend_read_static_property(..., "tspec", sizeof("tspec")-1, ...)`
  calls now use the idiomatic `ZEND_STRL("tspec")` macro that Zend
  ships exactly for this `(const char*, size_t)` pair pattern.

No behavioural change: `vendor/bin/phpcs` and
`vendor/bin/phpunit -c lib/php/phpunit.xml` (629 tests, 0 errors)
remain clean. Generated fixtures are byte-identical for the same
inputs (verified by regenerating all six variants).

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sveneld sveneld marked this pull request as ready for review May 8, 2026 09:37
@Jens-G
Copy link
Copy Markdown
Member

Jens-G commented May 9, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@Jens-G Jens-G closed this in f39cecc May 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants