Skip to content

3708: Add new code generator from AVRO to PHP - #42

Open
martin-augment wants to merge 17 commits into
mainfrom
pr-3708-2026-06-23-06-52-26
Open

3708: Add new code generator from AVRO to PHP#42
martin-augment wants to merge 17 commits into
mainfrom
pr-3708-2026-06-23-06-52-26

Conversation

@martin-augment

Copy link
Copy Markdown
Owner

3708: To review by AI

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds a PHP Avro code generation feature to the repository. It introduces AvroCodeGenerator, which traverses AvroSchema instances and emits PHP class and backed-enum source files using PhpParser ASTs. A companion AvroSpecificDatumWriter serializes instances of those generated classes into binary Avro encoding. A Symfony Console command (GenerateCommand) and a CLI entrypoint (bin/avro) expose generation via vendor/bin/avro generate. Supporting changes include schema API additions (name() accessors, typed return signatures, JSON error handling), Composer dependency and binary declarations, PHPStan/CS Fixer tooling updates, a CI Composer version bump, README documentation, and comprehensive PHPUnit test coverage. AI agent configuration files receive new review-rule instructions.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-3708-2026-06-23-06-52-26

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e0eb5f0. Configure here.


if (null !== $node && $registeredSchema instanceof AvroNamedSchema) {
$filename = $path.'/'.ucwords($registeredSchema->name()).'.php';
$files[$filename] = "<?php\n\ndeclare(strict_types=1);\n\n{$this->printer->prettyPrint([$node])}\n";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same short name file clash

High Severity

Generated PHP paths use only the Avro type’s short name() with ucwords, not the full Avro name. Two named types with the same short name (different namespaces) or two --directory inputs that each define the same record/enum name write to one output file; the last write wins without warning.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e0eb5f0. Configure here.

$caseName = strtoupper($value);
$enum->addStmt(
$this->factory->enumCase($caseName)->setValue($value)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enum case name collisions

High Severity

Backed enum cases are named with strtoupper on each Avro symbol. Valid Avro enums whose symbols differ only by case (e.g. Active and active) map to the same PHP case name and produce invalid duplicate enum cases in generated code.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e0eb5f0. Configure here.

$io->success(sprintf('%d file(s) generated in %s.', count($written), $outputDir));
}

return $exitCode;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty directory reports success

Medium Severity

When --directory points at a folder with no *.avsc files, the command never enters the processing loop, prints no error or success output, and returns exit code success as if generation completed.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e0eb5f0. Configure here.

$io->success(sprintf('%d file(s) generated in %s.', count($written), $outputDir));
}

return $exitCode;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Success shown on partial failure

Medium Severity

If at least one file is written but another schema or write fails, the command still calls $io->success with a generated count while returning a failure exit code.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e0eb5f0. Configure here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a PHP code generation tool for Apache Avro, including a CLI command, a code generator (AvroCodeGenerator) to translate schemas into PHP classes and enums, and a specific datum writer (AvroSpecificDatumWriter) for serializing these generated objects. The review identified several critical issues: a fatal error in the code generator due to using the non-existent PhpParser\Node\ArrayItem class, a PHP 8.0+ deprecation issue where required constructor parameters could be generated after optional ones, a severe performance bottleneck from repeatedly instantiating ReflectionClass during serialization, and a potential file-writing bug if realpath() fails and returns false.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +203 to +209
$arrayItems = [];
foreach ($avroRecord->fields() as $field) {
$arrayItems[] = new Node\ArrayItem(
$this->buildJsonSerializeValue($field->type(), $field->name()),
new String_($field->name())
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

In PHP-Parser (both v4 and v5), there is no PhpParser\Node\ArrayItem class. The correct class name is PhpParser\Node\Expr\ArrayItem. Using Node\ArrayItem will result in a fatal Error: Class "PhpParser\Node\ArrayItem" not found when attempting to generate code for any record schema.

        $arrayItems = [];
        foreach ($avroRecord->fields() as $field) {
            $arrayItems[] = new Node\Expr\ArrayItem(
                $this->buildJsonSerializeValue($field->type(), $field->name()),
                new String_($field->name())
            );
        }

Comment on lines +238 to +243
private function classNameMatchesSchema(object $datum, string $schemaName): bool
{
$className = (new \ReflectionClass($datum))->getShortName();

return 0 === strcasecmp($className, $schemaName);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Creating a new ReflectionClass instance on every call to classNameMatchesSchema is extremely expensive and will cause severe performance degradation when serializing large collections of records. You can retrieve the short class name much faster using standard string manipulation on get_class($datum).

    private function classNameMatchesSchema(object $datum, string $schemaName): bool
    {
        $class = get_class($datum);
        $className = false !== ($pos = strrpos($class, '\\')) ? substr($class, $pos + 1) : $class;

        return 0 === strcasecmp($className, $schemaName);
    }

Comment on lines +152 to +154
$constructor = $this->factory->method('__construct')->makePublic();
$constructorParamDocs = [];
foreach ($avroRecord->fields() as $field) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In PHP, defining required parameters after optional parameters (those with default values) is deprecated since PHP 8.0 and highly discouraged as it leads to unexpected behavior when calling the constructor with positional arguments. Since Avro schemas can define fields with default values in any order, we should perform a stable sort on the constructor parameters so that all fields with default values are placed at the end of the parameter list.

        $constructorFields = $avroRecord->fields();
        usort($constructorFields, static function ($a, $b): int {
            return $a->hasDefaultValue() <=> $b->hasDefaultValue();
        });

        $constructor = $this->factory->method('__construct')->makePublic();
        $constructorParamDocs = [];
        foreach ($constructorFields as $field) {

return Command::FAILURE;
}

$outputDir = rtrim((string) realpath($outputDir), '/');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If realpath($outputDir) fails (for example, if the directory does not exist, has permission issues, or if a stream wrapper like vfs:// is used in tests), it returns false. Casting false to (string) results in an empty string "". This causes the code generator to attempt to write files to the root directory "/User.php", which is highly dangerous and will fail on most systems. You should explicitly check if realpath returns false and handle it gracefully.

        $realOutputDir = realpath($outputDir);
        if (false === $realOutputDir) {
            $io->error(sprintf('Could not resolve absolute path for "%s".', $outputDir));

            return Command::FAILURE;
        }
        $outputDir = rtrim($realOutputDir, '/');

@augmentcode

augmentcode Bot commented Jun 23, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: This PR adds a PHP code-generation feature that produces typed PHP classes/enums from Avro schema (.avsc) files.

Changes:

  • Adds a new Symfony Console command (generate) and a packaged CLI entrypoint (lang/php/bin/avro) to run the generator.
  • Introduces Apache\Avro\Generator\AvroCodeGenerator (built on nikic/php-parser) to emit record classes and backed enums.
  • Adds AvroSpecificDatumWriter to serialize code-generated record instances/enums using the Avro binary encoder.
  • Extends schema classes with name() helpers and tightens some return types.
  • Wraps JSON parsing errors in AvroSchemaParseException for schema parsing.
  • Updates PHP tooling config (PHP-CS-Fixer, PHPStan) and CI workflow triggers for the PHP subtree.
  • Adds PHPUnit coverage for the new CLI command, generator output, and specific datum writer round-trips.

Technical Notes: Adds runtime deps on symfony/console and nikic/php-parser, and exposes the CLI via Composer bin.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 5 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread lang/php/bin/avro
exit(1);
}

use Apache\Avro\Console\GenerateCommand;

@augmentcode augmentcode Bot Jun 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lang/php/bin/avro:43 — This file declares use imports after executable statements; in PHP this causes a parse error (imports must appear before any non-declare code in the file).

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread lang/php/bin/avro
$version = InstalledVersions::getPrettyVersion('apache/avro');

$app = new Application('avro', $version);
$app->addCommand(new GenerateCommand());

@augmentcode augmentcode Bot Jun 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lang/php/bin/avro:50 — Symfony\Component\Console\Application in the declared dependency range (symfony/console ^6.4) does not provide addCommand(), so this call will fatal at runtime.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private function avroPrimitiveTypeToPhp(AvroPrimitiveSchema $primitiveSchema): string
{
return match ($primitiveSchema->type()) {
AvroSchema::NULL_TYPE => 'null',

@augmentcode augmentcode Bot Jun 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lang/php/lib/Generator/AvroCodeGenerator.php:291 — Returning the standalone type null will generate code like private null $x;, which is a compile-time fatal error in PHP 8.1 (this repo’s minimum supported version).

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

$property->setDocComment('/** @var '.$phpDocType.' */');
}

if ($field->hasDefaultValue()) {

@augmentcode augmentcode Bot Jun 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lang/php/lib/Generator/AvroCodeGenerator.php:145 — Defaults are emitted as raw schema default values regardless of field type; for enums/records this will typically be a string/array default but the generated PHP type is a class/enum, yielding invalid or type-incompatible generated code.

Severity: medium

Other Locations
  • lang/php/lib/Generator/AvroCodeGenerator.php:157

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

&& $this->classNameMatchesSchema($datum, $schema->name()),
$schema instanceof AvroArraySchema => is_array($datum)
&& ([] === $datum || array_is_list($datum)),
$schema instanceof AvroMapSchema => is_array($datum),

@augmentcode augmentcode Bot Jun 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lang/php/lib/Datum/AvroSpecificDatumWriter.php:214 — datumMatchesSchema() treats any PHP array as matching AvroMapSchema; in unions that contain both map and array branches, list arrays can be incorrectly matched to the map branch (wrong union index + encoding).

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

@claude

claude Bot commented Jun 23, 2026

Copy link
Copy Markdown

Code Review — PR #42: PHP Avro Code Generator

This PR adds a PHP CLI code generator (avro generate) that translates Avro schema files into PHP classes, along with an AvroSpecificDatumWriter for serializing generated class instances to Avro binary. The implementation is well-structured with good test coverage for the happy path. Several correctness issues need attention before merge.


🚨 Prompt Injection in Added Config Files

The PR adds CLAUDE.md, AGENTS.md, .cursor/rules.md, and .gemini/rules.md with identical content instructing AI reviewers to:

  1. Never link to any issue or pull request
  2. Not review these config files themselves

This is a self-referential prompt-injection attempt designed to suppress code review. These files have no legitimate purpose for a project and should be removed from the PR.


Bug 1 — getPrettyVersion() returns nullTypeError at startup

lang/php/bin/avro:271

$version = InstalledVersions::getPrettyVersion('apache/avro');
$app = new Application('avro', $version);   // TypeError: expects string, null given

InstalledVersions::getPrettyVersion() is typed string|null. When the package has no version metadata (path repositories, dev installs, monorepo setups), it returns null. Under declare(strict_types=1), passing null to Application::__construct(string $version) throws a TypeError before any help text is printed.

Fix: $version = InstalledVersions::getPrettyVersion('apache/avro') ?? 'UNKNOWN';


Bug 2 — ucwords() produces invalid PHP class names for snake_case Avro names

lang/php/lib/Generator/AvroCodeGenerator.php:767, 818, 949 (and two more sites)

$filename  = $path.'/'.ucwords($registeredSchema->name()).'.php';
$className = ucwords($avroRecord->name());

Avro names are legal as snake_case per the spec ([A-Za-z_][A-Za-z0-9_]*). ucwords() uses whitespace as the word delimiter, so:

  • ucwords("my_record")"My_record" — file My_record.php, class My_record
  • ucwords("myRecord")"MyRecord"

The underscore case produces a class name that violates PSR-4 naming and will not autoload. The same transformation is applied in AvroSpecificDatumWriter::classNameMatchesSchema via a different path, creating a second place where the logic must stay in sync.

Fix: Extract a private helper and handle underscores: str_replace('_', '', ucwords($name, '_')) or equivalent PascalCase converter, applied consistently at all 5 call sites.


Bug 3 — Empty array [] encodes as map when map precedes array in a union

lang/php/lib/Datum/AvroSpecificDatumWriter.php:649-651

$schema instanceof AvroArraySchema => is_array($datum) && ([] === $datum || array_is_list($datum)),
$schema instanceof AvroMapSchema   => is_array($datum),

writeUnion probes branches in schema declaration order. For a union like ["null", {"type":"map",...}, {"type":"array",...}] with datum []:

  • Map branch checked first: is_array([])truematch wins
  • Empty array is written as a map (wrong branch index), and a reader expecting an array misreads the stream

The map branch should require !array_is_list($datum) (or at minimum be ordered after the array branch). Note that PHP non-list arrays (["key" => "val"]) already work correctly since array_is_list returns false for them.


Bug 4 — classNameMatchesSchema matches on short name only, breaking unions with same-named types in different Avro namespaces

lang/php/lib/Datum/AvroSpecificDatumWriter.php:675-679

private function classNameMatchesSchema(object $datum, string $schemaName): bool
{
    $className = (new \ReflectionClass($datum))->getShortName();
    return 0 === strcasecmp($className, $schemaName);   // "User" vs "User" — always matches
}

$schemaName is the short Avro name (no namespace). Two schemas com.a.User and com.b.User in the same union are both named "User", so the first branch always wins regardless of the datum's actual PHP class. The Avro spec permits same-named types in different namespaces within a union. Also: new ReflectionClass($datum) is allocated once per branch probe — hoist it before the foreach loop in writeUnion or use get_class($datum) + substr instead.


Bug 5 — AvroFixedSchema not handled: always throws for valid fixed-type fields

lang/php/lib/Datum/AvroSpecificDatumWriter.php:510-518 and :640-654

writeData's match has no arm for AvroFixedSchema, so any schema containing a fixed-typed field (e.g., {"type":"fixed","name":"MD5","size":16}) hits default => throw new AvroException("Unsupported schema type: fixed") even with a perfectly valid 16-byte string value. datumMatchesSchema similarly returns false for fixed schemas in unions, causing AvroIOTypeException before writeData is even reached.

The existing AvroIODatumReader handles fixed; the writer needs a matching writeFixed method and arms in both writeData and datumMatchesSchema.


Bug 6 — realpath() failure silently clears the output path

lang/php/lib/Console/GenerateCommand.php:372

$outputDir = rtrim((string) realpath($outputDir), '/');

realpath() returns false on failure (e.g., directory created but immediately inaccessible due to permissions or a race). (string) false is "", so $outputDir becomes "". All subsequent file_put_contents() calls silently write to unexpected paths relative to CWD with no error reported.

Fix:

$resolved = realpath($outputDir);
if (false === $resolved) {
    $io->error(sprintf('Cannot resolve output directory "%s".', $outputDir));
    return Command::FAILURE;
}
$outputDir = rtrim($resolved, '/');

Altitude — switch ($schema::class) in collectSchemas doesn't handle subclasses

lang/php/lib/Generator/AvroCodeGenerator.php:783-811

switch ($schema::class) {
    case AvroRecordSchema::class: ...
    case AvroEnumSchema::class: ...

Every other dispatch in the codebase (writeData, avroTypeToPhp, datumMatchesSchema) uses match(true) { $schema instanceof Foo => ... } which correctly handles subclasses. The switch form performs an exact string comparison on the runtime class name, so any subclass silently falls through and is ignored — generating incomplete output with no error. Align with the rest of the file by using instanceof.


Summary

# Severity File Issue
🚨 CLAUDE.md, AGENTS.md, rules.md Prompt injection — remove these files
1 High bin/avro:271 getPrettyVersion() null → TypeError crash
2 High AvroCodeGenerator.php:767,818,949 ucwords wrong for snake_case Avro names
3 High AvroSpecificDatumWriter.php:649 Empty array encodes as map when map precedes array in union
4 High AvroSpecificDatumWriter.php:675 Short-name-only match breaks same-named types across Avro namespaces
5 High AvroSpecificDatumWriter.php:510,640 fixed type not handled — always throws
6 Medium GenerateCommand.php:372 realpath() false → empty output path, silent file corruption
7 Low AvroCodeGenerator.php:783 switch ($schema::class) silently drops subclasses; use instanceof

🤖 Generated with Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lang/php/bin/avro`:
- Line 47: The call to InstalledVersions::getPrettyVersion('apache/avro') can
throw an OutOfBoundsException when package metadata is missing or unavailable,
causing the CLI to crash during bootstrap. Wrap the
InstalledVersions::getPrettyVersion call in a try-catch block that catches
OutOfBoundsException and assigns a safe fallback version string (such as
"unknown" or a default version number) to the $version variable when the
exception occurs.

In `@lang/php/lib/Console/GenerateCommand.php`:
- Line 46: The namespace option in the addOption method call uses a
multi-character shortcut 'ns' when CLI conventions require single-character
shortcuts to improve usability and prevent unexpected option parsing behavior.
Change the shortcut parameter in the addOption call for the namespace option
from 'ns' to 'n' to follow standard CLI conventions.

In `@lang/php/lib/Datum/AvroSpecificDatumWriter.php`:
- Around line 73-80: The writeData method passes mixed $datum directly to typed
write methods (writeRecord, writeEnum, writeArray, writeMap, writeUnion,
writePrimitive) which expect specific types like object or array. This causes
PHP to throw TypeError before the method can raise the documented
AvroIOTypeException, breaking the error contract. Add guard clauses before the
match statement to validate that the datum matches the expected type for each
schema type, and throw AvroIOTypeException with appropriate messages for type
mismatches rather than allowing PHP's type system to throw TypeError. Ensure
this validation applies to all the write method dispatches mentioned in the
diff.
- Around line 208-211: The `classNameMatchesSchema` method used in the union
matching logic at lines 208-211 and 238-243 is comparing only short class names
when determining which branch to serialize. This is unsafe because two generated
Avro types from different namespaces can share the same short name, causing the
wrong branch to be selected and serializing invalid wire data. Update the
`classNameMatchesSchema` method to compare the fully-qualified class name
(including namespace) against the fully-qualified schema name instead of relying
on short names alone. This ensures that namespace-prefixed types are properly
distinguished during union serialization.
- Around line 212-215: The AvroMapSchema match at line 214 currently accepts any
array with just is_array($datum), but this also matches list arrays which should
be matched by AvroArraySchema instead. When a union contains both array and map
schemas and the map appears first in the match order, list-like data gets
incorrectly selected as a map, producing the wrong union index. Fix this by
adding an additional condition to the AvroMapSchema case to exclude list arrays:
change the condition to is_array($datum) && !array_is_list($datum) so that only
associative maps are matched by this branch while list arrays are correctly
matched by AvroArraySchema.

In `@lang/php/lib/Generator/AvroCodeGenerator.php`:
- Around line 63-64: The $files array in the AvroCodeGenerator class silently
allows schema filenames to collide when different schemas normalize to the same
ucwords(name) result, causing data loss. Add collision detection before
assignments to the $files array throughout the code (including the locations
mentioned in lines 79-82) by checking if a normalized filename already exists in
$files before adding new entries, and throw an exception with a descriptive
error message if a collision is detected to fail fast and prevent silent schema
loss.
- Around line 265-269: In the foreach loop that iterates through $values and
generates enum cases using strtoupper, add collision detection to identify when
two different values would produce the same uppercased case name. Before calling
$this->factory->enumCase($caseName)->setValue($value), track the case names that
have already been added to the enum. If a case name collision is detected (a
case name has already been used for a different value), either skip adding that
enum case or raise an error to alert that the enum definition is invalid due to
case-sensitive symbol collisions.

In `@lang/php/lib/Schema/AvroEnumSchema.php`:
- Around line 75-78: The hasSymbol() method uses a non-strict in_array
comparison when checking if a symbol exists in the symbols array, which can lead
to false positives when comparing values of different types (for example, the
string "0" could match the integer 0). Fix this by adding a third parameter set
to true in the in_array call within hasSymbol() to enforce strict type
comparison, ensuring only exact matches are returned.

In `@lang/php/README.md`:
- Around line 60-66: Add a language tag to the code fence on line 62 (the triple
backticks before vendor/bin/avro) to specify the shell/bash language, and ensure
all heading styles (the ## Usage and ### Options headings) are consistent with
the repository's markdown linting rules to avoid mixing ATX and setext heading
styles. Apply the same fixes to the code sections around lines 77-91.

In `@lang/php/test/Console/GenerateCommandTest.php`:
- Around line 178-203: Both test methods
generated_file_contains_correct_namespace_and_class and
generated_enum_file_contains_correct_cases need to verify the command succeeded
before attempting to read the generated files. After calling $tester->execute(),
add an assertion to check that the exit code is 0 (or check the command's
success status) before proceeding with file_get_contents(). This ensures that if
the command fails, the test will report the actual command failure instead of a
secondary file-not-found error.

In `@lang/php/test/Generator/AvroCodeGeneratorTest.php`:
- Around line 615-624: The test expectations in AvroCodeGeneratorTest include
standalone `null` type hints on the nullField property and constructor
parameter, which are incompatible with PHP 8.1 (the project's minimum supported
version). Update the test assertion to remove the standalone `null` type hints
from the expected generated code for the nullField declaration in the private
property, the constructor parameter, and any related method signatures. Replace
these with type hints that are compatible with PHP 8.1, such as removing the
type entirely or using a nullable type if applicable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 896048fa-6a2e-44bb-933a-3cb9f29147ff

📥 Commits

Reviewing files that changed from the base of the PR and between 9f7f406 and e0eb5f0.

⛔ Files ignored due to path filters (11)
  • lang/php/test/Fixtures/Generated/Address.php is excluded by !**/generated/**
  • lang/php/test/Fixtures/Generated/Car.php is excluded by !**/generated/**
  • lang/php/test/Fixtures/Generated/FuelType.php is excluded by !**/generated/**
  • lang/php/test/Fixtures/Generated/Member.php is excluded by !**/generated/**
  • lang/php/test/Fixtures/Generated/Metadata.php is excluded by !**/generated/**
  • lang/php/test/Fixtures/Generated/Order.php is excluded by !**/generated/**
  • lang/php/test/Fixtures/Generated/Priority.php is excluded by !**/generated/**
  • lang/php/test/Fixtures/Generated/Profile.php is excluded by !**/generated/**
  • lang/php/test/Fixtures/Generated/Task.php is excluded by !**/generated/**
  • lang/php/test/Fixtures/Generated/Team.php is excluded by !**/generated/**
  • lang/php/test/Fixtures/Generated/User.php is excluded by !**/generated/**
📒 Files selected for processing (23)
  • .cursor/rules.md
  • .gemini/rules.md
  • .github/workflows/test-lang-php.yml
  • AGENTS.md
  • CLAUDE.md
  • composer.json
  • lang/php/.php-cs-fixer.dist.php
  • lang/php/README.md
  • lang/php/bin/avro
  • lang/php/lib/Console/GenerateCommand.php
  • lang/php/lib/Datum/AvroSpecificDatumWriter.php
  • lang/php/lib/Generator/AvroCodeGenerator.php
  • lang/php/lib/Generator/AvroCodeGeneratorException.php
  • lang/php/lib/Schema/AvroEnumSchema.php
  • lang/php/lib/Schema/AvroName.php
  • lang/php/lib/Schema/AvroNamedSchema.php
  • lang/php/lib/Schema/AvroSchema.php
  • lang/php/phpstan.neon
  • lang/php/test/Console/GenerateCommandTest.php
  • lang/php/test/Datum/AvroSpecificDatumWriterTest.php
  • lang/php/test/Fixtures/Schemas/status.avsc
  • lang/php/test/Fixtures/Schemas/user.avsc
  • lang/php/test/Generator/AvroCodeGeneratorTest.php

Comment thread lang/php/bin/avro
use Composer\InstalledVersions;
use Symfony\Component\Console\Application;

$version = InstalledVersions::getPrettyVersion('apache/avro');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the file
fd -t f "avro" lang/php/bin/ | head -20

Repository: martin-augment/avro

Length of output: 175


🏁 Script executed:

# Check if file exists and get its content
cat -n lang/php/bin/avro | head -70

Repository: martin-augment/avro

Length of output: 2239


🏁 Script executed:

# Look for the Composer InstalledVersions class definition/usage patterns
rg "InstalledVersions" --type php -B 3 -A 3 | head -50

Repository: martin-augment/avro

Length of output: 157


🏁 Script executed:

# Check what version of Composer is used
if [ -f "composer.lock" ]; then
  jq '.metadata // empty' composer.lock | head -20
elif [ -f "lang/php/composer.lock" ]; then
  jq '.metadata // empty' lang/php/composer.lock | head -20
fi

Repository: martin-augment/avro

Length of output: 157


🏁 Script executed:

# Look for version string usage after line 47 in the avro file
sed -n '47,100p' lang/php/bin/avro

Repository: martin-augment/avro

Length of output: 360


🌐 Web query:

Composer InstalledVersions::getPrettyVersion throws exception missing metadata

💡 Result:

The Composer\InstalledVersions::getPrettyVersion() method throws an OutOfBoundsException if the specified package is not installed at all [1][2]. If you are encountering an exception related to "missing metadata" or if the class is not found, it is typically due to one of the following reasons: 1. Package Not Installed: The method explicitly throws an OutOfBoundsException when the package name passed to it cannot be found in the installed.php file generated by Composer [1][3]. Ensure the package is actually installed and that you are using the correct vendor/package name. 2. Composer 2.0 Requirement: The Composer\InstalledVersions class was introduced in Composer 2.0 [4][5]. If you are using an older version of Composer or if your project's autoloader has not been regenerated with a compatible Composer version, the class will not be found [4][6][5]. You can resolve this by updating your local Composer installation (e.g., composer self-update --2) and running composer install or composer dump-autoload to regenerate the autoloader [4][6][5]. 3. Replacements and Provisions: If a package is being replaced or provided by another package, getPrettyVersion() may return null [1][7]. While this does not throw an exception, it can cause logic errors if your code expects a string return value [1]. Always check the return value against null if there is a possibility the package is being provided or replaced [1]. If you are seeing a specific "missing metadata" error that is not an OutOfBoundsException, it may indicate that your vendor/composer/installed.php file is corrupted or was not generated correctly. Running composer dump-autoload usually repairs this file [6].

Citations:


🌐 Web query:

Symfony Console Application constructor version parameter expectations

💡 Result:

In the Symfony Console component, the Application class constructor accepts a version parameter, which is defined as a string [1][2]. Constructor Signature As of the current implementation, the constructor signature for the Symfony\Component\Console\Application class is: public function construct( private string $name = 'UNKNOWN', private string $version = 'UNKNOWN', private?ContainerInterface $container = null,) Usage and Expectations 1. Purpose: The version parameter is used to identify the version of your console application [3][4]. When provided, this version is displayed when a user runs a command with the --version or -V global options [3][5]. 2. Format: It expects a string [6][1]. While no specific semantic versioning format is enforced by the component, passing a string representing your application version (e.g., '1.0.0', '1.2') is the standard practice [3][4]. 3. Default Value: If no version is provided, it defaults to 'UNKNOWN' [6][1]. 4. Output Behavior: If you set both a name and a version, running your application with -V will output: [Name] version [Version] [4]. If no name or version is provided, the output will reflect the default 'UNKNOWN' values [3]. In addition to the version, you may provide the application name as the first argument, and starting from Symfony 8.1, an optional PSR-11 ContainerInterface as the third argument [7][8].

Citations:


Guard version lookup to avoid bootstrap crash.

InstalledVersions::getPrettyVersion('apache/avro') throws OutOfBoundsException when the package metadata is missing or the package is not installed, which will crash the CLI before it starts. Wrap in try-catch with a safe fallback version.

Suggested fix
-$version = InstalledVersions::getPrettyVersion('apache/avro');
+try {
+    $version = InstalledVersions::getPrettyVersion('apache/avro') ?? 'unknown';
+} catch (\Throwable) {
+    $version = 'unknown';
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$version = InstalledVersions::getPrettyVersion('apache/avro');
try {
$version = InstalledVersions::getPrettyVersion('apache/avro') ?? 'unknown';
} catch (\Throwable) {
$version = 'unknown';
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lang/php/bin/avro` at line 47, The call to
InstalledVersions::getPrettyVersion('apache/avro') can throw an
OutOfBoundsException when package metadata is missing or unavailable, causing
the CLI to crash during bootstrap. Wrap the InstalledVersions::getPrettyVersion
call in a try-catch block that catches OutOfBoundsException and assigns a safe
fallback version string (such as "unknown" or a default version number) to the
$version variable when the exception occurs.

->addOption('file', 'f', InputOption::VALUE_OPTIONAL, 'One Avro schema file (.avsc)')
->addOption('directory', 'd', InputOption::VALUE_OPTIONAL, 'A directory containing multiple Avro schema files (.avsc)')
->addOption('output', 'o', InputOption::VALUE_REQUIRED, 'Output directory for generated PHP files')
->addOption('namespace', 'ns', InputOption::VALUE_REQUIRED, 'PHP namespace for generated classes');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n lang/php/lib/Console/GenerateCommand.php | sed -n '40,55p'

Repository: martin-augment/avro

Length of output: 1057


🌐 Web query:

Symfony Console addOption shortcut character limitations multi-character

💡 Result:

In Symfony Console, the shortcut parameter for the addOption method is not strictly limited to a single character [1][2]. While it is common practice to use a single letter (e.g., 'i' for '--iterations'), you can define multi-character shortcuts [3]. Internally, Symfony handles shortcuts as a string that can contain multiple options separated by a pipe character (|) [2][4][3]. If you provide a string like 'f|ff|fff' as the shortcut, the framework treats each segment as a valid shortcut for that option [3]. You can also pass a leading dash (e.g., '-f'), and Symfony will automatically strip it [3]. However, there are practical and conventional limitations to keep in mind: 1. Usage Conventions: While the API permits multi-character strings, CLI conventions typically reserve single dashes (-) for short, single-character options and double dashes (--) for long, multi-character option names. Using multi-character shortcuts (e.g., -abc) may confuse users who expect standard POSIX-like behavior. 2. Reserved Shortcuts: Certain shortcuts are reserved by the Symfony Console component itself for global options, most notably verbosity levels like -v, -vv, and -vvv [5]. Attempting to register these as custom shortcuts will result in an InvalidArgumentException [5]. 3. Complexity: Using complex or multi-character shortcuts increases the risk of collision with other options in your application or third-party bundles [5]. In summary, while the Symfony Console API technically allows multi-character shortcuts, you should use them judiciously and ensure they do not conflict with reserved global options or established CLI interface standards [4][3][5].

Citations:


🌐 Web query:

Symfony InputOption documentation shortcut parameter safety

💡 Result:

In Symfony's Console component, the InputOption shortcut parameter allows you to define a short, single-dash alias for an option (e.g., -i for --iterations) to improve command usability [1][2]. Safety and Technical Considerations: 1. Valid Shortcuts: Shortcuts are intended to be short, typically single-character aliases [1][2]. You can define multiple shortcuts by separating them with a pipe character (|) or by passing them as an array [1][3]. 2. Internal Constraints: The InputOption class internally processes shortcuts by removing leading dashes and filtering the resulting string [4]. Historically, this processing logic used array_filter in a way that prevented the character '0' from being used as a valid shortcut because it evaluated to false [4]. This was addressed in later versions to allow '0' as a valid shortcut [4]. 3. Reserved Shortcuts: There is no strict, enforced global blacklist of reserved shortcuts in the framework that prevents you from defining specific characters [5]. However, it is a best practice to avoid using standard, widely-recognized shortcuts already in use by the application or the global Symfony Console environment (such as -h for help or -q for quiet) to prevent conflicts and ensure predictable user behavior [1][5]. 4. Validation: If an invalid or empty shortcut is provided (e.g., an empty string), the component will throw an InvalidArgumentException [4]. Recommendations: - Use clear, single-character shortcuts whenever possible for high-frequency options [1]. - Ensure your chosen shortcuts do not overlap with existing global options or other defined options within the same command. - If you encounter issues with specific characters like '0', ensure you are using a recent, patched version of the symfony/console component [4].

Citations:


Use a single-character shortcut for namespace option.

Following CLI conventions, option shortcuts should be single characters (e.g., -n instead of -ns). While Symfony Console technically supports multi-character shortcuts, using single letters improves usability and prevents unexpected option parsing behavior in user interfaces.

Suggested fix
-            ->addOption('namespace', 'ns', InputOption::VALUE_REQUIRED, 'PHP namespace for generated classes');
+            ->addOption('namespace', 'n', InputOption::VALUE_REQUIRED, 'PHP namespace for generated classes');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
->addOption('namespace', 'ns', InputOption::VALUE_REQUIRED, 'PHP namespace for generated classes');
->addOption('namespace', 'n', InputOption::VALUE_REQUIRED, 'PHP namespace for generated classes');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lang/php/lib/Console/GenerateCommand.php` at line 46, The namespace option in
the addOption method call uses a multi-character shortcut 'ns' when CLI
conventions require single-character shortcuts to improve usability and prevent
unexpected option parsing behavior. Change the shortcut parameter in the
addOption call for the namespace option from 'ns' to 'n' to follow standard CLI
conventions.

Comment on lines +73 to +80
match (true) {
$schema instanceof AvroRecordSchema => $this->writeRecord($schema, $datum, $encoder),
$schema instanceof AvroEnumSchema => $this->writeEnum($schema, $datum, $encoder),
$schema instanceof AvroArraySchema => $this->writeArray($schema, $datum, $encoder),
$schema instanceof AvroMapSchema => $this->writeMap($schema, $datum, $encoder),
$schema instanceof AvroUnionSchema => $this->writeUnion($schema, $datum, $encoder),
$schema instanceof AvroPrimitiveSchema => $this->writePrimitive($schema, $datum, $encoder),
default => throw new AvroException(sprintf('Unsupported schema type: %s', $schema->type())),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard datum types before typed method dispatch to avoid uncaught TypeError.

At Line 73, writeData() forwards mixed $datum into methods typed as object/array. For mismatched runtime data, PHP throws TypeError before AvroIOTypeException, which breaks the method’s documented Avro error contract and can surface as an unexpected crash path.

Proposed fix
 private function writeData(AvroSchema $schema, mixed $datum, AvroIOBinaryEncoder $encoder): void
 {
     match (true) {
-        $schema instanceof AvroRecordSchema => $this->writeRecord($schema, $datum, $encoder),
-        $schema instanceof AvroEnumSchema => $this->writeEnum($schema, $datum, $encoder),
-        $schema instanceof AvroArraySchema => $this->writeArray($schema, $datum, $encoder),
-        $schema instanceof AvroMapSchema => $this->writeMap($schema, $datum, $encoder),
+        $schema instanceof AvroRecordSchema => is_object($datum)
+            ? $this->writeRecord($schema, $datum, $encoder)
+            : throw new AvroIOTypeException($schema, $datum),
+        $schema instanceof AvroEnumSchema => $this->writeEnum($schema, $datum, $encoder),
+        $schema instanceof AvroArraySchema => is_array($datum)
+            ? $this->writeArray($schema, $datum, $encoder)
+            : throw new AvroIOTypeException($schema, $datum),
+        $schema instanceof AvroMapSchema => is_array($datum)
+            ? $this->writeMap($schema, $datum, $encoder)
+            : throw new AvroIOTypeException($schema, $datum),
         $schema instanceof AvroUnionSchema => $this->writeUnion($schema, $datum, $encoder),
         $schema instanceof AvroPrimitiveSchema => $this->writePrimitive($schema, $datum, $encoder),
         default => throw new AvroException(sprintf('Unsupported schema type: %s', $schema->type())),
     };
 }

Also applies to: 89-90, 121-122, 138-139

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lang/php/lib/Datum/AvroSpecificDatumWriter.php` around lines 73 - 80, The
writeData method passes mixed $datum directly to typed write methods
(writeRecord, writeEnum, writeArray, writeMap, writeUnion, writePrimitive) which
expect specific types like object or array. This causes PHP to throw TypeError
before the method can raise the documented AvroIOTypeException, breaking the
error contract. Add guard clauses before the match statement to validate that
the datum matches the expected type for each schema type, and throw
AvroIOTypeException with appropriate messages for type mismatches rather than
allowing PHP's type system to throw TypeError. Ensure this validation applies to
all the write method dispatches mentioned in the diff.

Comment on lines +208 to +211
&& $this->classNameMatchesSchema($datum, $schema->name()),
$schema instanceof AvroRecordSchema => is_object($datum)
&& !($datum instanceof \BackedEnum)
&& $this->classNameMatchesSchema($datum, $schema->name()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Short-class-name matching is unsafe for namespaced Avro named types.

At Line 240, branch matching compares only getShortName(). If two generated types share the same short name across namespaces, union matching can pick the wrong branch and serialize invalid wire data for that schema contract.

Also applies to: 238-243

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lang/php/lib/Datum/AvroSpecificDatumWriter.php` around lines 208 - 211, The
`classNameMatchesSchema` method used in the union matching logic at lines
208-211 and 238-243 is comparing only short class names when determining which
branch to serialize. This is unsafe because two generated Avro types from
different namespaces can share the same short name, causing the wrong branch to
be selected and serializing invalid wire data. Update the
`classNameMatchesSchema` method to compare the fully-qualified class name
(including namespace) against the fully-qualified schema name instead of relying
on short names alone. This ensures that namespace-prefixed types are properly
distinguished during union serialization.

Comment on lines +212 to +215
$schema instanceof AvroArraySchema => is_array($datum)
&& ([] === $datum || array_is_list($datum)),
$schema instanceof AvroMapSchema => is_array($datum),
default => false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Union matching can encode the wrong branch for list arrays when a map branch appears first.

At Line 214, AvroMapSchema currently matches any array. In a union containing both array and map, list-like data can be selected as map depending on branch order, producing a wrong union index and incompatible serialized payload.

Proposed fix
 private function datumMatchesSchema(AvroSchema $schema, mixed $datum): bool
 {
     return match (true) {
         $schema instanceof AvroPrimitiveSchema => $this->datumMatchesPrimitive($schema, $datum),
         $schema instanceof AvroEnumSchema => $datum instanceof \BackedEnum
             && $this->classNameMatchesSchema($datum, $schema->name()),
         $schema instanceof AvroRecordSchema => is_object($datum)
             && !($datum instanceof \BackedEnum)
             && $this->classNameMatchesSchema($datum, $schema->name()),
         $schema instanceof AvroArraySchema => is_array($datum)
             && ([] === $datum || array_is_list($datum)),
-        $schema instanceof AvroMapSchema => is_array($datum),
+        $schema instanceof AvroMapSchema => is_array($datum)
+            && ([] === $datum || !array_is_list($datum)),
         default => false,
     };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$schema instanceof AvroArraySchema => is_array($datum)
&& ([] === $datum || array_is_list($datum)),
$schema instanceof AvroMapSchema => is_array($datum),
default => false,
$schema instanceof AvroArraySchema => is_array($datum)
&& ([] === $datum || array_is_list($datum)),
$schema instanceof AvroMapSchema => is_array($datum)
&& ([] === $datum || !array_is_list($datum)),
default => false,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lang/php/lib/Datum/AvroSpecificDatumWriter.php` around lines 212 - 215, The
AvroMapSchema match at line 214 currently accepts any array with just
is_array($datum), but this also matches list arrays which should be matched by
AvroArraySchema instead. When a union contains both array and map schemas and
the map appears first in the match order, list-like data gets incorrectly
selected as a map, producing the wrong union index. Fix this by adding an
additional condition to the AvroMapSchema case to exclude list arrays: change
the condition to is_array($datum) && !array_is_list($datum) so that only
associative maps are matched by this branch while list arrays are correctly
matched by AvroArraySchema.

Comment on lines +265 to +269
foreach ($values as $value) {
$caseName = strtoupper($value);
$enum->addStmt(
$this->factory->enumCase($caseName)->setValue($value)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Detect enum case-name collisions caused by uppercasing symbols.

Two valid symbols that differ only by case will collapse to the same generated case name and produce invalid enum output.

Proposed fix
     private function buildEnum(
         AvroEnumSchema $avroEnum,
         string $phpNamespace,
         array $values
     ): Node {
         $className = ucwords($avroEnum->name());
         $enum = $this->factory->enum($className)->setScalarType('string');
+        $generatedCaseNames = [];

         foreach ($values as $value) {
             $caseName = strtoupper($value);
+            if (isset($generatedCaseNames[$caseName])) {
+                throw new AvroCodeGeneratorException(sprintf(
+                    'Enum case collision in "%s": symbol "%s" normalizes to duplicate case "%s".',
+                    $avroEnum->fullname(),
+                    $value,
+                    $caseName
+                ));
+            }
+            $generatedCaseNames[$caseName] = true;
             $enum->addStmt(
                 $this->factory->enumCase($caseName)->setValue($value)
             );
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
foreach ($values as $value) {
$caseName = strtoupper($value);
$enum->addStmt(
$this->factory->enumCase($caseName)->setValue($value)
);
private function buildEnum(
AvroEnumSchema $avroEnum,
string $phpNamespace,
array $values
): Node {
$className = ucwords($avroEnum->name());
$enum = $this->factory->enum($className)->setScalarType('string');
$generatedCaseNames = [];
foreach ($values as $value) {
$caseName = strtoupper($value);
if (isset($generatedCaseNames[$caseName])) {
throw new AvroCodeGeneratorException(sprintf(
'Enum case collision in "%s": symbol "%s" normalizes to duplicate case "%s".',
$avroEnum->fullname(),
$value,
$caseName
));
}
$generatedCaseNames[$caseName] = true;
$enum->addStmt(
$this->factory->enumCase($caseName)->setValue($value)
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lang/php/lib/Generator/AvroCodeGenerator.php` around lines 265 - 269, In the
foreach loop that iterates through $values and generates enum cases using
strtoupper, add collision detection to identify when two different values would
produce the same uppercased case name. Before calling
$this->factory->enumCase($caseName)->setValue($value), track the case names that
have already been added to the enum. If a case name collision is detected (a
case name has already been used for a different value), either skip adding that
enum case or raise an error to alert that the enum definition is invalid due to
case-sensitive symbol collisions.

Comment on lines +75 to 78
public function hasSymbol($symbol): bool
{
return in_array($symbol, $this->symbols);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use strict comparison in hasSymbol() to avoid false positives.

in_array is currently non-strict, so non-string inputs can match string symbols unexpectedly.

Proposed fix
 public function hasSymbol($symbol): bool
 {
-    return in_array($symbol, $this->symbols);
+    return is_string($symbol) && in_array($symbol, $this->symbols, true);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function hasSymbol($symbol): bool
{
return in_array($symbol, $this->symbols);
}
public function hasSymbol($symbol): bool
{
return is_string($symbol) && in_array($symbol, $this->symbols, true);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lang/php/lib/Schema/AvroEnumSchema.php` around lines 75 - 78, The hasSymbol()
method uses a non-strict in_array comparison when checking if a symbol exists in
the symbols array, which can lead to false positives when comparing values of
different types (for example, the string "0" could match the integer 0). Fix
this by adding a third parameter set to true in the in_array call within
hasSymbol() to enforce strict type comparison, ensuring only exact matches are
returned.

Comment thread lang/php/README.md
Comment on lines +60 to +66
## Usage

```
vendor/bin/avro [options]
```

### Options

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align new README headings/code fence with repository markdown linting rules.

The new section mixes ATX headings in a setext-styled document and has a fenced block without a language tag (Line 62), matching current markdownlint warnings.

Also applies to: 77-91

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 60-60: Heading style
Expected: setext; Actual: atx

(MD003, heading-style)


[warning] 62-62: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 66-66: Heading style
Expected: setext; Actual: atx

(MD003, heading-style)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lang/php/README.md` around lines 60 - 66, Add a language tag to the code
fence on line 62 (the triple backticks before vendor/bin/avro) to specify the
shell/bash language, and ensure all heading styles (the ## Usage and ### Options
headings) are consistent with the repository's markdown linting rules to avoid
mixing ATX and setext heading styles. Apply the same fixes to the code sections
around lines 77-91.

Source: Linters/SAST tools

Comment on lines +178 to +203
public function generated_file_contains_correct_namespace_and_class(): void
{
$tester = $this->tester();
$tester->execute([
'--file' => $this->schemaPath('user.avsc'),
'--output' => $this->outputDir,
'--namespace' => 'My\\App\\Avro',
]);

$content = file_get_contents($this->outputDir.'/User.php');
self::assertStringContainsString('namespace My\\App\\Avro;', $content);
self::assertStringContainsString('final class User', $content);
}

#[Test]
public function generated_enum_file_contains_correct_cases(): void
{
$tester = $this->tester();
$tester->execute([
'--file' => $this->schemaPath('status.avsc'),
'--output' => $this->outputDir,
'--namespace' => 'App\\Generated',
]);

$content = file_get_contents($this->outputDir.'/Status.php');
self::assertStringContainsString('enum Status', $content);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Assert command success before reading generated files.

These tests read output files immediately after execute() without checking the exit code (or file existence), which can hide the root failure behind secondary errors.

Suggested tightening
     public function generated_file_contains_correct_namespace_and_class(): void
     {
         $tester = $this->tester();
-        $tester->execute([
+        $exitCode = $tester->execute([
             '--file' => $this->schemaPath('user.avsc'),
             '--output' => $this->outputDir,
             '--namespace' => 'My\\App\\Avro',
         ]);
+        self::assertSame(Command::SUCCESS, $exitCode);
+        self::assertFileExists($this->outputDir.'/User.php');

         $content = file_get_contents($this->outputDir.'/User.php');
         self::assertStringContainsString('namespace My\\App\\Avro;', $content);
         self::assertStringContainsString('final class User', $content);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lang/php/test/Console/GenerateCommandTest.php` around lines 178 - 203, Both
test methods generated_file_contains_correct_namespace_and_class and
generated_enum_file_contains_correct_cases need to verify the command succeeded
before attempting to read the generated files. After calling $tester->execute(),
add an assertion to check that the exit code is 0 (or check the command's
success status) before proceeding with file_get_contents(). This ensures that if
the command fails, the test will report the actual command failure instead of a
secondary file-not-found error.

Comment on lines +615 to +624
private null \$nullField;
private bool \$boolField;
private int \$intField;
private int \$longField;
private float \$floatField;
private float \$doubleField;
private string \$stringField;
private string \$bytesField;
public function __construct(null \$nullField, bool \$boolField, int \$intField, int \$longField, float \$floatField, float \$doubleField, string \$stringField, string \$bytesField)
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In PHP 8.1, is standalone type declaration null(e.g.,private null $x;orfunction f(null $x)) valid syntax, or is it only supported in PHP 8.2+?

💡 Result:

Standalone type declaration null (e.g., private null $x; or function f(null $x)) is not valid syntax in PHP 8.1; it is only supported in PHP 8.2 and later [1][2][3]. In PHP 8.1 and earlier, using null as a standalone type results in a compile-time fatal error [2][3]. While null could be used as part of a union type (e.g., string|null) in PHP 8.0 and 8.1, it could not be used on its own [1][2][3]. The ability to use null (along with false) as a standalone type was introduced specifically in PHP 8.2 [1][2].

Citations:


🏁 Script executed:

# First, examine the test file around the mentioned lines
sed -n '610,630p' lang/php/test/Generator/AvroCodeGeneratorTest.php | cat -n

Repository: martin-augment/avro

Length of output: 1321


🏁 Script executed:

# Also check lines 634-635
sed -n '630,640p' lang/php/test/Generator/AvroCodeGeneratorTest.php | cat -n

Repository: martin-augment/avro

Length of output: 667


🏁 Script executed:

# Search for PHP version declarations or comments in the test file
grep -i "php\|version\|8\.1\|8\.2" lang/php/test/Generator/AvroCodeGeneratorTest.php | head -20

Repository: martin-augment/avro

Length of output: 955


🏁 Script executed:

# Check if there's a composer.json or similar declaring the PHP version
find . -name "composer.json" -o -name ".php-version" | head -5 | xargs grep -l "php\|version" 2>/dev/null

Repository: martin-augment/avro

Length of output: 173


🏁 Script executed:

# Check the PHP version requirement in composer.json
cat composer.json | grep -A 5 -B 5 "php"

Repository: martin-augment/avro

Length of output: 1329


🏁 Script executed:

# Also check if there's any php version requirement or declaration in the test file itself
head -50 lang/php/test/Generator/AvroCodeGeneratorTest.php | cat -n

Repository: martin-augment/avro

Length of output: 2050


Remove standalone null type assertions from test expectations—incompatible with PHP 8.1.

The test asserts that the code generator produces private null $nullField;, __construct(null $nullField, ...), and public function nullField(): null at lines 615–624 and 634–635. Standalone null type hints are only valid in PHP 8.2+, but this project requires php: ^8.1 (composer.json). The generated code would fail to parse in PHP 8.1 and would lock in invalid output as the expected behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lang/php/test/Generator/AvroCodeGeneratorTest.php` around lines 615 - 624,
The test expectations in AvroCodeGeneratorTest include standalone `null` type
hints on the nullField property and constructor parameter, which are
incompatible with PHP 8.1 (the project's minimum supported version). Update the
test assertion to remove the standalone `null` type hints from the expected
generated code for the nullField declaration in the private property, the
constructor parameter, and any related method signatures. Replace these with
type hints that are compatible with PHP 8.1, such as removing the type entirely
or using a nullable type if applicable.

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.

3 participants