This is an automated email from the ASF dual-hosted git repository.

mgrigorov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro.git


The following commit(s) were added to refs/heads/main by this push:
     new 3af7a4654d AVRO-4226: Missing doc attributes for record fields (#3624)
3af7a4654d is described below

commit 3af7a4654d574be7c78125e610a7d4ca23542eab
Author: Mattia Basone <[email protected]>
AuthorDate: Mon Jan 19 07:21:32 2026 +0100

    AVRO-4226: Missing doc attributes for record fields (#3624)
    
    * bump composer version
    
    * add missing doc attribute to AvroField
    
    * review + code improvements
    
    * remove AvroNamedSchemata passed by reference
    
    * SchemaTest improvements
    
    * apply Copilot/Martin suggestions
    
    * Update lang/php/lib/Schema/AvroArraySchema.php
    
    Co-authored-by: Martin Grigorov <[email protected]>
    
    ---------
    
    Co-authored-by: Martin Grigorov <[email protected]>
---
 .github/workflows/test-lang-php.yml       |    4 +-
 lang/php/lib/Schema/AvroArraySchema.php   |   31 +-
 lang/php/lib/Schema/AvroEnumSchema.php    |    9 +-
 lang/php/lib/Schema/AvroField.php         |   99 ++-
 lang/php/lib/Schema/AvroFixedSchema.php   |    6 +-
 lang/php/lib/Schema/AvroMapSchema.php     |    2 +-
 lang/php/lib/Schema/AvroName.php          |    3 +-
 lang/php/lib/Schema/AvroNamedSchema.php   |    4 +-
 lang/php/lib/Schema/AvroNamedSchemata.php |   47 +-
 lang/php/lib/Schema/AvroRecordSchema.php  |   60 +-
 lang/php/lib/Schema/AvroSchema.php        |   28 +-
 lang/php/lib/Schema/AvroUnionSchema.php   |    2 +-
 lang/php/test/SchemaTest.php              | 1314 ++++++++++++++++++++++-------
 13 files changed, 1182 insertions(+), 427 deletions(-)

diff --git a/.github/workflows/test-lang-php.yml 
b/.github/workflows/test-lang-php.yml
index 5d1a4994b2..faa9454590 100644
--- a/.github/workflows/test-lang-php.yml
+++ b/.github/workflows/test-lang-php.yml
@@ -55,7 +55,7 @@ jobs:
         uses: shivammathur/setup-php@v2
         with:
           php-version: ${{ matrix.php }}
-          tools: composer:2.8.12
+          tools: composer:2.9.3
 
       - name: Get Composer Cache Directory
         id: composer-cache
@@ -99,7 +99,7 @@ jobs:
         uses: shivammathur/setup-php@v2
         with:
           php-version: ${{ matrix.php }}
-          tools: composer:2.8.12
+          tools: composer:2.9.3
 
       - name: Cache Local Maven Repository
         uses: actions/cache@v5
diff --git a/lang/php/lib/Schema/AvroArraySchema.php 
b/lang/php/lib/Schema/AvroArraySchema.php
index c7c8d9d459..ff8d6563b1 100644
--- a/lang/php/lib/Schema/AvroArraySchema.php
+++ b/lang/php/lib/Schema/AvroArraySchema.php
@@ -27,42 +27,35 @@ namespace Apache\Avro\Schema;
 class AvroArraySchema extends AvroSchema
 {
     /**
-     * @var AvroName|AvroSchema named schema name or AvroSchema of
-     *                          array element
+     * @var AvroSchema The schema of the array elements
      */
-    private $items;
+    private AvroSchema $items;
 
-    /**
-     * @var bool true if the items schema
-     * FIXME: couldn't we derive this from whether or not $this->items
-     *        is an AvroName or an AvroSchema?
-     */
-    private $is_items_schema_from_schemata;
+    private bool $isItemsSchemaFromSchemata;
 
     /**
      * @param mixed|string $items AvroNamedSchema name or object form
      *        of decoded JSON schema representation.
-     * @param string $defaultNamespace namespace of enclosing schema
-     * @param AvroNamedSchemata &$schemata
+     * @throws AvroSchemaParseException
      */
-    public function __construct($items, $defaultNamespace, &$schemata = null)
+    public function __construct($items, ?string $defaultNamespace, 
AvroNamedSchemata $schemata)
     {
         parent::__construct(AvroSchema::ARRAY_SCHEMA);
 
-        $this->is_items_schema_from_schemata = false;
-        $items_schema = null;
+        $itemsSchema = null;
+        $this->isItemsSchemaFromSchemata = false;
         if (
             is_string($items)
-            && $items_schema = $schemata->schemaByName(
+            && $itemsSchema = $schemata->schemaByName(
                 new AvroName($items, null, $defaultNamespace)
             )
         ) {
-            $this->is_items_schema_from_schemata = true;
+            $this->isItemsSchemaFromSchemata = true;
         } else {
-            $items_schema = AvroSchema::subparse($items, $defaultNamespace, 
$schemata);
+            $itemsSchema = AvroSchema::subparse($items, $defaultNamespace, 
$schemata);
         }
 
-        $this->items = $items_schema;
+        $this->items = $itemsSchema;
     }
 
     /**
@@ -77,7 +70,7 @@ class AvroArraySchema extends AvroSchema
     public function toAvro(): string|array
     {
         $avro = parent::toAvro();
-        $avro[AvroSchema::ITEMS_ATTR] = $this->is_items_schema_from_schemata
+        $avro[AvroSchema::ITEMS_ATTR] = $this->isItemsSchemaFromSchemata && 
$this->items instanceof AvroNamedSchema
             ? $this->items->qualifiedName() : $this->items->toAvro();
 
         return $avro;
diff --git a/lang/php/lib/Schema/AvroEnumSchema.php 
b/lang/php/lib/Schema/AvroEnumSchema.php
index dcbcca07e8..8159bfead1 100644
--- a/lang/php/lib/Schema/AvroEnumSchema.php
+++ b/lang/php/lib/Schema/AvroEnumSchema.php
@@ -28,17 +28,14 @@ class AvroEnumSchema extends AvroNamedSchema
     /**
      * @var string[] array of symbols
      */
-    private $symbols;
+    private array $symbols;
 
     /**
-     * @param AvroName $name
-     * @param string $doc
      * @param string[] $symbols
-     * @param AvroNamedSchemata &$schemata
-     * @param null|mixed $aliases
+     * @param null|array<string> $aliases
      * @throws AvroSchemaParseException
      */
-    public function __construct($name, $doc, $symbols, &$schemata = null, 
$aliases = null)
+    public function __construct(AvroName $name, ?string $doc, mixed $symbols, 
AvroNamedSchemata $schemata, ?array $aliases = null)
     {
         if (!AvroUtil::isList($symbols)) {
             throw new AvroSchemaParseException('Enum Schema symbols are not a 
list');
diff --git a/lang/php/lib/Schema/AvroField.php 
b/lang/php/lib/Schema/AvroField.php
index d7c67e1b0f..2d1510ffc0 100644
--- a/lang/php/lib/Schema/AvroField.php
+++ b/lang/php/lib/Schema/AvroField.php
@@ -18,6 +18,8 @@
  * limitations under the License.
  */
 
+declare(strict_types=1);
+
 namespace Apache\Avro\Schema;
 
 /**
@@ -64,7 +66,7 @@ class AvroField extends AvroSchema implements 
AvroAliasedSchema
         self::IGNORE_SORT_ORDER,
     ];
 
-    private ?string $name;
+    private string $name;
 
     private bool $isTypeFromSchemata;
 
@@ -82,24 +84,22 @@ class AvroField extends AvroSchema implements 
AvroAliasedSchema
      */
     private ?string $order;
     private ?array $aliases;
+    private ?string $doc;
 
     /**
-     * @throws AvroSchemaParseException
+     * @param array<string> $aliases
      * @todo Check validity of $default value
      */
-    public function __construct(
-        ?string $name,
+    private function __construct(
+        string $name,
         string|AvroSchema $schema,
         bool $isTypeFromSchemata,
         bool $hasDefault,
         mixed $default,
         ?string $order = null,
-        mixed $aliases = null
+        ?array $aliases = null,
+        ?string $doc = null
     ) {
-        if (!AvroName::isWellFormedName($name)) {
-            throw new AvroSchemaParseException('Field requires a "name" 
attribute');
-        }
-
         parent::__construct($schema);
         $this->name = $name;
         $this->isTypeFromSchemata = $isTypeFromSchemata;
@@ -107,10 +107,63 @@ class AvroField extends AvroSchema implements 
AvroAliasedSchema
         if ($this->hasDefault) {
             $this->default = $default;
         }
-        self::checkOrderValue($order);
         $this->order = $order;
-        self::hasValidAliases($aliases);
         $this->aliases = $aliases;
+
+        $this->doc = $doc;
+    }
+
+    /**
+     * @throws AvroSchemaParseException
+     */
+    public static function fromFieldDefinition(array $avro, ?string 
$defaultNamespace, AvroNamedSchemata $schemata): self
+    {
+        $name = $avro[self::FIELD_NAME_ATTR] ?? null;
+        $type = $avro[AvroSchema::TYPE_ATTR] ?? null;
+        $order = $avro[self::ORDER_ATTR] ?? null;
+        $aliases = $avro[AvroSchema::ALIASES_ATTR] ?? null;
+        $doc = $avro[AvroSchema::DOC_ATTR] ?? null;
+
+        if (!AvroName::isWellFormedName($name)) {
+            throw new AvroSchemaParseException('Field requires a "name" 
attribute');
+        }
+
+        self::checkOrderValue($order);
+        self::hasValidAliases($aliases);
+        self::hasValidDoc($doc);
+
+        $default = null;
+        $hasDefault = false;
+        if (array_key_exists(self::DEFAULT_ATTR, $avro)) {
+            $default = $avro[self::DEFAULT_ATTR];
+            $hasDefault = true;
+        }
+
+        $isSchemaFromSchemata = false;
+        $fieldAvroSchema = null;
+        if (
+            is_string($type)
+            && $fieldAvroSchema = $schemata->schemaByName(
+                new AvroName($type, null, $defaultNamespace)
+            )
+        ) {
+            $isSchemaFromSchemata = true;
+        } elseif (is_string($type) && self::isPrimitiveType($type)) {
+            $fieldAvroSchema = self::subparse($avro, $defaultNamespace, 
$schemata);
+        } else {
+            $fieldAvroSchema = self::subparse($type, $defaultNamespace, 
$schemata);
+        }
+
+        return new self(
+            name: $name,
+            schema: $fieldAvroSchema,
+            isTypeFromSchemata: $isSchemaFromSchemata,
+            hasDefault: $hasDefault,
+            default: $default,
+            order: $order,
+            aliases: $aliases,
+            doc: $doc
+        );
     }
 
     public function toAvro(): string|array
@@ -131,13 +184,21 @@ class AvroField extends AvroSchema implements 
AvroAliasedSchema
             $avro[self::ORDER_ATTR] = $this->order;
         }
 
+        if (!is_null($this->aliases)) {
+            $avro[AvroSchema::ALIASES_ATTR] = $this->aliases;
+        }
+
+        if (!is_null($this->doc)) {
+            $avro[AvroSchema::DOC_ATTR] = $this->doc;
+        }
+
         return $avro;
     }
 
     /**
      * @returns string the name of this field
      */
-    public function name()
+    public function name(): string
     {
         return $this->name;
     }
@@ -153,7 +214,7 @@ class AvroField extends AvroSchema implements 
AvroAliasedSchema
     /**
      * @returns boolean true if the field has a default and false otherwise
      */
-    public function hasDefaultValue()
+    public function hasDefaultValue(): bool
     {
         return $this->hasDefault;
     }
@@ -163,11 +224,21 @@ class AvroField extends AvroSchema implements 
AvroAliasedSchema
         return $this->aliases;
     }
 
-    public function hasAliases()
+    public function hasAliases(): bool
     {
         return null !== $this->aliases;
     }
 
+    public function getDoc(): ?string
+    {
+        return $this->doc;
+    }
+
+    public function hasDoc(): bool
+    {
+        return null !== $this->doc;
+    }
+
     /**
      * @throws AvroSchemaParseException if $order is not a valid
      *                                  field order value.
diff --git a/lang/php/lib/Schema/AvroFixedSchema.php 
b/lang/php/lib/Schema/AvroFixedSchema.php
index 4cf6e4f794..8fa70fd476 100644
--- a/lang/php/lib/Schema/AvroFixedSchema.php
+++ b/lang/php/lib/Schema/AvroFixedSchema.php
@@ -36,7 +36,7 @@ class AvroFixedSchema extends AvroNamedSchema
      * @param int $size byte count of this fixed schema data value
      * @throws AvroSchemaParseException
      */
-    public function __construct(AvroName $name, ?string $doc, int $size, 
?AvroNamedSchemata &$schemata = null, ?array $aliases = null)
+    public function __construct(AvroName $name, ?string $doc, int $size, 
AvroNamedSchemata $schemata, ?array $aliases = null)
     {
         parent::__construct(AvroSchema::FIXED_SCHEMA, $name, $doc, $schemata, 
$aliases);
         $this->size = $size;
@@ -65,7 +65,7 @@ class AvroFixedSchema extends AvroNamedSchema
     public static function duration(
         AvroName $name,
         ?string $doc,
-        ?AvroNamedSchemata &$schemata = null,
+        AvroNamedSchemata $schemata,
         ?array $aliases = null
     ): self {
         $fixedSchema = new self(
@@ -92,7 +92,7 @@ class AvroFixedSchema extends AvroNamedSchema
         int $size,
         int $precision,
         int $scale,
-        ?AvroNamedSchemata &$schemata = null,
+        AvroNamedSchemata $schemata,
         ?array $aliases = null
     ): self {
         $self = new self(
diff --git a/lang/php/lib/Schema/AvroMapSchema.php 
b/lang/php/lib/Schema/AvroMapSchema.php
index fc305d5399..32e9e7ba86 100644
--- a/lang/php/lib/Schema/AvroMapSchema.php
+++ b/lang/php/lib/Schema/AvroMapSchema.php
@@ -37,7 +37,7 @@ class AvroMapSchema extends AvroSchema
      */
     private bool $isValuesSchemaFromSchemata;
 
-    public function __construct(string|array $values, ?string 
$defaultNamespace, ?AvroNamedSchemata &$schemata = null)
+    public function __construct(string|array $values, ?string 
$defaultNamespace, AvroNamedSchemata $schemata)
     {
         parent::__construct(AvroSchema::MAP_SCHEMA);
 
diff --git a/lang/php/lib/Schema/AvroName.php b/lang/php/lib/Schema/AvroName.php
index bcce7e3a86..a7869b998c 100644
--- a/lang/php/lib/Schema/AvroName.php
+++ b/lang/php/lib/Schema/AvroName.php
@@ -95,9 +95,8 @@ class AvroName implements \Stringable
     /**
      * @returns boolean true if the given name is well-formed
      *          (is a non-null, non-empty string) and false otherwise
-     * @param mixed $name
      */
-    public static function isWellFormedName($name): bool
+    public static function isWellFormedName(mixed $name): bool
     {
         return is_string($name) && !empty($name) && 
preg_match(self::NAME_REGEXP, $name);
     }
diff --git a/lang/php/lib/Schema/AvroNamedSchema.php 
b/lang/php/lib/Schema/AvroNamedSchema.php
index e329498a17..4064fbeff8 100644
--- a/lang/php/lib/Schema/AvroNamedSchema.php
+++ b/lang/php/lib/Schema/AvroNamedSchema.php
@@ -32,7 +32,7 @@ class AvroNamedSchema extends AvroSchema implements 
AvroAliasedSchema
         string $type,
         private readonly AvroName $name,
         private readonly ?string $doc = null,
-        ?AvroNamedSchemata &$schemata = null,
+        ?AvroNamedSchemata $schemata = null,
         private ?array $aliases = null
     ) {
         parent::__construct($type);
@@ -42,7 +42,7 @@ class AvroNamedSchema extends AvroSchema implements 
AvroAliasedSchema
         }
 
         if (!is_null($schemata)) {
-            $schemata = $schemata->cloneWithNewSchema($this);
+            $schemata->registerNamedSchema($this);
         }
     }
 
diff --git a/lang/php/lib/Schema/AvroNamedSchemata.php 
b/lang/php/lib/Schema/AvroNamedSchemata.php
index 37b2b252f3..78912d0ffe 100644
--- a/lang/php/lib/Schema/AvroNamedSchemata.php
+++ b/lang/php/lib/Schema/AvroNamedSchemata.php
@@ -18,6 +18,8 @@
  * limitations under the License.
  */
 
+declare(strict_types=1);
+
 namespace Apache\Avro\Schema;
 
 /**
@@ -34,7 +36,7 @@ class AvroNamedSchemata
     ) {
     }
 
-    public function listSchemas()
+    public function listSchemas(): void
     {
         var_export($this->schemata);
         foreach ($this->schemata as $sch) {
@@ -59,24 +61,37 @@ class AvroNamedSchemata
     /**
      * Creates a new AvroNamedSchemata instance of this schemata instance
      * with the given $schema appended.
+     *
      * @param AvroNamedSchema $schema schema to add to this existing schemata
      * @throws AvroSchemaParseException
      */
-    public function cloneWithNewSchema(AvroNamedSchema $schema): 
AvroNamedSchemata
+    public function cloneWithNewSchema(AvroNamedSchema $schema): self
     {
         $name = $schema->fullname();
-        if (AvroSchema::isValidType($name)) {
-            throw new AvroSchemaParseException(sprintf('Name "%s" is a 
reserved type name', $name));
-        }
-        if ($this->hasName($name)) {
-            throw new AvroSchemaParseException(sprintf('Name "%s" is already 
in use', $name));
-        }
-        $schemata = new AvroNamedSchemata($this->schemata);
+        $this->validateNamedSchema($name);
+
+        $schemata = new self($this->schemata);
         $schemata->schemata[$name] = $schema;
 
         return $schemata;
     }
 
+    /**
+     * Append the given AvroNamedSchema to this schemata instance.
+     *
+     * @param AvroNamedSchema $schema schema to add to this existing schemata
+     * @throws AvroSchemaParseException
+     */
+    public function registerNamedSchema(AvroNamedSchema $schema): self
+    {
+        $name = $schema->fullname();
+        $this->validateNamedSchema($name);
+
+        $this->schemata[$name] = $schema;
+
+        return $this;
+    }
+
     /**
      * @returns bool true if there exists a schema with the given name
      *                  and false otherwise.
@@ -85,4 +100,18 @@ class AvroNamedSchemata
     {
         return array_key_exists($fullname, $this->schemata);
     }
+
+    /**
+     * @throws AvroSchemaParseException
+     */
+    private function validateNamedSchema(string $name): void
+    {
+        if (AvroSchema::isValidType($name)) {
+            throw new AvroSchemaParseException(sprintf('Name "%s" is a 
reserved type name', $name));
+        }
+
+        if ($this->hasName($name)) {
+            throw new AvroSchemaParseException(sprintf('Name "%s" is already 
in use', $name));
+        }
+    }
 }
diff --git a/lang/php/lib/Schema/AvroRecordSchema.php 
b/lang/php/lib/Schema/AvroRecordSchema.php
index b234b50cab..c2337a17e2 100644
--- a/lang/php/lib/Schema/AvroRecordSchema.php
+++ b/lang/php/lib/Schema/AvroRecordSchema.php
@@ -37,7 +37,7 @@ class AvroRecordSchema extends AvroNamedSchema
         AvroName $name,
         ?string $doc,
         ?array $fields,
-        ?AvroNamedSchemata &$schemata = null,
+        AvroNamedSchemata $schemata,
         string $schema_type = AvroSchema::RECORD_SCHEMA,
         ?array $aliases = null
     ) {
@@ -62,64 +62,32 @@ class AvroRecordSchema extends AvroNamedSchema
      * @throws AvroSchemaParseException
      */
     public static function parseFields(
-        array $field_data,
+        array $fieldsDefinitions,
         ?string $default_namespace,
-        ?AvroNamedSchemata $schemata = null
+        AvroNamedSchemata $schemata
     ): array {
         $fields = [];
-        $field_names = [];
-        $alias_names = [];
-        foreach ($field_data as $field) {
-            $name = $field[AvroField::FIELD_NAME_ATTR] ?? null;
-            $type = $field[AvroSchema::TYPE_ATTR] ?? null;
-            $order = $field[AvroField::ORDER_ATTR] ?? null;
-            $aliases = $field[AvroSchema::ALIASES_ATTR] ?? null;
-
-            $default = null;
-            $has_default = false;
-            if (array_key_exists(AvroField::DEFAULT_ATTR, $field)) {
-                $default = $field[AvroField::DEFAULT_ATTR];
-                $has_default = true;
-            }
+        $fieldNames = [];
+        $aliasNames = [];
+        foreach ($fieldsDefinitions as $fieldDefinition) {
+            $name = $fieldDefinition[AvroField::FIELD_NAME_ATTR] ?? null;
 
-            if (in_array($name, $field_names)) {
+            if (in_array($name, $fieldNames)) {
                 throw new AvroSchemaParseException(
                     sprintf("Field name %s is already in use", $name)
                 );
             }
 
-            $is_schema_from_schemata = false;
-            $field_schema = null;
-            if (
-                is_string($type)
-                && $field_schema = $schemata->schemaByName(
-                    new AvroName($type, null, $default_namespace)
-                )
-            ) {
-                $is_schema_from_schemata = true;
-            } elseif (is_string($type) && self::isPrimitiveType($type)) {
-                $field_schema = self::subparse($field, $default_namespace, 
$schemata);
-            } else {
-                $field_schema = self::subparse($type, $default_namespace, 
$schemata);
-            }
+            $newField = AvroField::fromFieldDefinition($fieldDefinition, 
$default_namespace, $schemata);
 
-            $new_field = new AvroField(
-                name: $name,
-                schema: $field_schema,
-                isTypeFromSchemata: $is_schema_from_schemata,
-                hasDefault: $has_default,
-                default: $default,
-                order: $order,
-                aliases: $aliases
-            );
-            $field_names[] = $name;
-            if ($new_field->hasAliases() && array_intersect($alias_names, 
$new_field->getAliases())) {
+            $fieldNames[] = $name;
+            if ($newField->hasAliases() && array_intersect($aliasNames, 
$newField->getAliases())) {
                 throw new AvroSchemaParseException("Alias already in use");
             }
-            if ($new_field->hasAliases()) {
-                array_push($alias_names, ...$new_field->getAliases());
+            if ($newField->hasAliases()) {
+                array_push($aliasNames, ...$newField->getAliases());
             }
-            $fields[] = $new_field;
+            $fields[] = $newField;
         }
 
         return $fields;
diff --git a/lang/php/lib/Schema/AvroSchema.php 
b/lang/php/lib/Schema/AvroSchema.php
index 48337d75db..22cc8a5f30 100644
--- a/lang/php/lib/Schema/AvroSchema.php
+++ b/lang/php/lib/Schema/AvroSchema.php
@@ -335,19 +335,15 @@ class AvroSchema implements \Stringable
     /**
      * @param null|array|string $avro JSON-decoded schema
      * @param null|string $default_namespace namespace of enclosing schema
-     * @param null|AvroNamedSchemata $schemata reference to named schemas
+     * @param AvroNamedSchemata $schemata reference to named schemas
      * @throws AvroSchemaParseException
      * @throws AvroException
      */
     public static function realParse(
         array|string|null $avro,
         ?string $default_namespace = null,
-        ?AvroNamedSchemata &$schemata = null
+        AvroNamedSchemata $schemata = new AvroNamedSchemata()
     ): AvroSchema {
-        if (is_null($schemata)) {
-            $schemata = new AvroNamedSchemata();
-        }
-
         if (is_array($avro)) {
             $type = $avro[self::TYPE_ATTR] ?? null;
 
@@ -384,7 +380,10 @@ class AvroSchema implements \Stringable
                 $new_name = new AvroName($name, $namespace, 
$default_namespace);
                 $doc = $avro[self::DOC_ATTR] ?? null;
                 $aliases = $avro[self::ALIASES_ATTR] ?? null;
-                AvroNamedSchema::hasValidAliases($aliases);
+
+                self::hasValidAliases($aliases);
+                self::hasValidDoc($doc);
+
                 switch ($type) {
                     case self::FIXED_SCHEMA:
                         $size = $avro[self::SIZE_ATTR] ?? throw new 
AvroSchemaParseException(
@@ -544,6 +543,17 @@ class AvroSchema implements \Stringable
         }
     }
 
+    public static function hasValidDoc(mixed $doc): void
+    {
+        if (is_string($doc) || is_null($doc)) {
+            return;
+        }
+
+        throw new AvroSchemaParseException(
+            'Invalid doc value. Must be a string or null.'
+        );
+    }
+
     /**
      * @returns boolean true if $datum is valid for $expected_schema
      *                  and false otherwise.
@@ -701,12 +711,10 @@ class AvroSchema implements \Stringable
     }
 
     /**
-     * @param mixed $avro
-     * @returns AvroSchema
      * @throws AvroSchemaParseException
      * @uses AvroSchema::realParse()
      */
-    protected static function subparse($avro, ?string $default_namespace, 
?AvroNamedSchemata &$schemata = null): AvroSchema
+    protected static function subparse(array|string|null $avro, ?string 
$default_namespace, AvroNamedSchemata $schemata): self
     {
         try {
             return self::realParse($avro, $default_namespace, $schemata);
diff --git a/lang/php/lib/Schema/AvroUnionSchema.php 
b/lang/php/lib/Schema/AvroUnionSchema.php
index 924af99e44..8220fb6463 100644
--- a/lang/php/lib/Schema/AvroUnionSchema.php
+++ b/lang/php/lib/Schema/AvroUnionSchema.php
@@ -41,7 +41,7 @@ class AvroUnionSchema extends AvroSchema
      * @param null|string $defaultNamespace namespace of enclosing schema
      * @throws AvroSchemaParseException
      */
-    public function __construct(array $schemas, ?string $defaultNamespace, 
?AvroNamedSchemata &$schemata = null)
+    public function __construct(array $schemas, ?string $defaultNamespace, 
AvroNamedSchemata $schemata)
     {
         parent::__construct(AvroSchema::UNION_SCHEMA);
 
diff --git a/lang/php/test/SchemaTest.php b/lang/php/test/SchemaTest.php
index 8a0cb29312..2db5bfd03e 100644
--- a/lang/php/test/SchemaTest.php
+++ b/lang/php/test/SchemaTest.php
@@ -18,6 +18,8 @@
  * limitations under the License.
  */
 
+declare(strict_types=1);
+
 namespace Apache\Avro\Tests;
 
 use Apache\Avro\AvroException;
@@ -28,25 +30,25 @@ use PHPUnit\Framework\TestCase;
 
 class SchemaExample
 {
-    public $name;
-    public $normalized_schema_string;
+    public string $name;
+    public string $normalizedSchemaString;
 
     public function __construct(
-        public $schema_string,
-        public $is_valid,
-        $normalized_schema_string = null,
-        $name = null,
-        public $comment = null
+        public string $schemaString,
+        public bool $isValid,
+        ?string $normalizedSchemaString = null,
+        ?string $name = null,
+        public ?string $comment = null
     ) {
-        $this->name = $name ?: $this->schema_string;
-        $this->normalized_schema_string = $normalized_schema_string ?: 
json_encode(json_decode((string) $this->schema_string, true));
+        $this->name = $name ?: $this->schemaString;
+        $this->normalizedSchemaString = $normalizedSchemaString ?: 
json_encode(json_decode((string) $this->schemaString, true));
     }
 }
 
 class SchemaTest extends TestCase
 {
-    private static $examples = [];
-    private static $valid_examples = [];
+    /** @var array<SchemaExample> */
+    private static array $examples = [];
 
     public function test_json_decode(): void
     {
@@ -70,6 +72,9 @@ class SchemaTest extends TestCase
         $this->assertEquals('boolean', json_decode('"boolean"'));
     }
 
+    /**
+     * @return array<array<SchemaExample>>
+     */
     public static function schema_examples_provider(): array
     {
         self::make_examples();
@@ -82,15 +87,15 @@ class SchemaTest extends TestCase
     }
 
     #[DataProvider('schema_examples_provider')]
-    public function test_parse($example): void
+    public function test_parse(SchemaExample $example): void
     {
-        $schema_string = $example->schema_string;
+        $schema_string = $example->schemaString;
 
         try {
-            $normalized_schema_string = $example->normalized_schema_string;
+            $normalized_schema_string = $example->normalizedSchemaString;
             $schema = AvroSchema::parse($schema_string);
             $this->assertTrue(
-                $example->is_valid,
+                $example->isValid,
                 sprintf(
                     "schema_string: %s\n",
                     $schema_string
@@ -99,7 +104,7 @@ class SchemaTest extends TestCase
             $this->assertEquals($normalized_schema_string, (string) $schema);
         } catch (AvroSchemaParseException $e) {
             $this->assertFalse(
-                $example->is_valid,
+                $example->isValid,
                 sprintf(
                     "schema_string: %s\n%s",
                     $schema_string,
@@ -232,6 +237,47 @@ class SchemaTest extends TestCase
         );
     }
 
+    public function test_doc_attribute_on_primitive_fields(): void
+    {
+        $schemaJson = <<<JSON
+            {
+                "type": "record",
+                "name": "fruits",
+                "fields": [
+                    {
+                        "name": "banana",
+                        "type": "string",
+                        "doc": "This is a banana"
+                    }
+                ]
+            }
+            JSON;
+
+        $schema = AvroSchema::parse($schemaJson);
+
+        self::assertEquals(json_decode($schemaJson, associative: true), 
$schema->toAvro());
+    }
+
+    public function test_invalid_doc_attribute_on_field_throws_an_exception(): 
void
+    {
+        $schemaJson = <<<JSON
+            {
+                "type": "record",
+                "name": "fruits",
+                "fields": [
+                    {
+                        "name": "banana",
+                        "type": "string",
+                        "doc": 1
+                    }
+                ]
+            }
+            JSON;
+
+        $this->expectException(AvroSchemaParseException::class);
+        AvroSchema::parse($schemaJson);
+    }
+
     public function test_logical_types_in_record(): void
     {
         $avro = <<<JSON
@@ -376,114 +422,230 @@ class SchemaTest extends TestCase
     {
         $primitive_examples = array_merge(
             [
-                new SchemaExample('"True"', false),
-                new SchemaExample('{"no_type": "test"}', false),
-                new SchemaExample('{"type": "panther"}', false),
+                new SchemaExample(
+                    '"True"',
+                    false
+                ),
+                new SchemaExample(
+                    '{"no_type": "test"}',
+                    false
+                ),
+                new SchemaExample(
+                    '{"type": "panther"}',
+                    false
+                ),
             ],
-            self::make_primitive_examples()
+            self::makePrimitiveExamples()
         );
 
         $array_examples = [
             new SchemaExample('{"type": "array", "items": "long"}', true),
-            new SchemaExample('
-    {"type": "array",
-     "items": {"type": "enum", "name": "Test", "symbols": ["A", "B"]}}
-    ', true),
+            new SchemaExample(
+                <<<JSON
+                    {
+                       "type":"array",
+                       "items":{
+                          "type":"enum",
+                          "name":"Test",
+                          "symbols":[
+                             "A",
+                             "B"
+                          ]
+                       }
+                    }
+                    JSON,
+                true
+            ),
         ];
 
         $map_examples = [
-            new SchemaExample('{"type": "map", "values": "long"}', true),
-            new SchemaExample('
-    {"type": "map",
-     "values": {"type": "enum", "name": "Test", "symbols": ["A", "B"]}}
-    ', true),
+            new SchemaExample(
+                '{"type": "map", "values": "long"}',
+                true
+            ),
+            new SchemaExample(
+                <<<JSON
+                    {
+                       "type":"map",
+                       "values":{
+                          "type":"enum",
+                          "name":"Test",
+                          "symbols":[
+                             "A",
+                             "B"
+                          ]
+                       }
+                    }
+                    JSON,
+                true
+            ),
         ];
 
         $union_examples = [
             new SchemaExample('["string", "null", "long"]', true),
             new SchemaExample('["null", "null"]', false),
             new SchemaExample('["long", "long"]', false),
-            new SchemaExample('
-    [{"type": "array", "items": "long"}
-     {"type": "array", "items": "string"}]
-    ', false),
-            new SchemaExample('["long",
-                          {"type": "long"},
-                          "int"]', false),
-            new SchemaExample('["long",
-                          {"type": "array", "items": "long"},
-                          {"type": "map", "values": "long"},
-                          "int"]', true),
-            new SchemaExample('["long",
-                          ["string", "null"],
-                          "int"]', false),
-            new SchemaExample('["long",
-                          ["string", "null"],
-                          "int"]', false),
             new SchemaExample(
-                '["null", "boolean", "int", "long", "float", "double",
-                          "string", "bytes",
-                          {"type": "array", "items":"int"},
-                          {"type": "map", "values":"int"},
-                          {"name": "bar", "type":"record",
-                           "fields":[{"name":"label", "type":"string"}]},
-                          {"name": "foo", "type":"fixed",
-                           "size":16},
-                          {"name": "baz", "type":"enum", "symbols":["A", "B", 
"C"]}
-                         ]',
+                '[{"type": "array", "items": "long"} {"type": "array", 
"items": "string"}]',
+                false
+            ),
+            new SchemaExample(
+                '["long", {"type": "long"}, "int"]',
+                false
+            ),
+            new SchemaExample(
+                <<<JSON
+                    [
+                       "long",
+                       {
+                          "type":"array",
+                          "items":"long"
+                       },
+                       {
+                          "type":"map",
+                          "values":"long"
+                       },
+                       "int"
+                    ]
+                    JSON,
+                true
+            ),
+            new SchemaExample(
+                '["long", ["string", "null"], "int"]',
+                false
+            ),
+            new SchemaExample(
+                '["long", ["string", "null"], "int"]',
+                false
+            ),
+            new SchemaExample(
+                <<<JSON
+                    [
+                       "null",
+                       "boolean",
+                       "int",
+                       "long",
+                       "float",
+                       "double",
+                       "string",
+                       "bytes",
+                       {
+                          "type":"array",
+                          "items":"int"
+                       },
+                       {
+                          "type":"map",
+                          "values":"int"
+                       },
+                       {
+                          "name":"bar",
+                          "type":"record",
+                          "fields":[
+                             {
+                                "name":"label",
+                                "type":"string"
+                             }
+                          ]
+                       },
+                       {
+                          "name":"foo",
+                          "type":"fixed",
+                          "size":16
+                       },
+                       {
+                          "name":"baz",
+                          "type":"enum",
+                          "symbols":[
+                             "A",
+                             "B",
+                             "C"
+                          ]
+                       }
+                    ]
+                    JSON,
                 true,
                 
'["null","boolean","int","long","float","double","string","bytes",{"type":"array","items":"int"},{"type":"map","values":"int"},{"type":"record","name":"bar","fields":[{"name":"label","type":"string"}]},{"type":"fixed","name":"foo","size":16},{"type":"enum","name":"baz","symbols":["A","B","C"]}]'
             ),
             new SchemaExample(
-                '
-    [{"name":"subtract", "namespace":"com.example",
-      "type":"record",
-      "fields":[{"name":"minuend", "type":"int"},
-                {"name":"subtrahend", "type":"int"}]},
-      {"name": "divide", "namespace":"com.example",
-      "type":"record",
-      "fields":[{"name":"quotient", "type":"int"},
-                {"name":"dividend", "type":"int"}]},
-      {"type": "array", "items": "string"}]
-    ',
+                <<<JSON
+                    [
+                       {
+                          "name":"subtract",
+                          "namespace":"com.example",
+                          "type":"record",
+                          "fields":[
+                             {
+                                "name":"minuend",
+                                "type":"int"
+                             },
+                             {
+                                "name":"subtrahend",
+                                "type":"int"
+                             }
+                          ]
+                       },
+                       {
+                          "name":"divide",
+                          "namespace":"com.example",
+                          "type":"record",
+                          "fields":[
+                             {
+                                "name":"quotient",
+                                "type":"int"
+                             },
+                             {
+                                "name":"dividend",
+                                "type":"int"
+                             }
+                          ]
+                       },
+                       {
+                          "type":"array",
+                          "items":"string"
+                       }
+                    ]
+                    JSON,
                 true,
                 
'[{"type":"record","name":"subtract","namespace":"com.example","fields":[{"name":"minuend","type":"int"},{"name":"subtrahend","type":"int"}]},{"type":"record","name":"divide","namespace":"com.example","fields":[{"name":"quotient","type":"int"},{"name":"dividend","type":"int"}]},{"type":"array","items":"string"}]'
             ),
         ];
 
         $fixed_examples = [
-            new SchemaExample('{"type": "fixed", "name": "Test", "size": 1}', 
true),
-            new SchemaExample('
-    {"type": "fixed",
-     "name": "MyFixed",
-     "namespace": "org.apache.hadoop.avro",
-     "size": 1}
-    ', true),
-            new SchemaExample('
-    {"type": "fixed",
-     "name": "Missing size"}
-    ', false),
-            new SchemaExample('
-    {"type": "fixed",
-     "size": 314}
-    ', false),
+            new SchemaExample(
+                '{"type": "fixed", "name": "Test", "size": 1}',
+                true
+            ),
+            new SchemaExample(
+                <<<JSON
+                    {
+                       "type":"fixed",
+                       "name":"MyFixed",
+                       "namespace":"org.apache.hadoop.avro",
+                       "size":1
+                    }
+                    JSON,
+                true
+            ),
+            new SchemaExample(
+                '{"type": "fixed", "name": "Missing size"}',
+                false
+            ),
+            new SchemaExample(
+                '{"type": "fixed", "size": 314}',
+                false
+            ),
             new SchemaExample(
                 '{"type":"fixed","name":"ex","doc":"this shouldn\'t be 
ignored","size": 314}',
                 true,
                 '{"type":"fixed","name":"ex","doc":"this shouldn\'t be 
ignored","size":314}'
             ),
             new SchemaExample(
-                '{"name": "bar",
-                          "namespace": "com.example",
-                          "type": "fixed",
-                          "size": 32 }',
+                '{"name": "bar", "namespace": "com.example", "type": "fixed", 
"size": 32 }',
                 true,
                 
'{"type":"fixed","name":"bar","namespace":"com.example","size":32}'
             ),
             new SchemaExample(
-                '{"name": "com.example.bar",
-                          "type": "fixed",
-                          "size": 32 }',
+                '{"name": "com.example.bar", "type": "fixed", "size": 32}',
                 true,
                 
'{"type":"fixed","name":"bar","namespace":"com.example","size":32}'
             ),
@@ -509,258 +671,792 @@ class SchemaTest extends TestCase
         );
 
         $enum_examples = [
-            new SchemaExample('{"type": "enum", "name": "Test", "symbols": 
["A", "B"]}', true),
-            new SchemaExample('
-    {"type": "enum",
-     "name": "Status",
-     "symbols": "Normal Caution Critical"}
-    ', false),
-            new SchemaExample('
-    {"type": "enum",
-     "name": [ 0, 1, 1, 2, 3, 5, 8 ],
-     "symbols": ["Golden", "Mean"]}
-    ', false),
-            new SchemaExample('
-    {"type": "enum",
-     "symbols" : ["I", "will", "fail", "no", "name"]}
-    ', false),
-            new SchemaExample('
-    {"type": "enum",
-     "name": "Test"
-     "symbols" : ["AA", "AA"]}
-    ', false),
+            new SchemaExample(
+                '{"type": "enum", "name": "Test", "symbols": ["A", "B"]}',
+                true
+            ),
+            new SchemaExample(
+                '{"type": "enum", "name": "Status", "symbols": "Normal Caution 
Critical"}',
+                false
+            ),
+            new SchemaExample(
+                '{"type": "enum", "name": [ 0, 1, 1, 2, 3, 5, 8 ], "symbols": 
["Golden", "Mean"]}',
+                false
+            ),
+            new SchemaExample(
+                '{"type": "enum", "symbols" : ["I", "will", "fail", "no", 
"name"]}',
+                false
+            ),
+            new SchemaExample(
+                '{"type": "enum", "name": "Test" "symbols" : ["AA", "AA"]}',
+                false
+            ),
             new SchemaExample(
                 '{"type":"enum","name":"Test","symbols":["AA", 16]}',
                 false
             ),
-            new SchemaExample('
-    {"type": "enum",
-     "name": "blood_types",
-     "doc": "AB is freaky.",
-     "symbols" : ["A", "AB", "B", "O"]}
-    ', true),
-            new SchemaExample('
-    {"type": "enum",
-     "name": "blood-types",
-     "doc": 16,
-     "symbols" : ["A", "AB", "B", "O"]}
-    ', false),
+            new SchemaExample(
+                <<<JSON
+                    {
+                       "type":"enum",
+                       "name":"blood_types",
+                       "doc":"AB is freaky.",
+                       "symbols":[
+                          "A",
+                          "AB",
+                          "B",
+                          "O"
+                       ]
+                    }
+                    JSON,
+                true
+            ),
+            new SchemaExample(
+                <<<JSON
+                    {
+                       "type":"enum",
+                       "name":"blood-types",
+                       "doc":16,
+                       "symbols":[
+                          "A",
+                          "AB",
+                          "B",
+                          "O"
+                       ]
+                    }
+                    JSON,
+                false
+            ),
         ];
 
         $record_examples = [];
-        $record_examples[] = new SchemaExample('
-    {"type": "record",
-     "name": "Test",
-     "fields": [{"name": "f",
-                 "type": "long"}]}
-    ', true);
-        $record_examples[] = new SchemaExample('
-    {"type": "error",
-     "name": "Test",
-     "fields": [{"name": "f",
-                 "type": "long"}]}
-    ', true);
-        $record_examples[] = new SchemaExample('
-    {"type": "record",
-     "name": "Node",
-     "fields": [{"name": "label", "type": "string"},
-                {"name": "children",
-                 "type": {"type": "array", "items": "Node"}}]}
-    ', true);
-        $record_examples[] = new SchemaExample('
-    {"type": "record",
-     "name": "ListLink",
-     "fields": [{"name": "car", "type": "int"},
-                {"name": "cdr", "type": "ListLink"}]}
-    ', true);
-        $record_examples[] = new SchemaExample('
-    {"type": "record",
-     "name": "Lisp",
-     "fields": [{"name": "value",
-                 "type": ["null", "string"]}]}
-    ', true);
-        $record_examples[] = new SchemaExample('
-    {"type": "record",
-     "name": "Lisp",
-     "fields": [{"name": "value",
-                 "type": ["null", "string",
-                          {"type": "record",
-                           "name": "Cons",
-                           "fields": [{"name": "car", "type": "string"},
-                                      {"name": "cdr", "type": "string"}]}]}]}
-    ', true);
-        $record_examples[] = new SchemaExample('
-    {"type": "record",
-     "name": "Lisp",
-     "fields": [{"name": "value",
-                 "type": ["null", "string",
-                          {"type": "record",
-                           "name": "Cons",
-                           "fields": [{"name": "car", "type": "Lisp"},
-                                      {"name": "cdr", "type": "Lisp"}]}]}]}
-    ', true);
-        $record_examples[] = new SchemaExample('
-    {"type": "record",
-     "name": "HandshakeRequest",
-     "namespace": "org.apache.avro.ipc",
-     "fields": [{"name": "clientHash",
-                 "type": {"type": "fixed", "name": "MD5", "size": 16}},
-                {"name": "meta",
-                 "type": ["null", {"type": "map", "values": "bytes"}]}]}
-    ', true);
-        $record_examples[] = new SchemaExample('
-    {"type": "record",
-     "name": "HandshakeRequest",
-     "namespace": "org.apache.avro.ipc",
-     "fields": [{"name": "clientHash",
-                 "type": {"type": "fixed", "name": "MD5", "size": 16}},
-                {"name": "clientProtocol", "type": ["null", "string"]},
-                {"name": "serverHash", "type": "MD5"},
-                {"name": "meta",
-                 "type": ["null", {"type": "map", "values": "bytes"}]}]}
-    ', true);
         $record_examples[] = new SchemaExample(
-            '
-    {"type": "record",
-     "name": "HandshakeResponse",
-     "namespace": "org.apache.avro.ipc",
-     "fields": [{"name": "match",
-                 "type": {"type": "enum",
-                          "name": "HandshakeMatch",
-                          "symbols": ["BOTH", "CLIENT", "NONE"]}},
-                {"name": "serverProtocol", "type": ["null", "string"]},
-                {"name": "serverHash",
-                 "type": ["null",
-                          {"name": "MD5", "size": 16, "type": "fixed"}]},
-                {"name": "meta",
-                 "type": ["null", {"type": "map", "values": "bytes"}]}]}
-    ',
+            <<<JSON
+                            {
+                   "type":"record",
+                   "name":"Test",
+                   "fields":[
+                      {
+                         "name":"f",
+                         "type":"long"
+                      }
+                   ]
+                }
+                JSON,
+            true
+        );
+        $record_examples[] = new SchemaExample(
+            <<<JSON
+                {
+                   "type":"error",
+                   "name":"Test",
+                   "fields":[
+                      {
+                         "name":"f",
+                         "type":"long"
+                      }
+                   ]
+                }
+                JSON,
+            true
+        );
+        $record_examples[] = new SchemaExample(
+            <<<JSON
+                {
+                   "type":"record",
+                   "name":"Node",
+                   "fields":[
+                      {
+                         "name":"label",
+                         "type":"string"
+                      },
+                      {
+                         "name":"children",
+                         "type":{
+                            "type":"array",
+                            "items":"Node"
+                         }
+                      }
+                   ]
+                }
+                JSON,
+            true
+        );
+        $record_examples[] = new SchemaExample(
+            <<<JSON
+                {
+                   "type":"record",
+                   "name":"ListLink",
+                   "fields":[
+                      {
+                         "name":"car",
+                         "type":"int"
+                      },
+                      {
+                         "name":"cdr",
+                         "type":"ListLink"
+                      }
+                   ]
+                }
+                JSON,
+            true
+        );
+        $record_examples[] = new SchemaExample(
+            <<<JSON
+                {
+                   "type":"record",
+                   "name":"Lisp",
+                   "fields":[
+                      {
+                         "name":"value",
+                         "type":[
+                            "null",
+                            "string"
+                         ]
+                      }
+                   ]
+                }
+                JSON,
+            true
+        );
+        $record_examples[] = new SchemaExample(
+            <<<JSON
+                {
+                   "type":"record",
+                   "name":"Lisp",
+                   "fields":[
+                      {
+                         "name":"value",
+                         "type":[
+                            "null",
+                            "string",
+                            {
+                               "type":"record",
+                               "name":"Cons",
+                               "fields":[
+                                  {
+                                     "name":"car",
+                                     "type":"string"
+                                  },
+                                  {
+                                     "name":"cdr",
+                                     "type":"string"
+                                  }
+                               ]
+                            }
+                         ]
+                      }
+                   ]
+                }
+                JSON,
+            true
+        );
+        $record_examples[] = new SchemaExample(
+            <<<JSON
+                {
+                   "type":"record",
+                   "name":"Lisp",
+                   "fields":[
+                      {
+                         "name":"value",
+                         "type":[
+                            "null",
+                            "string",
+                            {
+                               "type":"record",
+                               "name":"Cons",
+                               "fields":[
+                                  {
+                                     "name":"car",
+                                     "type":"Lisp"
+                                  },
+                                  {
+                                     "name":"cdr",
+                                     "type":"Lisp"
+                                  }
+                               ]
+                            }
+                         ]
+                      }
+                   ]
+                }
+                JSON,
+            true
+        );
+        $record_examples[] = new SchemaExample(
+            <<<JSON
+                {
+                   "type":"record",
+                   "name":"HandshakeRequest",
+                   "namespace":"org.apache.avro.ipc",
+                   "fields":[
+                      {
+                         "name":"clientHash",
+                         "type":{
+                            "type":"fixed",
+                            "name":"MD5",
+                            "size":16
+                         }
+                      },
+                      {
+                         "name":"meta",
+                         "type":[
+                            "null",
+                            {
+                               "type":"map",
+                               "values":"bytes"
+                            }
+                         ]
+                      }
+                   ]
+                }
+                JSON,
+            true
+        );
+        $record_examples[] = new SchemaExample(
+            <<<JSON
+                {
+                   "type":"record",
+                   "name":"HandshakeRequest",
+                   "namespace":"org.apache.avro.ipc",
+                   "fields":[
+                      {
+                         "name":"clientHash",
+                         "type":{
+                            "type":"fixed",
+                            "name":"MD5",
+                            "size":16
+                         }
+                      },
+                      {
+                         "name":"clientProtocol",
+                         "type":[
+                            "null",
+                            "string"
+                         ]
+                      },
+                      {
+                         "name":"serverHash",
+                         "type":"MD5"
+                      },
+                      {
+                         "name":"meta",
+                         "type":[
+                            "null",
+                            {
+                               "type":"map",
+                               "values":"bytes"
+                            }
+                         ]
+                      }
+                   ]
+                }
+                JSON,
+            true
+        );
+        $record_examples[] = new SchemaExample(
+            <<<JSON
+                {
+                   "type":"record",
+                   "name":"HandshakeResponse",
+                   "namespace":"org.apache.avro.ipc",
+                   "fields":[
+                      {
+                         "name":"match",
+                         "type":{
+                            "type":"enum",
+                            "name":"HandshakeMatch",
+                            "symbols":[
+                               "BOTH",
+                               "CLIENT",
+                               "NONE"
+                            ]
+                         }
+                      },
+                      {
+                         "name":"serverProtocol",
+                         "type":[
+                            "null",
+                            "string"
+                         ]
+                      },
+                      {
+                         "name":"serverHash",
+                         "type":[
+                            "null",
+                            {
+                               "name":"MD5",
+                               "size":16,
+                               "type":"fixed"
+                            }
+                         ]
+                      },
+                      {
+                         "name":"meta",
+                         "type":[
+                            "null",
+                            {
+                               "type":"map",
+                               "values":"bytes"
+                            }
+                         ]
+                      }
+                   ]
+                }
+                JSON,
             true,
             
'{"type":"record","name":"HandshakeResponse","namespace":"org.apache.avro.ipc","fields":[{"name":"match","type":{"type":"enum","name":"HandshakeMatch","symbols":["BOTH","CLIENT","NONE"]}},{"name":"serverProtocol","type":["null","string"]},{"name":"serverHash","type":["null",{"type":"fixed","name":"MD5","size":16}]},{"name":"meta","type":["null",{"type":"map","values":"bytes"}]}]}'
         );
         $record_examples[] = new SchemaExample(
-            '{"type": "record",
- "namespace": "org.apache.avro",
- "name": "Interop",
- "fields": [{"type": {"fields": [{"type": {"items": "org.apache.avro.Node",
-                                           "type": "array"},
-                                  "name": "children"}],
-                      "type": "record",
-                      "name": "Node"},
-             "name": "recordField"}]}
-',
+            <<<JSON
+                {
+                   "type":"record",
+                   "namespace":"org.apache.avro",
+                   "name":"Interop",
+                   "fields":[
+                      {
+                         "type":{
+                            "fields":[
+                               {
+                                  "type":{
+                                     "items":"org.apache.avro.Node",
+                                     "type":"array"
+                                  },
+                                  "name":"children"
+                               }
+                            ],
+                            "type":"record",
+                            "name":"Node"
+                         },
+                         "name":"recordField"
+                      }
+                   ]
+                }
+                JSON,
             true,
             
'{"type":"record","name":"Interop","namespace":"org.apache.avro","fields":[{"name":"recordField","type":{"type":"record","name":"Node","fields":[{"name":"children","type":{"type":"array","items":"Node"}}]}}]}'
         );
         $record_examples[] = new SchemaExample(
-            '{"type": "record",
- "namespace": "org.apache.avro",
- "name": "Interop",
- "fields": [{"type": {"symbols": ["A", "B", "C"], "type": "enum", "name": 
"Kind"},
-             "name": "enumField"},
-            {"type": {"fields": [{"type": "string", "name": "label"},
-                                 {"type": {"items": "org.apache.avro.Node", 
"type": "array"},
-                                  "name": "children"}],
-                      "type": "record",
-                      "name": "Node"},
-             "name": "recordField"}]}',
+            <<<JSON
+                {
+                   "type":"record",
+                   "namespace":"org.apache.avro",
+                   "name":"Interop",
+                   "fields":[
+                      {
+                         "type":{
+                            "symbols":[
+                               "A",
+                               "B",
+                               "C"
+                            ],
+                            "type":"enum",
+                            "name":"Kind"
+                         },
+                         "name":"enumField"
+                      },
+                      {
+                         "type":{
+                            "fields":[
+                               {
+                                  "type":"string",
+                                  "name":"label"
+                               },
+                               {
+                                  "type":{
+                                     "items":"org.apache.avro.Node",
+                                     "type":"array"
+                                  },
+                                  "name":"children"
+                               }
+                            ],
+                            "type":"record",
+                            "name":"Node"
+                         },
+                         "name":"recordField"
+                      }
+                   ]
+                }
+                JSON,
             true,
             
'{"type":"record","name":"Interop","namespace":"org.apache.avro","fields":[{"name":"enumField","type":{"type":"enum","name":"Kind","symbols":["A","B","C"]}},{"name":"recordField","type":{"type":"record","name":"Node","fields":[{"name":"label","type":"string"},{"name":"children","type":{"type":"array","items":"Node"}}]}}]}'
         );
 
         $record_examples[] = new SchemaExample(
-            '
-    {"type": "record",
-     "name": "Interop",
-     "namespace": "org.apache.avro",
-     "fields": [{"name": "intField", "type": "int"},
-                {"name": "longField", "type": "long"},
-                {"name": "stringField", "type": "string"},
-                {"name": "boolField", "type": "boolean"},
-                {"name": "floatField", "type": "float"},
-                {"name": "doubleField", "type": "double"},
-                {"name": "bytesField", "type": "bytes"},
-                {"name": "nullField", "type": "null"},
-                {"name": "arrayField",
-                 "type": {"type": "array", "items": "double"}},
-                {"name": "mapField",
-                 "type": {"type": "map",
-                          "values": {"name": "Foo",
-                                     "type": "record",
-                                     "fields": [{"name": "label",
-                                                 "type": "string"}]}}},
-                {"name": "unionField",
-                 "type": ["boolean",
-                          "double",
-                          {"type": "array", "items": "bytes"}]},
-                {"name": "enumField",
-                 "type": {"type": "enum",
-                          "name": "Kind",
-                          "symbols": ["A", "B", "C"]}},
-                {"name": "fixedField",
-                 "type": {"type": "fixed", "name": "MD5", "size": 16}},
-                {"name": "recordField",
-                 "type": {"type": "record",
-                          "name": "Node",
-                          "fields": [{"name": "label", "type": "string"},
-                                     {"name": "children",
-                                      "type": {"type": "array",
-                                               "items": "Node"}}]}}]}
-    ',
+            <<<JSON
+                {
+                   "type":"record",
+                   "name":"Interop",
+                   "namespace":"org.apache.avro",
+                   "fields":[
+                      {
+                         "name":"intField",
+                         "type":"int"
+                      },
+                      {
+                         "name":"longField",
+                         "type":"long"
+                      },
+                      {
+                         "name":"stringField",
+                         "type":"string"
+                      },
+                      {
+                         "name":"boolField",
+                         "type":"boolean"
+                      },
+                      {
+                         "name":"floatField",
+                         "type":"float"
+                      },
+                      {
+                         "name":"doubleField",
+                         "type":"double"
+                      },
+                      {
+                         "name":"bytesField",
+                         "type":"bytes"
+                      },
+                      {
+                         "name":"nullField",
+                         "type":"null"
+                      },
+                      {
+                         "name":"arrayField",
+                         "type":{
+                            "type":"array",
+                            "items":"double"
+                         }
+                      },
+                      {
+                         "name":"mapField",
+                         "type":{
+                            "type":"map",
+                            "values":{
+                               "name":"Foo",
+                               "type":"record",
+                               "fields":[
+                                  {
+                                     "name":"label",
+                                     "type":"string"
+                                  }
+                               ]
+                            }
+                         }
+                      },
+                      {
+                         "name":"unionField",
+                         "type":[
+                            "boolean",
+                            "double",
+                            {
+                               "type":"array",
+                               "items":"bytes"
+                            }
+                         ]
+                      },
+                      {
+                         "name":"enumField",
+                         "type":{
+                            "type":"enum",
+                            "name":"Kind",
+                            "symbols":[
+                               "A",
+                               "B",
+                               "C"
+                            ]
+                         }
+                      },
+                      {
+                         "name":"fixedField",
+                         "type":{
+                            "type":"fixed",
+                            "name":"MD5",
+                            "size":16
+                         }
+                      },
+                      {
+                         "name":"recordField",
+                         "type":{
+                            "type":"record",
+                            "name":"Node",
+                            "fields":[
+                               {
+                                  "name":"label",
+                                  "type":"string"
+                               },
+                               {
+                                  "name":"children",
+                                  "type":{
+                                     "type":"array",
+                                     "items":"Node"
+                                  }
+                               }
+                            ]
+                         }
+                      }
+                   ]
+                }
+                JSON,
             true,
             
'{"type":"record","name":"Interop","namespace":"org.apache.avro","fields":[{"name":"intField","type":"int"},{"name":"longField","type":"long"},{"name":"stringField","type":"string"},{"name":"boolField","type":"boolean"},{"name":"floatField","type":"float"},{"name":"doubleField","type":"double"},{"name":"bytesField","type":"bytes"},{"name":"nullField","type":"null"},{"name":"arrayField","type":{"type":"array","items":"double"}},{"name":"mapField","type":{"type":"map","values":
 [...]
         );
+
         $record_examples[] = new SchemaExample(
-            '{"type": "record", "namespace": "org.apache.avro", "name": 
"Interop", "fields": [{"type": "int", "name": "intField"}, {"type": "long", 
"name": "longField"}, {"type": "string", "name": "stringField"}, {"type": 
"boolean", "name": "boolField"}, {"type": "float", "name": "floatField"}, 
{"type": "double", "name": "doubleField"}, {"type": "bytes", "name": 
"bytesField"}, {"type": "null", "name": "nullField"}, {"type": {"items": 
"double", "type": "array"}, "name": "arrayField"}, {"t [...]
-',
+            <<<JSON
+                {
+                   "type":"record",
+                   "namespace":"org.apache.avro",
+                   "name":"Interop",
+                   "fields":[
+                      {
+                         "type":"int",
+                         "name":"intField"
+                      },
+                      {
+                         "type":"long",
+                         "name":"longField"
+                      },
+                      {
+                         "type":"string",
+                         "name":"stringField"
+                      },
+                      {
+                         "type":"boolean",
+                         "name":"boolField"
+                      },
+                      {
+                         "type":"float",
+                         "name":"floatField"
+                      },
+                      {
+                         "type":"double",
+                         "name":"doubleField"
+                      },
+                      {
+                         "type":"bytes",
+                         "name":"bytesField"
+                      },
+                      {
+                         "type":"null",
+                         "name":"nullField"
+                      },
+                      {
+                         "type":{
+                            "items":"double",
+                            "type":"array"
+                         },
+                         "name":"arrayField"
+                      },
+                      {
+                         "type":{
+                            "type":"map",
+                            "values":{
+                               "fields":[
+                                  {
+                                     "type":"string",
+                                     "name":"label"
+                                  }
+                               ],
+                               "type":"record",
+                               "name":"Foo"
+                            }
+                         },
+                         "name":"mapField"
+                      },
+                      {
+                         "type":[
+                            "boolean",
+                            "double",
+                            {
+                               "items":"bytes",
+                               "type":"array"
+                            }
+                         ],
+                         "name":"unionField"
+                      },
+                      {
+                         "type":{
+                            "symbols":[
+                               "A",
+                               "B",
+                               "C"
+                            ],
+                            "type":"enum",
+                            "name":"Kind"
+                         },
+                         "name":"enumField"
+                      },
+                      {
+                         "type":{
+                            "type":"fixed",
+                            "name":"MD5",
+                            "size":16
+                         },
+                         "name":"fixedField"
+                      },
+                      {
+                         "type":{
+                            "fields":[
+                               {
+                                  "type":"string",
+                                  "name":"label"
+                               },
+                               {
+                                  "type":{
+                                     "items":"org.apache.avro.Node",
+                                     "type":"array"
+                                  },
+                                  "name":"children"
+                               }
+                            ],
+                            "type":"record",
+                            "name":"Node"
+                         },
+                         "name":"recordField"
+                      }
+                   ]
+                }
+                JSON,
             true,
             
'{"type":"record","name":"Interop","namespace":"org.apache.avro","fields":[{"name":"intField","type":"int"},{"name":"longField","type":"long"},{"name":"stringField","type":"string"},{"name":"boolField","type":"boolean"},{"name":"floatField","type":"float"},{"name":"doubleField","type":"double"},{"name":"bytesField","type":"bytes"},{"name":"nullField","type":"null"},{"name":"arrayField","type":{"type":"array","items":"double"}},{"name":"mapField","type":{"type":"map","values":
 [...]
         );
+
         $record_examples[] = new SchemaExample(
-            '
-    {"type": "record",
-     "name": "ipAddr",
-     "fields": [{"name": "addr",
-                 "type": [{"name": "IPv6", "type": "fixed", "size": 16},
-                          {"name": "IPv4", "type": "fixed", "size": 4}]}]}
-    ',
+            <<<JSON
+                {
+                   "type":"record",
+                   "name":"ipAddr",
+                   "fields":[
+                      {
+                         "name":"addr",
+                         "type":[
+                            {
+                               "name":"IPv6",
+                               "type":"fixed",
+                               "size":16
+                            },
+                            {
+                               "name":"IPv4",
+                               "type":"fixed",
+                               "size":4
+                            }
+                         ]
+                      }
+                   ]
+                }
+                JSON,
             true,
             
'{"type":"record","name":"ipAddr","fields":[{"name":"addr","type":[{"type":"fixed","name":"IPv6","size":16},{"type":"fixed","name":"IPv4","size":4}]}]}'
         );
-        $record_examples[] = new SchemaExample('
-    {"type": "record",
-     "name": "Address",
-     "fields": [{"type": "string"},
-                {"type": "string", "name": "City"}]}
-    ', false);
-        $record_examples[] = new SchemaExample('
-    {"type": "record",
-     "name": "Event",
-     "fields": [{"name": "Sponsor"},
-                {"name": "City", "type": "string"}]}
-    ', false);
-        $record_examples[] = new SchemaExample('
-    {"type": "record",
-     "fields": "His vision, from the constantly passing bars,"
-     "name", "Rainer"}
-    ', false);
-        $record_examples[] = new SchemaExample('
-    {"name": ["Tom", "Jerry"],
-     "type": "record",
-     "fields": [{"name": "name", "type": "string"}]}
-    ', false);
+
+        $record_examples[] = new SchemaExample(
+            <<<JSON
+                {
+                   "type":"record",
+                   "name":"Address",
+                   "fields":[
+                      {
+                         "type":"string"
+                      },
+                      {
+                         "type":"string",
+                         "name":"City"
+                      }
+                   ]
+                }
+                JSON,
+            false
+        );
+
+        $record_examples[] = new SchemaExample(
+            <<<JSON
+                {
+                   "type":"record",
+                   "name":"Event",
+                   "fields":[
+                      {
+                         "name":"Sponsor"
+                      },
+                      {
+                         "name":"City",
+                         "type":"string"
+                      }
+                   ]
+                }
+                JSON,
+            false
+        );
+
+        $record_examples[] = new SchemaExample(
+            <<<JSON
+                {
+                  "type": "record",
+                  "fields": "His vision, from the constantly passing bars,"
+                  "name",
+                  "Rainer"
+                }
+                JSON,
+            false
+        );
+
         $record_examples[] = new SchemaExample(
-            '
-    {"type":"record","name":"foo","doc":"doc string",
-     "fields":[{"name":"bar", "type":"int", "order":"ascending", "default":1}]}
-',
+            <<<JSON
+                {
+                   "name":[
+                      "Tom",
+                      "Jerry"
+                   ],
+                   "type":"record",
+                   "fields":[
+                      {
+                         "name":"name",
+                         "type":"string"
+                      }
+                   ]
+                }
+                JSON,
+            false
+        );
+
+        $record_examples[] = new SchemaExample(
+            <<<JSON
+                {
+                   "type":"record",
+                   "name":"foo",
+                   "doc":"doc string",
+                   "fields":[
+                      {
+                         "name":"bar",
+                         "type":"int",
+                         "order":"ascending",
+                         "default":1
+                      }
+                   ]
+                }
+                JSON,
             true,
             '{"type":"record","name":"foo","doc":"doc 
string","fields":[{"name":"bar","type":"int","default":1,"order":"ascending"}]}'
         );
-        $record_examples[] = new SchemaExample('
-    {"type":"record", "name":"foo", "doc":"doc string",
-     "fields":[{"name":"bar", "type":"int", "order":"bad"}]}
-', false);
+
+        $record_examples[] = new SchemaExample(
+            <<<JSON
+                {
+                   "type":"record",
+                   "name":"foo",
+                   "doc":"doc string",
+                   "fields":[
+                      {
+                         "name":"bar",
+                         "type":"int",
+                         "order":"bad"
+                      }
+                   ]
+                }
+                JSON,
+            false
+        );
+
         $record_examples[] = new SchemaExample(
             '{"type":"record", "name":"Record2", "aliases":["Record1"]}',
             false
@@ -775,15 +1471,9 @@ class SchemaTest extends TestCase
             $union_examples,
             $record_examples
         );
-        self::$valid_examples = [];
-        foreach (self::$examples as $example) {
-            if ($example->is_valid) {
-                self::$valid_examples[] = $example;
-            }
-        }
     }
 
-    protected static function make_primitive_examples()
+    protected static function makePrimitiveExamples(): array
     {
         $examples = [];
         foreach ([

Reply via email to