diff --git a/Tests/Attributes/ValidatorTest.php b/Tests/Attributes/ValidatorTest.php new file mode 100644 index 0000000..8ca3650 --- /dev/null +++ b/Tests/Attributes/ValidatorTest.php @@ -0,0 +1,83 @@ +expectException(\OutOfRangeException::class); + Validator::validateProperty(200, $prop); + } + + public function testValidatesEmail(): void + { + $prop = new ReflectionProperty(ValidatorFixture::class, 'email'); + Validator::validateProperty('user@example.com', $prop); + $this->expectException(\InvalidArgumentException::class); + Validator::validateProperty('not-an-email', $prop); + } + + public function testValidatesLength(): void + { + $prop = new ReflectionProperty(ValidatorFixture::class, 'name'); + Validator::validateProperty('Nejc', $prop); + $this->expectException(\InvalidArgumentException::class); + Validator::validateProperty('x', $prop); + } + + public function testValidatesNotNull(): void + { + $prop = new ReflectionProperty(ValidatorFixture::class, 'name'); + $this->expectException(\InvalidArgumentException::class); + Validator::validateProperty(null, $prop); + } + + public function testCacheReusedAcrossCalls(): void + { + // First call populates the cache. + $prop1 = new ReflectionProperty(ValidatorFixture::class, 'age'); + Validator::validateProperty(30, $prop1); + + // A freshly constructed ReflectionProperty for the same field should + // hit the cache (key is Class::property, not the reflection object). + $prop2 = new ReflectionProperty(ValidatorFixture::class, 'age'); + Validator::validateProperty(25, $prop2); + + // Sanity: clearing resets state without errors. + Validator::clearCache(); + Validator::validateProperty(40, $prop1); + $this->assertTrue(true); + } +} + +final class ValidatorFixture +{ + #[Range(min: 0, max: 150)] + public int $age = 0; + + #[Email] + #[NotNull] + public ?string $email = null; + + #[NotNull] + #[Length(min: 2, max: 50)] + public ?string $name = null; +} diff --git a/src/Abstract/AbstractVector.php b/src/Abstract/AbstractVector.php index f995107..234d8f7 100644 --- a/src/Abstract/AbstractVector.php +++ b/src/Abstract/AbstractVector.php @@ -11,9 +11,17 @@ abstract class AbstractVector implements DataTypeInterface { protected array $components; - public function __construct(array $components) + /** + * @param array $components + * @param bool $trusted Internal use only. When true, skips component validation. + * Used by arithmetic ops whose result is known to be valid + * (same dimension, all numeric) by construction. + */ + public function __construct(array $components, bool $trusted = false) { - $this->validateComponents($components); + if (!$trusted) { + $this->validateComponents($components); + } $this->components = $components; } @@ -29,7 +37,11 @@ public function getComponents(): array public function magnitude(): float { - return sqrt(array_sum(array_map(fn ($component) => $component ** 2, $this->components))); + $sum = 0.0; + foreach ($this->components as $c) { + $sum += $c * $c; + } + return sqrt($sum); } public function normalize(): self @@ -39,8 +51,11 @@ public function normalize(): self throw new InvalidArgumentException("Cannot normalize a zero vector"); } - $normalized = array_map(fn ($component) => $component / $magnitude, $this->components); - return new static($normalized); + $result = []; + foreach ($this->components as $i => $c) { + $result[$i] = $c / $magnitude; + } + return new static($result, true); } public function dot(self $other): float @@ -49,11 +64,12 @@ public function dot(self $other): float throw new InvalidArgumentException("Cannot calculate dot product of vectors with different dimensions"); } - return array_sum(array_map( - fn ($a, $b) => $a * $b, - $this->components, - $other->components - )); + $sum = 0.0; + $b = $other->components; + foreach ($this->components as $i => $a) { + $sum += $a * $b[$i]; + } + return $sum; } public function add(self $other): self @@ -62,13 +78,12 @@ public function add(self $other): self throw new InvalidArgumentException("Cannot add vectors with different dimensions"); } - $result = array_map( - fn ($a, $b) => $a + $b, - $this->components, - $other->components - ); - - return new static($result); + $result = []; + $b = $other->components; + foreach ($this->components as $i => $a) { + $result[$i] = $a + $b[$i]; + } + return new static($result, true); } public function subtract(self $other): self @@ -77,23 +92,21 @@ public function subtract(self $other): self throw new InvalidArgumentException("Cannot subtract vectors with different dimensions"); } - $result = array_map( - fn ($a, $b) => $a - $b, - $this->components, - $other->components - ); - - return new static($result); + $result = []; + $b = $other->components; + foreach ($this->components as $i => $a) { + $result[$i] = $a - $b[$i]; + } + return new static($result, true); } public function scale(float $scalar): self { - $result = array_map( - fn ($component) => $component * $scalar, - $this->components - ); - - return new static($result); + $result = []; + foreach ($this->components as $i => $c) { + $result[$i] = $c * $scalar; + } + return new static($result, true); } public function getComponent(int $index): float @@ -119,13 +132,13 @@ public function distance(self $other): float throw new InvalidArgumentException("Cannot calculate distance between vectors with different dimensions"); } - $squaredDiff = array_map( - fn ($a, $b) => ($a - $b) ** 2, - $this->components, - $other->components - ); - - return sqrt(array_sum($squaredDiff)); + $sum = 0.0; + $b = $other->components; + foreach ($this->components as $i => $a) { + $diff = $a - $b[$i]; + $sum += $diff * $diff; + } + return sqrt($sum); } abstract protected function validateComponents(array $components): void; diff --git a/src/Attributes/Validator.php b/src/Attributes/Validator.php index a3088c3..134e4d7 100644 --- a/src/Attributes/Validator.php +++ b/src/Attributes/Validator.php @@ -14,14 +14,27 @@ final class Validator { /** - * Validate a property value against its attributes + * Cache of parsed attribute instances keyed by "Class::property". + * + * Reflection-driven attribute parsing is the dominant cost on this hot + * path (newInstance() per attribute, per call). Properties don't change + * structure at runtime, so we instantiate once and reuse. + * + * @var array> + */ + private static array $cache = []; + + /** + * Validate a property value against its attributes. */ public static function validateProperty( mixed $value, ReflectionProperty $property ): void { - foreach ($property->getAttributes() as $attribute) { - $instance = $attribute->newInstance(); + $key = $property->class . '::' . $property->name; + $instances = self::$cache[$key] ??= self::compileAttributes($property); + + foreach ($instances as $instance) { match (true) { $instance instanceof Range => self::validateRange($value, $instance), $instance instanceof Email => self::validateEmail($value), @@ -35,6 +48,27 @@ public static function validateProperty( } } + /** + * @return list + */ + private static function compileAttributes(ReflectionProperty $property): array + { + $instances = []; + foreach ($property->getAttributes() as $attribute) { + $instances[] = $attribute->newInstance(); + } + return $instances; + } + + /** + * Clear the attribute cache. Useful for long-running processes that + * reload classes (rare) or for tests that want a clean slate. + */ + public static function clearCache(): void + { + self::$cache = []; + } + private static function validateRange(mixed $value, Range $range): void { if (!is_numeric($value)) { diff --git a/src/Composite/Struct/AdvancedStruct.php b/src/Composite/Struct/AdvancedStruct.php index fc46a79..e48929b 100644 --- a/src/Composite/Struct/AdvancedStruct.php +++ b/src/Composite/Struct/AdvancedStruct.php @@ -16,17 +16,36 @@ public function __construct(array $schema, array $values = []) { $this->schema = $schema; foreach ($schema as $field => $def) { - $alias = $def['alias'] ?? $field; $type = $def['type'] ?? 'mixed'; $nullable = $def['nullable'] ?? false; $default = $def['default'] ?? null; $rules = $def['rules'] ?? []; - $value = $values[$field] ?? $values[$alias] ?? $default; - if ($value === null && !$nullable && $default === null && !array_key_exists($field, $values)) { - throw new InvalidArgumentException("Field '$field' is required and has no value"); + + // Resolve value: explicit field, then alias, then default. + if (isset($values[$field])) { + $value = $values[$field]; + } elseif (array_key_exists($field, $values)) { + $value = null; + } elseif (isset($def['alias']) && array_key_exists($def['alias'], $values)) { + $value = $values[$def['alias']]; + } else { + $value = $default; + if ($value === null && !$nullable && $default === null) { + throw new InvalidArgumentException("Field '$field' is required and has no value"); + } } + if ($value !== null) { - $this->validateField($field, $value, $type, $rules, $nullable); + if ($type !== 'mixed' && !self::isValidType($value, $type)) { + throw new InvalidArgumentException("Field '$field' must be of type $type"); + } + if ($rules !== []) { + foreach ($rules as $rule) { + if (is_callable($rule) && !$rule($value)) { + throw new ValidationException("Validation failed for field '$field'"); + } + } + } } $this->data[$field] = $value; } @@ -37,30 +56,29 @@ protected function validateField(string $field, $value, $type, array $rules, boo if ($value === null && $nullable) { return; } - // Type check - if ($type !== 'mixed' && !$this->isValidType($value, $type)) { + if ($type !== 'mixed' && !self::isValidType($value, $type)) { throw new InvalidArgumentException("Field '$field' must be of type $type"); } - // Rules - foreach ($rules as $rule) { - if (is_callable($rule)) { - if (!$rule($value)) { + if ($rules !== []) { + foreach ($rules as $rule) { + if (is_callable($rule) && !$rule($value)) { throw new ValidationException("Validation failed for field '$field'"); } } } } - protected function isValidType($value, $type): bool + protected static function isValidType(mixed $value, string $type): bool { - if ($type === 'int' || $type === 'integer') return is_int($value); - if ($type === 'float' || $type === 'double') return is_float($value); - if ($type === 'string') return is_string($value); - if ($type === 'bool' || $type === 'boolean') return is_bool($value); - if ($type === 'array') return is_array($value); - if ($type === 'object') return is_object($value); - if (class_exists($type)) return $value instanceof $type; - return true; + return match ($type) { + 'int', 'integer' => is_int($value), + 'float', 'double' => is_float($value), + 'string' => is_string($value), + 'bool', 'boolean' => is_bool($value), + 'array' => is_array($value), + 'object' => is_object($value), + default => class_exists($type) ? $value instanceof $type : true, + }; } public function get(string $field) diff --git a/src/Composite/Struct/CompiledSchema.php b/src/Composite/Struct/CompiledSchema.php new file mode 100644 index 0000000..b81bd85 --- /dev/null +++ b/src/Composite/Struct/CompiledSchema.php @@ -0,0 +1,72 @@ + 'type'] format, computing the "required" + * flag) is expensive enough to dominate Struct construction for small payloads. + * Compile once, construct many. + * + * Typical use: + * + * $compiled = CompiledSchema::compile($schema); + * foreach ($rows as $row) { + * $structs[] = new Struct($compiled, $row); + * } + * + * Immutable; safe to share between threads/requests/coroutines. + */ +final class CompiledSchema +{ + /** + * Per-field tuple: [type, nullable, default, rules, alias, required]. + * + * @var array + */ + public readonly array $fields; + + /** + * Original (post-BC-conversion) schema, kept so Struct::toArray and + * friends that walk the schema can still see the user's definitions. + * + * @var array + */ + public readonly array $original; + + private function __construct(array $fields, array $original) + { + $this->fields = $fields; + $this->original = $original; + } + + public static function compile(array $schema): self + { + // Legacy format detection: ['id' => 'int', ...] -> wrap each entry. + $firstKey = array_key_first($schema); + if ($firstKey !== null && is_string($schema[$firstKey])) { + $newSchema = []; + foreach ($schema as $field => $type) { + $newSchema[$field] = ['type' => $type, 'nullable' => true]; + } + $schema = $newSchema; + } + + $compiled = []; + foreach ($schema as $name => $def) { + $type = $def['type'] ?? 'mixed'; + $nullable = $def['nullable'] ?? false; + $default = $def['default'] ?? null; + $rules = $def['rules'] ?? []; + $alias = $def['alias'] ?? null; + $required = !$nullable && $default === null; + $compiled[$name] = [$type, $nullable, $default, $rules, $alias, $required]; + } + + return new self($compiled, $schema); + } +} diff --git a/src/Composite/Struct/ImmutableStruct.php b/src/Composite/Struct/ImmutableStruct.php index d33c0e7..1fd53f9 100644 --- a/src/Composite/Struct/ImmutableStruct.php +++ b/src/Composite/Struct/ImmutableStruct.php @@ -405,30 +405,37 @@ private function setInitialValues(array $initialValues): void private function validateValue(string $name, mixed $value): void { $type = $this->fields[$name]['type']; - $actualType = get_debug_type($value); - // Handle nullable types - if ($this->isNullable($type) && $value === null) { + + // Inline nullable detection: str_starts_with($type, '?') without the call. + $nullable = ($type !== '' && $type[0] === '?'); + if ($nullable && $value === null) { return; } - $baseType = $this->stripNullable($type); + $baseType = $nullable ? substr($type, 1) : $type; + // Handle nested structs if (is_subclass_of($baseType, StructInterface::class)) { if (!($value instanceof $baseType)) { + throw new InvalidArgumentException( + "Field '$name' expects type '$type', but got '" . get_debug_type($value) . "'" + ); + } + } else { + // Handle primitive types + $actualType = get_debug_type($value); + if ($actualType !== $baseType && !is_subclass_of($value, $baseType)) { throw new InvalidArgumentException( "Field '$name' expects type '$type', but got '$actualType'" ); } - return; - } - // Handle primitive types - if ($actualType !== $baseType && !is_subclass_of($value, $baseType)) { - throw new InvalidArgumentException( - "Field '$name' expects type '$type', but got '$actualType'" - ); } - // Apply validation rules - foreach ($this->fields[$name]['rules'] as $rule) { - $rule->validate($value, $name); + + // Apply validation rules — skip the loop entirely when there are none. + $rules = $this->fields[$name]['rules']; + if ($rules !== []) { + foreach ($rules as $rule) { + $rule->validate($value, $name); + } } } diff --git a/src/Composite/Struct/Struct.php b/src/Composite/Struct/Struct.php index 4cceb65..daf39fe 100644 --- a/src/Composite/Struct/Struct.php +++ b/src/Composite/Struct/Struct.php @@ -12,11 +12,24 @@ class Struct protected array $data = []; protected array $schema = []; - public function __construct(array $schema, array $values = []) + /** + * @param array|CompiledSchema $schema Raw schema array, or a pre-compiled + * schema. The compiled form skips the + * per-construction normalisation work + * and is faster when the same schema + * is used for many instances. + */ + public function __construct(array|CompiledSchema $schema, array $values = []) { - // Backward compatibility: convert old format ['id' => 'int', ...] to new format - $first = reset($schema); - if (is_string($first)) { + if ($schema instanceof CompiledSchema) { + $this->initFromCompiled($schema, $values); + return; + } + + // Raw-array path: skip the intermediate CompiledSchema allocation + // for one-shot callers. + $firstKey = array_key_first($schema); + if ($firstKey !== null && is_string($schema[$firstKey])) { $newSchema = []; foreach ($schema as $field => $type) { $newSchema[$field] = ['type' => $type, 'nullable' => true]; @@ -25,17 +38,72 @@ public function __construct(array $schema, array $values = []) } $this->schema = $schema; foreach ($schema as $field => $def) { - $alias = $def['alias'] ?? $field; $type = $def['type'] ?? 'mixed'; $nullable = $def['nullable'] ?? false; $default = $def['default'] ?? null; $rules = $def['rules'] ?? []; - $value = $values[$field] ?? $values[$alias] ?? $default; - if ($value === null && !$nullable && $default === null && !array_key_exists($field, $values)) { + + if (isset($values[$field])) { + $value = $values[$field]; + } elseif (array_key_exists($field, $values)) { + $value = null; + } elseif (isset($def['alias']) && array_key_exists($def['alias'], $values)) { + $value = $values[$def['alias']]; + } else { + $value = $default; + if ($value === null && !$nullable && $default === null) { + throw new InvalidArgumentException("Field '$field' is required and has no value"); + } + } + + if ($value !== null) { + if ($type !== 'mixed' && !self::isValidType($value, $type)) { + throw new InvalidArgumentException("Field '$field' must be of type $type"); + } + if ($rules !== []) { + foreach ($rules as $rule) { + if (is_callable($rule) && !$rule($value)) { + throw new ValidationException("Validation failed for field '$field'"); + } + } + } + } + $this->data[$field] = $value; + } + } + + private function initFromCompiled(CompiledSchema $compiled, array $values): void + { + $this->schema = $compiled->original; + foreach ($compiled->fields as $field => $def) { + // $def is [type, nullable, default, rules, alias, required] + $alias = $def[4]; + + if (isset($values[$field])) { + $value = $values[$field]; + } elseif (array_key_exists($field, $values)) { + $value = null; + } elseif ($alias !== null && array_key_exists($alias, $values)) { + $value = $values[$alias]; + } elseif ($def[5]) { // required throw new InvalidArgumentException("Field '$field' is required and has no value"); + } else { + $value = $def[2]; // default } + if ($value !== null) { - $this->validateField($field, $value, $type, $rules, $nullable); + $type = $def[0]; + if ($type !== 'mixed' && !self::isValidType($value, $type)) { + throw new InvalidArgumentException("Field '$field' must be of type $type"); + } + $rules = $def[3]; + if ($rules !== []) { + foreach ($rules as $rule) { + if (is_callable($rule) && !$rule($value)) { + throw new ValidationException("Validation failed for field '$field'"); + } + } + } } $this->data[$field] = $value; } @@ -46,26 +114,34 @@ protected function validateField(string $field, mixed $value, string $type, arra if ($value === null && $nullable) { return; } - // Type check - if ($type !== 'mixed' && !$this->isValidType($value, $type)) { + if ($type !== 'mixed' && !self::isValidType($value, $type)) { throw new InvalidArgumentException("Field '$field' must be of type $type"); } - // Rules - if (!array_all($rules, fn($rule) => !is_callable($rule) || $rule($value))) { - throw new ValidationException("Validation failed for field '$field'"); + if ($rules !== []) { + foreach ($rules as $rule) { + if (is_callable($rule) && !$rule($value)) { + throw new ValidationException("Validation failed for field '$field'"); + } + } } } - protected function isValidType(mixed $value, string $type): bool + /** + * Fast type check. Static so it doesn't pay a $this dispatch. + * Match on the common scalar/array types first; only fall back to + * class_exists() when the type clearly isn't a builtin. + */ + protected static function isValidType(mixed $value, string $type): bool { - if ($type === 'int' || $type === 'integer') return is_int($value); - if ($type === 'float' || $type === 'double') return is_float($value); - if ($type === 'string') return is_string($value); - if ($type === 'bool' || $type === 'boolean') return is_bool($value); - if ($type === 'array') return is_array($value); - if ($type === 'object') return is_object($value); - if (class_exists($type)) return $value instanceof $type; - return true; + return match ($type) { + 'int', 'integer' => is_int($value), + 'float', 'double' => is_float($value), + 'string' => is_string($value), + 'bool', 'boolean' => is_bool($value), + 'array' => is_array($value), + 'object' => is_object($value), + default => class_exists($type) ? $value instanceof $type : true, + }; } public function get(string $field): mixed diff --git a/src/Composite/Vector/Vec2.php b/src/Composite/Vector/Vec2.php index db61b60..19ee546 100644 --- a/src/Composite/Vector/Vec2.php +++ b/src/Composite/Vector/Vec2.php @@ -57,4 +57,68 @@ protected function validateComponents(array $components): void $this->validateComponentCount($components, 2); $this->validateNumericComponents($components); } + + public function add(AbstractVector $other): self + { + if (!$other instanceof self) { + throw new InvalidArgumentException("Cannot add vectors with different dimensions"); + } + $a = $this->components; + $b = $other->components; + return new self([$a[0] + $b[0], $a[1] + $b[1]], true); + } + + public function subtract(AbstractVector $other): self + { + if (!$other instanceof self) { + throw new InvalidArgumentException("Cannot subtract vectors with different dimensions"); + } + $a = $this->components; + $b = $other->components; + return new self([$a[0] - $b[0], $a[1] - $b[1]], true); + } + + public function scale(float $scalar): self + { + $a = $this->components; + return new self([$a[0] * $scalar, $a[1] * $scalar], true); + } + + public function dot(AbstractVector $other): float + { + if (!$other instanceof self) { + throw new InvalidArgumentException("Cannot calculate dot product of vectors with different dimensions"); + } + $a = $this->components; + $b = $other->components; + return $a[0] * $b[0] + $a[1] * $b[1]; + } + + public function magnitude(): float + { + $a = $this->components; + return sqrt($a[0] * $a[0] + $a[1] * $a[1]); + } + + public function distance(AbstractVector $other): float + { + if (!$other instanceof self) { + throw new InvalidArgumentException("Cannot calculate distance between vectors with different dimensions"); + } + $a = $this->components; + $b = $other->components; + $dx = $a[0] - $b[0]; + $dy = $a[1] - $b[1]; + return sqrt($dx * $dx + $dy * $dy); + } + + public function normalize(): self + { + $a = $this->components; + $mag = sqrt($a[0] * $a[0] + $a[1] * $a[1]); + if ($mag === 0.0) { + throw new InvalidArgumentException("Cannot normalize a zero vector"); + } + return new self([$a[0] / $mag, $a[1] / $mag], true); + } } diff --git a/src/Composite/Vector/Vec3.php b/src/Composite/Vector/Vec3.php index 3276890..7666e34 100644 --- a/src/Composite/Vector/Vec3.php +++ b/src/Composite/Vector/Vec3.php @@ -71,4 +71,69 @@ protected function validateComponents(array $components): void $this->validateComponentCount($components, 3); $this->validateNumericComponents($components); } + + public function add(AbstractVector $other): self + { + if (!$other instanceof self) { + throw new InvalidArgumentException("Cannot add vectors with different dimensions"); + } + $a = $this->components; + $b = $other->components; + return new self([$a[0] + $b[0], $a[1] + $b[1], $a[2] + $b[2]], true); + } + + public function subtract(AbstractVector $other): self + { + if (!$other instanceof self) { + throw new InvalidArgumentException("Cannot subtract vectors with different dimensions"); + } + $a = $this->components; + $b = $other->components; + return new self([$a[0] - $b[0], $a[1] - $b[1], $a[2] - $b[2]], true); + } + + public function scale(float $scalar): self + { + $a = $this->components; + return new self([$a[0] * $scalar, $a[1] * $scalar, $a[2] * $scalar], true); + } + + public function dot(AbstractVector $other): float + { + if (!$other instanceof self) { + throw new InvalidArgumentException("Cannot calculate dot product of vectors with different dimensions"); + } + $a = $this->components; + $b = $other->components; + return $a[0] * $b[0] + $a[1] * $b[1] + $a[2] * $b[2]; + } + + public function magnitude(): float + { + $a = $this->components; + return sqrt($a[0] * $a[0] + $a[1] * $a[1] + $a[2] * $a[2]); + } + + public function distance(AbstractVector $other): float + { + if (!$other instanceof self) { + throw new InvalidArgumentException("Cannot calculate distance between vectors with different dimensions"); + } + $a = $this->components; + $b = $other->components; + $dx = $a[0] - $b[0]; + $dy = $a[1] - $b[1]; + $dz = $a[2] - $b[2]; + return sqrt($dx * $dx + $dy * $dy + $dz * $dz); + } + + public function normalize(): self + { + $a = $this->components; + $mag = sqrt($a[0] * $a[0] + $a[1] * $a[1] + $a[2] * $a[2]); + if ($mag === 0.0) { + throw new InvalidArgumentException("Cannot normalize a zero vector"); + } + return new self([$a[0] / $mag, $a[1] / $mag, $a[2] / $mag], true); + } } diff --git a/src/Composite/Vector/Vec4.php b/src/Composite/Vector/Vec4.php index 596ddd8..edfc58a 100644 --- a/src/Composite/Vector/Vec4.php +++ b/src/Composite/Vector/Vec4.php @@ -72,4 +72,70 @@ protected function validateComponents(array $components): void $this->validateComponentCount($components, 4); $this->validateNumericComponents($components); } + + public function add(AbstractVector $other): self + { + if (!$other instanceof self) { + throw new InvalidArgumentException("Cannot add vectors with different dimensions"); + } + $a = $this->components; + $b = $other->components; + return new self([$a[0] + $b[0], $a[1] + $b[1], $a[2] + $b[2], $a[3] + $b[3]], true); + } + + public function subtract(AbstractVector $other): self + { + if (!$other instanceof self) { + throw new InvalidArgumentException("Cannot subtract vectors with different dimensions"); + } + $a = $this->components; + $b = $other->components; + return new self([$a[0] - $b[0], $a[1] - $b[1], $a[2] - $b[2], $a[3] - $b[3]], true); + } + + public function scale(float $scalar): self + { + $a = $this->components; + return new self([$a[0] * $scalar, $a[1] * $scalar, $a[2] * $scalar, $a[3] * $scalar], true); + } + + public function dot(AbstractVector $other): float + { + if (!$other instanceof self) { + throw new InvalidArgumentException("Cannot calculate dot product of vectors with different dimensions"); + } + $a = $this->components; + $b = $other->components; + return $a[0] * $b[0] + $a[1] * $b[1] + $a[2] * $b[2] + $a[3] * $b[3]; + } + + public function magnitude(): float + { + $a = $this->components; + return sqrt($a[0] * $a[0] + $a[1] * $a[1] + $a[2] * $a[2] + $a[3] * $a[3]); + } + + public function distance(AbstractVector $other): float + { + if (!$other instanceof self) { + throw new InvalidArgumentException("Cannot calculate distance between vectors with different dimensions"); + } + $a = $this->components; + $b = $other->components; + $dx = $a[0] - $b[0]; + $dy = $a[1] - $b[1]; + $dz = $a[2] - $b[2]; + $dw = $a[3] - $b[3]; + return sqrt($dx * $dx + $dy * $dy + $dz * $dz + $dw * $dw); + } + + public function normalize(): self + { + $a = $this->components; + $mag = sqrt($a[0] * $a[0] + $a[1] * $a[1] + $a[2] * $a[2] + $a[3] * $a[3]); + if ($mag === 0.0) { + throw new InvalidArgumentException("Cannot normalize a zero vector"); + } + return new self([$a[0] / $mag, $a[1] / $mag, $a[2] / $mag, $a[3] / $mag], true); + } }