Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Data\Types;
4
5use SocialDept\AtpSchema\Data\TypeDefinition;
6use SocialDept\AtpSchema\Exceptions\RecordValidationException;
7
8class ObjectType extends TypeDefinition
9{
10 /**
11 * Object properties.
12 *
13 * @var array<string, TypeDefinition>
14 */
15 public readonly array $properties;
16
17 /**
18 * Required property names.
19 *
20 * @var array<string>
21 */
22 public readonly array $required;
23
24 /**
25 * Whether nullable properties are allowed.
26 */
27 public readonly bool $nullable;
28
29 /**
30 * Create a new ObjectType.
31 *
32 * @param array<string, TypeDefinition> $properties
33 * @param array<string> $required
34 */
35 public function __construct(
36 array $properties = [],
37 array $required = [],
38 bool $nullable = false,
39 ?string $description = null
40 ) {
41 parent::__construct('object', $description);
42
43 $this->properties = $properties;
44 $this->required = $required;
45 $this->nullable = $nullable;
46 }
47
48 /**
49 * Create from array data.
50 */
51 public static function fromArray(array $data): self
52 {
53 // Properties will be parsed by TypeParser, this is just a placeholder
54 return new self(
55 properties: [],
56 required: $data['required'] ?? [],
57 nullable: $data['nullable'] ?? false,
58 description: $data['description'] ?? null
59 );
60 }
61
62 /**
63 * Convert to array.
64 */
65 public function toArray(): array
66 {
67 $array = ['type' => $this->type];
68
69 if ($this->description !== null) {
70 $array['description'] = $this->description;
71 }
72
73 if (! empty($this->properties)) {
74 $array['properties'] = array_map(
75 fn (TypeDefinition $type) => $type->toArray(),
76 $this->properties
77 );
78 }
79
80 if (! empty($this->required)) {
81 $array['required'] = $this->required;
82 }
83
84 if ($this->nullable) {
85 $array['nullable'] = $this->nullable;
86 }
87
88 return $array;
89 }
90
91 /**
92 * Validate a value against this type definition.
93 */
94 public function validate(mixed $value, string $path = ''): void
95 {
96 if (! is_array($value)) {
97 throw RecordValidationException::invalidType($path, 'object', gettype($value));
98 }
99
100 // Validate required properties
101 foreach ($this->required as $requiredKey) {
102 if (! array_key_exists($requiredKey, $value)) {
103 throw RecordValidationException::invalidValue($path, "missing required property '{$requiredKey}'");
104 }
105 }
106
107 // Validate each property
108 foreach ($this->properties as $key => $propertyType) {
109 if (! array_key_exists($key, $value)) {
110 continue;
111 }
112
113 $propertyPath = $path ? "{$path}.{$key}" : $key;
114 $propertyValue = $value[$key];
115
116 // Handle nullable
117 if ($propertyValue === null && $this->nullable) {
118 continue;
119 }
120
121 $propertyType->validate($propertyValue, $propertyPath);
122 }
123 }
124
125 /**
126 * Set properties after construction.
127 *
128 * @param array<string, TypeDefinition> $properties
129 */
130 public function withProperties(array $properties): self
131 {
132 return new self(
133 properties: $properties,
134 required: $this->required,
135 nullable: $this->nullable,
136 description: $this->description
137 );
138 }
139}