Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Validation;
4
5use JsonSerializable;
6
7class ValidationError implements JsonSerializable
8{
9 /**
10 * Tracks which optional values were explicitly set.
11 */
12 protected bool $hasExpectedValue = false;
13
14 protected bool $hasActualValue = false;
15
16 /**
17 * Create a new ValidationError.
18 */
19 public function __construct(
20 public readonly string $field,
21 public readonly string $message,
22 public readonly ?string $rule = null,
23 mixed $expected = null,
24 mixed $actual = null,
25 public readonly array $context = []
26 ) {
27 $this->hasExpectedValue = func_num_args() >= 4;
28 $this->hasActualValue = func_num_args() >= 5;
29
30 // Use object properties for values that can be null
31 $this->expected = $expected;
32 $this->actual = $actual;
33 }
34
35 public readonly mixed $expected;
36
37 public readonly mixed $actual;
38
39 /**
40 * Create from field and message.
41 */
42 public static function make(string $field, string $message): self
43 {
44 return new self($field, $message);
45 }
46
47 /**
48 * Create with rule context.
49 */
50 public static function withRule(string $field, string $message, string $rule): self
51 {
52 return new self($field, $message, $rule);
53 }
54
55 /**
56 * Create with full context.
57 */
58 public static function withContext(
59 string $field,
60 string $message,
61 string $rule,
62 mixed $expected = null,
63 mixed $actual = null,
64 array $context = []
65 ): self {
66 return new self($field, $message, $rule, $expected, $actual, $context);
67 }
68
69 /**
70 * Get the field path.
71 */
72 public function getField(): string
73 {
74 return $this->field;
75 }
76
77 /**
78 * Get the error message.
79 */
80 public function getMessage(): string
81 {
82 return $this->message;
83 }
84
85 /**
86 * Get the validation rule that failed.
87 */
88 public function getRule(): ?string
89 {
90 return $this->rule;
91 }
92
93 /**
94 * Get the expected value.
95 */
96 public function getExpected(): mixed
97 {
98 return $this->expected;
99 }
100
101 /**
102 * Get the actual value.
103 */
104 public function getActual(): mixed
105 {
106 return $this->actual;
107 }
108
109 /**
110 * Get additional context.
111 */
112 public function getContext(): array
113 {
114 return $this->context;
115 }
116
117 /**
118 * Check if error has rule information.
119 */
120 public function hasRule(): bool
121 {
122 return $this->rule !== null;
123 }
124
125 /**
126 * Check if error has expected value.
127 */
128 public function hasExpected(): bool
129 {
130 return $this->hasExpectedValue;
131 }
132
133 /**
134 * Check if error has actual value.
135 */
136 public function hasActual(): bool
137 {
138 return $this->hasActualValue;
139 }
140
141 /**
142 * Convert to array.
143 *
144 * @return array<string, mixed>
145 */
146 public function toArray(): array
147 {
148 $data = [
149 'field' => $this->field,
150 'message' => $this->message,
151 ];
152
153 if ($this->rule !== null) {
154 $data['rule'] = $this->rule;
155 }
156
157 if ($this->hasExpectedValue) {
158 $data['expected'] = $this->expected;
159 }
160
161 if ($this->hasActualValue) {
162 $data['actual'] = $this->actual;
163 }
164
165 if (! empty($this->context)) {
166 $data['context'] = $this->context;
167 }
168
169 return $data;
170 }
171
172 /**
173 * Convert to JSON.
174 */
175 public function jsonSerialize(): array
176 {
177 return $this->toArray();
178 }
179
180 /**
181 * Convert to string.
182 */
183 public function __toString(): string
184 {
185 return "{$this->field}: {$this->message}";
186 }
187}