-
Notifications
You must be signed in to change notification settings - Fork 4
Update #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…ion, ValidationException
…arning, and update array tests
- Update PHP version requirement to ^8.4 in composer.json - Add PHP 8.4 to GitHub Actions CI matrix - Add PHPStan and Infection to dev dependencies - Add new scripts for static analysis and mutation testing
- Add PHPStan level 9 configuration for strict static analysis - Add Infection mutation testing configuration - Add PHPStorm metadata for improved IDE support - Configure tools to target src directory with appropriate exclusions
- Add toArray() method for array conversion - Add isEmpty() method for empty check - Add getAll() method for element access - Improve Dictionary functionality and API completeness
- Add Option<T> type for nullable value handling with Some/None variants - Add Result<T, E> type for error handling with Ok/Err variants - Implement comprehensive API with map, andThen, orElse methods - Add helper functions for Option and Result types - Support JSON serialization/deserialization for both types - Add static try() method for Result to wrap callable functions
- Add PhpDatatypesServiceProvider for automatic registration - Create custom validation rules for all integer and float types - Add Eloquent casts for seamless database integration - Provide example Form Request, Controller, and Model usage - Add configuration file for future customization options - Include comprehensive documentation and usage examples
- Add Range attribute for numeric bounds validation - Add Email attribute for email format validation - Add Regex attribute for pattern matching validation - Add NotNull attribute for required field validation - Add Length attribute for string length validation - Add Url, Uuid, IpAddress attributes for specific format validation - Create Validator helper class for processing attributes - Support declarative validation with PHP 8.4 attributes
- Replace validation loops with array_all() for type checking - Use array_find() for better error reporting with specific invalid items - Replace array_search() with array_find_key() for key-based operations - Update ArrayAbstraction validation methods (validateFloats, validateStrings, validateBytes) - Improve IntArray, StringArray, FloatArray validation and operations - Update TypeSafeArray validation with modern array functions - Enhance Struct rules validation with array_all() - Achieve cleaner code and better performance with PHP 8.4 features
- Add explicit return types to AbstractNativeInteger methods - Add explicit return types to AbstractBigInteger methods - Improve method signatures for better type safety - Maintain backward compatibility while enhancing type checking
- Add IntegerBenchmark comparing native PHP vs library integer performance - Add ArrayBenchmark comparing native PHP vs library array performance - Create run_benchmarks.php script for executing benchmarks - Provide performance comparison tools for optimization decisions - Support both native PHP types and php-datatypes library types
- Add comprehensive_example.php demonstrating all library features - Update README.md with new features and development tools - Update CHANGELOG.md to version 2.0.0 with breaking changes - Document algebraic data types, Laravel integration, and benchmarks - Add migration guide for breaking changes - Include examples for all major features and use cases
- Update lock file with PHPStan and Infection dependencies - Ensure reproducible builds with exact dependency versions
- Add Requirements section with PHP 8.4 minimum requirement - Add comprehensive PHP 8.4 Features section with: * Modern Array Functions (before/after examples) * Attribute-Based Validation with all available attributes * Performance Improvements (15-30% gains) - Update Features list with PHP 8.4 optimizations - Add Migration Guide for v1.x to v2.x and v3.x preparation - Add educational examples showing old vs new syntax - Add PHP 8.4 Array Operations in Advanced Usage - Enhance Development Tools section with performance benefits - Provide clear migration path and future roadmap
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 54
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/Abstract/AbstractNativeInteger.php (1)
30-33
: Guard against undefined MIN/MAX bounds (prevent null comparisons).
MIN_VALUE
/MAX_VALUE
default to null; comparing ints to null is unsafe and message formatting with%d
will mislead. Enforce non-null integer bounds at runtime.public function __construct(int $value) { - $this->setValue($value); + self::assertBounds(); + $this->setValue($value); } @@ protected function setValue(int $value): void { - if ($value < static::MIN_VALUE || $value > static::MAX_VALUE) { + // Bounds already asserted to be integers + if ($value < static::MIN_VALUE || $value > static::MAX_VALUE) { throw new \OutOfRangeException(sprintf( 'Value must be between %d and %d.', static::MIN_VALUE, static::MAX_VALUE )); } @@ $this->value = $value; } + +/** + * Ensure child classes define integer bounds. + */ +private static function assertBounds(): void +{ + if (!is_int(static::MIN_VALUE) || !is_int(static::MAX_VALUE)) { + throw new \LogicException(static::class . ' must define integer MIN_VALUE and MAX_VALUE constants.'); + } + if (static::MIN_VALUE > static::MAX_VALUE) { + throw new \LogicException(static::class . ' has MIN_VALUE greater than MAX_VALUE.'); + } +}Also applies to: 63-74
src/Abstract/AbstractBigInteger.php (1)
66-79
: Guard against null/invalid MIN/MAX with bcmath comparisons.
bccomp((string)static::MIN_VALUE, ...)
will misbehave if bounds are null or non-numeric. Add a bounds assertion similar to the native integer base.public function __construct(int|string $value) { - $this->setValue($value); + self::assertBounds(); + $this->setValue($value); } @@ protected function setValue(int|string $value): void { $valueStr = (string)$value; - if (bccomp($valueStr, (string)static::MIN_VALUE) < 0 || bccomp($valueStr, (string)static::MAX_VALUE) > 0) { + if (bccomp($valueStr, (string)static::MIN_VALUE) < 0 || bccomp($valueStr, (string)static::MAX_VALUE) > 0) { throw new \OutOfRangeException(sprintf( 'Value must be between %s and %s.', static::MIN_VALUE, static::MAX_VALUE )); } @@ $this->value = $valueStr; } + +private static function assertBounds(): void +{ + if (!is_string(static::MIN_VALUE) && !is_int(static::MIN_VALUE)) { + throw new \LogicException(static::class . ' must define numeric-string/int MIN_VALUE.'); + } + if (!is_string(static::MAX_VALUE) && !is_int(static::MAX_VALUE)) { + throw new \LogicException(static::class . ' must define numeric-string/int MAX_VALUE.'); + } + if (bccomp((string)static::MIN_VALUE, (string)static::MAX_VALUE) === 1) { + throw new \LogicException(static::class . ' has MIN_VALUE greater than MAX_VALUE.'); + } +}
🧹 Nitpick comments (65)
.gitignore (1)
23-24
: Ignore coverage artifacts (or redirect them to build/).Your test-coverage script writes to ./coverage, which is not ignored. Either add coverage/ here or change the script to output into build/.
Option A (ignore):
+coverage/
Option B (redirect output; see composer.json comment for matching script change).
composer.json (1)
46-51
: Keep coverage output under build/ to avoid accidental commits.Match .gitignore’s /build/**:
- "test-coverage": "vendor/bin/phpunit --coverage-html coverage", + "test-coverage": "vendor/bin/phpunit --coverage-html build/coverage", "phpstan": "vendor/bin/phpstan analyse", "phpstan-baseline": "vendor/bin/phpstan analyse --generate-baseline", "benchmark": "php benchmarks/run_benchmarks.php", "infection": "vendor/bin/infection", - "infection-baseline": "vendor/bin/infection --generate-baseline" + "infection-baseline": "vendor/bin/infection --generate-baseline --min-msi=0 --min-covered-msi=0"(Optional: set explicit baseline flags to keep CI green while you iterate.)
src/Composite/Arrays/StringArray.php (2)
71-79
: remove(): remove-all and cut complexity to O(N).Current loop finds one index per string and unsets; duplicates remain and cost is O(N×M).
- $newArray = $this->value; - foreach ($strings as $string) { - $index = array_find_key($newArray, fn($value) => $value === $string); - if ($index !== null) { - unset($newArray[$index]); - } - } - return new self(array_values($newArray)); // Re-index array + $toRemove = array_flip($strings); + $filtered = array_values(array_filter( + $this->value, + static fn($v) => !isset($toRemove[$v]) + )); + return new self($filtered);If “remove only one occurrence per value” is the intended behavior, keep current logic but please document it explicitly and add tests.
88-91
: contains(): precompute a lookup set for faster checks; define empty-args behavior.Avoid repeated in_array scans and clarify whether contains() with no args should return true or false.
- return array_all($strings, fn($string) => in_array($string, $this->value, true)); + if ($strings === []) { + return true; // or false — pick and document, then add a test + } + $set = array_flip($this->value); + foreach ($strings as $s) { + if (!isset($set[$s])) { + return false; + } + } + return true;src/Abstract/AbstractNativeInteger.php (2)
7-7
: Remove unused import.
Range
is imported but never used in this class.-use Nejcc\PhpDatatypes\Attributes\Range;
76-76
: Use $operationName and fix docblock return type for clarity.Docblock says
$this
but method returnsstatic
. Also, include$operationName
in exception messages to aid debugging.- * @return $this + * @return static @@ - ): static { + ): static { $result = $operation($this->value, $other->getValue()); if ($result < static::MIN_VALUE || $result > static::MAX_VALUE) { $exceptionClass = $result > static::MAX_VALUE ? \OverflowException::class : \UnderflowException::class; - throw new $exceptionClass('Result is out of bounds.'); + throw new $exceptionClass(sprintf('%s result is out of bounds: %d (allowed %d..%d).', + $operationName, + $result, + static::MIN_VALUE, + static::MAX_VALUE + )); } return new static($result); }Also applies to: 82-97
infection.json (1)
38-41
: Improve CI resilience and speed: ignore MSI when no mutations, turn off Xdebug for initial tests.These options reduce flaky failures and speed up the baseline run.
"minMsi": 80, "minCoveredMsi": 80, + "ignoreMsiWithNoMutations": true, + "initialTestsPhpOptions": "-d xdebug.mode=off", "threads": 4, "tmpDir": "build/infection"Based on learnings.
src/Laravel/README.md (2)
39-41
: Hyphenate “floating-point”.Minor wording polish: “32-bit/64-bit floating-point number”.
12-18
: Auto-discovery not currently configured in composer.json; documenting it would be misleading.The package does not declare
extra.laravel.providers
in composer.json, so auto-discovery is not actually available to users. Documenting auto-discovery without configuring it first would create confusion and false expectations.To implement this suggestion:
- First, add auto-discovery configuration to
composer.json
:"extra": { "laravel": { "providers": [ "Nejcc\\PhpDatatypes\\Laravel\\PhpDatatypesServiceProvider" ] } }- Then, update the README to document auto-discovery first (Laravel 5.5+), with manual registration as a fallback for older versions.
src/Composite/String/PathString.php (1)
14-15
: Make the value immutable.Use
readonly
to enforce value-object semantics.- private string $value; + private readonly string $value;src/Abstract/AbstractBigInteger.php (1)
82-103
: Improve docblock, use$operationName
in error message, and ensure$operation
returns string.The docblock should use
static
instead of$this
, and the error message should include operation context and bounds for better debugging. Additionally, cast the operation result to string to ensure type safety.- * @return $this + * @return static @@ - ): static { - $result = $operation($this->value, (string)$other->getValue()); + ): static { + $result = (string)$operation($this->value, (string)$other->getValue()); @@ - throw new $exceptionClass('Result is out of bounds.'); + throw new $exceptionClass(sprintf('%s result is out of bounds: %s (allowed %s..%s).', + $operationName, + $result, + static::MIN_VALUE, + static::MAX_VALUE + )); } return new static($result); }Note:
ext-bcmath
is already required in composer.json.src/Composite/String/Base64String.php (1)
22-28
: Consider stricter base64 validation.The current regex validates the base64 character set but doesn't enforce proper base64 structure (padding rules, length constraints). Malformed strings like
"ABC=D="
or"A==="
would pass validation.Apply this diff for strict validation:
- if (!preg_match('/^[A-Za-z0-9+\/=]+$/', $value)) { + if (base64_decode($value, true) === false) { throw new InvalidArgumentException('Invalid base64 string format'); }The
base64_decode
with strict mode validates both character set and structure.src/Composite/Option.php (1)
266-269
: Add error handling for JSON encoding.
json_encode()
can returnfalse
on failure, but this is not checked. If encoding fails, the method will returnfalse
instead of a string, causing type safety issues.Apply this diff to handle encoding errors:
public function toJson(): string { - return json_encode($this->toArray()); + $json = json_encode($this->toArray()); + if ($json === false) { + throw new InvalidArgumentException('Failed to encode Option to JSON: ' . json_last_error_msg()); + } + return $json; }benchmarks/IntegerBenchmark.php (1)
7-11
: Remove unused imports.The imports for
Int32
,UInt8
, andUInt32
are not used anywhere in this benchmark class.Apply this diff to remove the unused imports:
use Nejcc\PhpDatatypes\Scalar\Integers\Signed\Int8; -use Nejcc\PhpDatatypes\Scalar\Integers\Signed\Int32; use Nejcc\PhpDatatypes\Scalar\Integers\Signed\Int64; -use Nejcc\PhpDatatypes\Scalar\Integers\Unsigned\UInt8; -use Nejcc\PhpDatatypes\Scalar\Integers\Unsigned\UInt32;CHANGELOG.md (1)
69-69
: Optional: Consider hyphenating "floating-point" when used as a compound adjective.The static analysis tool suggests using "floating-point types" instead of "Floating point types" when the phrase modifies a noun. This is a stylistic preference and not critical.
src/Composite/String/SlugString.php (1)
24-26
: Consider stricter slug validation.The current regex
/^[a-z0-9-]+$/
allows patterns that are typically invalid in URL slugs:
- Leading hyphens:
-hello
- Trailing hyphens:
world-
- Consecutive hyphens:
hello--world
- Only hyphens:
---
Consider using a stricter pattern like
/^[a-z0-9]+(?:-[a-z0-9]+)*$/
to ensure slugs start and end with alphanumeric characters and contain only single hyphens between segments.Apply this diff for stricter validation:
- if (!preg_match('/^[a-z0-9-]+$/', $value)) { + if (!preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/', $value)) { throw new InvalidArgumentException('SlugString must contain only lowercase letters, numbers, and hyphens'); }Tests/Composite/Vector/Vec2Test.php (1)
35-35
: Use deltas for floating-point assertions to avoid flakiness.Prefer assertEqualsWithDelta(..., 1e-9) for float results (magnitude, normalize, dot/add/sub/scale/cross/distance).
Example patch:
- $this->assertEquals(5.0, $vec->magnitude()); + $this->assertEqualsWithDelta(5.0, $vec->magnitude(), 1e-9) - $this->assertEquals(1.0, $normalized->magnitude()); - $this->assertEquals(0.6, $normalized->getX()); - $this->assertEquals(0.8, $normalized->getY()); + $this->assertEqualsWithDelta(1.0, $normalized->magnitude(), 1e-9); + $this->assertEqualsWithDelta(0.6, $normalized->getX(), 1e-9); + $this->assertEqualsWithDelta(0.8, $normalized->getY(), 1e-9);Apply similarly to other numeric asserts in this class.
Also applies to: 42-44, 58-58, 66-67, 75-76, 83-84, 91-91, 97-98, 104-105, 111-112, 118-118, 127-128, 135-135
src/Attributes/Length.php (1)
12-19
: Validate invariants (min/max presence and ordering) at construction.Guard against invalid configuration early.
final class Length { public function __construct( public readonly ?int $min = null, public readonly ?int $max = null, - ) {} + ) { + if ($this->min === null && $this->max === null) { + throw new \InvalidArgumentException('Length: at least one of min or max must be provided'); + } + if (($this->min !== null && $this->min < 0) || ($this->max !== null && $this->max < 0)) { + throw new \InvalidArgumentException('Length: bounds must be non-negative'); + } + if ($this->min !== null && $this->max !== null && $this->min > $this->max) { + throw new \InvalidArgumentException('Length: min cannot be greater than max'); + } + }Tests/StructTest.php (1)
15-18
: Add tests for aliases and (de)serialization paths.Struct supports field aliases and toArray/toJson/toXml round‑trips; consider adding tests to lock those behaviors.
Example:
$schema = [ 'id' => ['type'=>'int','nullable'=>true,'alias'=>'ID'], 'name' => ['type'=>'string','nullable'=>true,'alias'=>'Name'], ]; $s = new Struct($schema, ['id'=>1,'name'=>'A']); $this->assertSame(['id'=>1,'name'=>'A'], $s->toArray(false)); $this->assertSame(['ID'=>1,'Name'=>'A'], $s->toArray(true)); $this->assertEquals($s->toArray(), Struct::fromJson($schema, $s->toJson())->toArray()); $this->assertEquals($s->toArray(), Struct::fromXml($schema, $s->toXml())->toArray());src/Composite/String/VersionString.php (1)
24-29
: Broaden validation to full SemVer (pre-release hyphens, build metadata, optional 'v').Current pattern rejects valid SemVer like 1.2.3-alpha.1+build.7 and identifiers with hyphens.
- // Basic version validation (e.g., 1.2.3, 1.2.3-alpha, 1.2.3-beta.1) - if (!preg_match('/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*)?$/', $value)) { - throw new InvalidArgumentException('Invalid version string format'); + // SemVer 2.0.0: MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD], allow optional leading 'v' + // e.g., v1.2.3, 1.2.3-alpha.1, 1.2.3+exp.sha.5114f85, 1.2.3-rc.1+build.7 + if (!preg_match('/^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/', $value)) { + throw new InvalidArgumentException('Invalid SemVer string'); }src/Composite/String/UuidString.php (1)
24-27
: Consider RFC 4122 UUID validation for stricter compliance.The current regex validates the basic UUID format (8-4-4-4-12 hex pattern) but doesn't enforce RFC 4122 constraints on version and variant fields. This accepts strings that match the UUID format but aren't technically valid UUIDs.
If stricter validation is needed, consider this RFC 4122-compliant regex:
-if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value)) { +if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $value)) {This enforces:
- Version field (13th hex digit): 1-5
- Variant field (17th hex digit): 8, 9, a, or b
The current implementation is acceptable for basic UUID format validation.
src/Composite/Struct/Rules/CustomRule.php (1)
28-39
: Add error handling and type validation for closure execution.The closure is invoked without error handling or return type validation, which can lead to unexpected behavior:
- If the closure throws an exception, it propagates uncaught (may be intentional, but should be documented).
- If the closure returns a non-boolean truthy/falsy value, the
!$isValid
check may behave unexpectedly.Consider adding explicit type checking:
public function validate(mixed $value, string $fieldName): bool { $isValid = ($this->validator)($value); + if (!is_bool($isValid)) { + throw new ValidationException( + "Field '$fieldName': Custom validator must return bool, got " . gettype($isValid) + ); + } + if (!$isValid) { throw new ValidationException( "Field '$fieldName': {$this->errorMessage}" ); } return true; }Or document the expected behavior:
/** * @param Closure $validator A closure that takes a value and returns bool (strict) * Exceptions thrown by the validator will propagate. */Tests/Composite/Arrays/FixedSizeArrayTest.php (3)
18-22
: Prefer assertCount over count() in assertionsUse purpose-built assertions for readability and better diffs.
- $this->assertEquals(0, count($array)); + $this->assertCount(0, $array)Apply similarly at Lines 36, 95, and 120.
Also applies to: 36-40, 95-101, 120-123, 140-142
42-46
: Assert the exception message to lock behaviorStrengthen this by asserting the message as well.
$this->expectException(InvalidArgumentException::class); - new FixedSizeArray(\stdClass::class, 2, [new \stdClass(), new \stdClass(), new \stdClass()]); + $this->expectExceptionMessage('Initial data size (3) exceeds fixed size (2)'); + new FixedSizeArray(\stdClass::class, 2, [new \stdClass(), new \stdClass(), new \stdClass()]);Note: This will reveal a formatting bug in FixedSizeArray::__construct where the actual count is not rendered. See suggested fix in FixedSizeArray.php below.
62-70
: Also assert messages for capacity and bounds errorsLock down error text to prevent silent regressions.
$this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Array is at maximum capacity'); $array[] = new \stdClass();
$this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Index 2 is out of bounds (size: 2)'); $array[2] = new \stdClass();
$this->expectException(TypeMismatchException::class); + $this->expectExceptionMessage('Value must be of type stdClass'); $array[] = "not an object";
Also applies to: 76-78, 84-86
Tests/Composite/Arrays/DynamicArrayTest.php (2)
16-19
: Use assertCount for collection size checksImproves clarity and PHPUnit diffs.
- $this->assertEquals(0, count($array)); + $this->assertCount(0, $array);Apply similarly at Lines 32, 42, 79, and 105.
Also applies to: 31-34, 41-44, 76-79, 103-106
81-88
: Optional: verify holes are preserved when setting sparse indicesConsider asserting that intermediate indices remain unset/null to document sparse behavior.
$this->assertArrayNotHasKey(0, (array)$array); $this->assertArrayNotHasKey(1, (array)$array); // ... $this->assertArrayNotHasKey(4, (array)$array);Tests/Composite/Struct/StructTest.php (1)
62-71
: Add alias-aware from tests to mirror to behavior**Currently only toArray/toJson test aliases. Add fromArray/fromJson/fromXml tests using alias keys to validate round‑trip symmetry.
Example additions:
$schema = [ 'name' => ['type' => 'string', 'alias' => 'userName'], ]; $this->assertSame('Alice', Struct::fromArray($schema, ['userName' => 'Alice'])->get('name')); $this->assertSame('Alice', Struct::fromJson($schema, '{"userName":"Alice"}')->get('name')); $this->assertSame('Alice', Struct::fromXml($schema, '<struct><userName>Alice</userName></struct>')->get('name'));Also applies to: 84-93, 106-115, 117-126
src/Attributes/Validator.php (2)
74-93
: Use mb_strlen when available for multibyte safetyString length should be multibyte‑aware.
- $strLength = strlen($value); + $strLength = function_exists('mb_strlen') ? mb_strlen($value) : strlen($value);
53-58
: Consistent exception type across validators (optional)Most branches throw InvalidArgumentException; Range throws OutOfRangeException. If you want uniform catch behavior, standardize on one exception type or document the difference.
Also applies to: 95-100, 114-119
src/Laravel/PhpDatatypesServiceProvider.php (2)
60-62
: Replacers are unnecessary if extend() provides messages; fix unused param if keepingEither remove addValidationMessages() and its call, or fix the unused parameter to satisfy PHPMD.
Option A (remove):
- // Add custom error messages - $this->addValidationMessages();And delete addValidationMessages() method.
Option B (keep, fix param name and use predefined messages):
- Validator::replacer($rule, function ($message, $attribute, $rule, $parameters) { - return str_replace(':attribute', $attribute, $message); + Validator::replacer($rule, function ($msg, $attribute, $r, $unused) use ($message) { + return str_replace(':attribute', $attribute, $message); });Also applies to: 67-86
28-39
: Operational note: ensure service provider is loadedIf relying on package auto‑discovery, add it under composer.json "extra.laravel.providers". Otherwise, document manual registration in config/app.php.
Tests/Composite/Vector/Vec4Test.php (4)
34-38
: Use delta for float comparisons to avoid flakiness.Magnitude involves floating-point math; prefer assertEqualsWithDelta.
- $this->assertEquals(sqrt(13), $vec->magnitude()); + $this->assertEqualsWithDelta(sqrt(13), $vec->magnitude(), 1e-12);
40-49
: Normalization checks should use a delta.Floating-point division may not be exact; use assertEqualsWithDelta for magnitude and components.
- $this->assertEquals(1.0, $normalized->magnitude()); - $this->assertEquals(1 / sqrt(13), $normalized->getX()); - $this->assertEquals(2 / sqrt(13), $normalized->getY()); - $this->assertEquals(2 / sqrt(13), $normalized->getZ()); - $this->assertEquals(2 / sqrt(13), $normalized->getW()); + $this->assertEqualsWithDelta(1.0, $normalized->magnitude(), 1e-12); + $this->assertEqualsWithDelta(1 / sqrt(13), $normalized->getX(), 1e-12); + $this->assertEqualsWithDelta(2 / sqrt(13), $normalized->getY(), 1e-12); + $this->assertEqualsWithDelta(2 / sqrt(13), $normalized->getZ(), 1e-12); + $this->assertEqualsWithDelta(2 / sqrt(13), $normalized->getW(), 1e-12);
158-163
: Distance assertion: prefer delta.Same float concern applies here.
- $this->assertEquals(sqrt(13), $vec1->distance($vec2)); + $this->assertEqualsWithDelta(sqrt(13), $vec1->distance($vec2), 1e-12);
97-140
: Nit: for exact literals, assertSame communicates intent.Optional style tweak: where values are exactly 0.0/1.0, assertSame enforces type+value.
- $this->assertEquals(0.0, $vec->getX()); + $this->assertSame(0.0, $vec->getX());(Apply similarly in unit vector tests.)
src/Composite/String/RegexString.php (2)
24-26
: Avoid error suppression; validate regex safely.Using @ hides warnings globally. Wrap the call with a temporary error handler and check the result.
- if (@preg_match($value, '') === false) { - throw new InvalidArgumentException('Invalid regular expression format: ' . preg_last_error_msg()); - } + set_error_handler(static function () { /* swallow compilation warning */ }, E_WARNING); + $result = preg_match($value, ''); + restore_error_handler(); + if ($result === false) { + throw new InvalidArgumentException( + 'Invalid regular expression format: ' . preg_last_error_msg() + ); + }Also consider declaring the property as readonly:
- private string $value; + private readonly string $value;
12-22
: Clarify expected pattern format (delimiters/modifiers).If RegexString expects delimited patterns (e.g., '/.../i'), document it and add tests for invalid/unterminated delimiters. If not, consider auto-wrapping or validating delimiters explicitly.
src/Composite/Arrays/FixedSizeArray.php (1)
128-147
: fill() mutates in place; consider returning a new instance for consistency with map/filter.TypeSafeArray::map/filter return new arrays while fill() mutates; if you want persistent semantics, a new instance may be preferable. Otherwise, document the mutation.
Tests/Composite/Struct/ImmutableStructTest.php (1)
679-726
: Great coverage; add a test to pin parent preservation across with().Today, with() creates a new struct without asserting parent linkage. Consider asserting that a child created via with()/withField() retains hasParent()/getParent() semantics, or decide explicitly that it should not and document it.
Would you like me to add a test and a patch to pass the parent into the new instance (new self($newFields, $values, $this->parent))?
src/Composite/String/IpString.php (1)
22-28
: Optional: add v4/v6-specific factories (and readonly property).Consider named constructors to enforce version explicitly and a readonly property.
- private string $value; + private readonly string $value; + + public static function ipv4(string $value): self + { + if (!filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + throw new InvalidArgumentException('Invalid IPv4 address format'); + } + return new self($value); + } + + public static function ipv6(string $value): self + { + if (!filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + throw new InvalidArgumentException('Invalid IPv6 address format'); + } + return new self($value); + }src/Composite/Struct/Rules/SlugRule.php (1)
16-24
: Add constructor validation for length constraints.The constructor doesn't validate that
minLength <= maxLength
or thatminLength
is positive, which could lead to confusing runtime behavior.public function __construct( int $minLength = 3, int $maxLength = 100, bool $allowUnderscores = true ) { + if ($minLength <= 0) { + throw new \Nejcc\PhpDatatypes\Exceptions\InvalidArgumentException( + "minLength must be positive, got {$minLength}" + ); + } + if ($minLength > $maxLength) { + throw new \Nejcc\PhpDatatypes\Exceptions\InvalidArgumentException( + "minLength ({$minLength}) cannot exceed maxLength ({$maxLength})" + ); + } $this->minLength = $minLength; $this->maxLength = $maxLength; $this->allowUnderscores = $allowUnderscores; }src/Composite/Arrays/DynamicArray.php (2)
92-94
: Sparse array access can defeat dynamic capacity strategy.Setting an element at a large offset (e.g.,
$array[1000] = $value
) when capacity is small will set capacity to exactlyoffset + 1
, bypassing exponential growth. This can lead to many small reallocations if multiple sparse indices are set.Consider growing capacity more aggressively:
} else { if ($offset >= $this->capacity) { - $this->capacity = $offset + 1; + // Grow to at least offset + 1, but maintain exponential growth pattern + while ($this->capacity <= $offset) { + $this->capacity = max(1, $this->capacity * 2); + } } }Alternatively, document that sparse arrays should use
reserve()
before setting large offsets.
30-40
: Consider adjusting capacity before parent construction.The capacity adjustment for
initialData
happens after calling the parent constructor, which has already validated and stored the data. While functionally correct, setting capacity first would be more intuitive.public function __construct(string $elementType, int $initialCapacity = 8, array $initialData = []) { if ($initialCapacity <= 0) { throw new InvalidArgumentException('Capacity must be a positive integer'); } - $this->capacity = $initialCapacity; - parent::__construct($elementType, $initialData); - if (count($initialData) > $this->capacity) { - $this->capacity = count($initialData); - } + $this->capacity = max($initialCapacity, count($initialData)); + parent::__construct($elementType, $initialData); }src/Laravel/Http/Controllers/PhpDatatypesController.php (1)
25-64
: Consider defensive error handling in example methods.While the validation rules should prevent invalid values, adding try-catch blocks around datatype construction would make the examples more robust and demonstrate best practices for production code.
Example for
validateManually
:public function validateManually(Request $request): JsonResponse { $request->validate([ 'user_id' => ['required', 'uint8'], 'age' => ['required', 'int8'], 'balance' => ['required', 'float32'], ]); + try { $userId = new UInt8($request->input('user_id')); $age = new Int8($request->input('age')); $balance = new Float32($request->input('balance')); + } catch (\OutOfRangeException | \OverflowException $e) { + return response()->json([ + 'error' => 'Value out of range', + 'message' => $e->getMessage(), + ], 400); + } return response()->json([ 'user_id' => $userId->getValue(), 'age' => $age->getValue(), 'balance' => $balance->getValue(), ]); }src/Composite/Vector/Vec4.php (1)
62-69
: Normalize stored components to float in setValue()Accepting numeric strings passes validation but may leak strings into $components. Normalize to float to keep math stable across the API.
public function setValue(mixed $value): void { if (!is_array($value)) { throw new InvalidArgumentException('Value must be an array of components.'); } - $this->validateComponents($value); - $this->components = $value; + $this->validateComponents($value); + $this->components = array_map(static fn ($c) => (float) $c, $value); }src/Composite/Struct/Rules/PasswordRule.php (2)
43-49
: Use multibyte-safe length (UTF‑8) with fallbackstrlen() counts bytes; for non‑ASCII passwords this under/over counts characters.
- $length = strlen($value); + $length = function_exists('mb_strlen') ? mb_strlen($value, 'UTF-8') : strlen($value);
56-61
: Make character class checks Unicode-aware and tighten “special” charactersCurrent regexes are ASCII-only and treat whitespace/control chars as “special”. Prefer Unicode properties and exclude whitespace from specials.
- if ($this->requireUppercase && !preg_match('/[A-Z]/', $value)) { + if ($this->requireUppercase && !preg_match('/\p{Lu}/u', $value)) { throw new ValidationException( "Field '$fieldName' must contain at least one uppercase letter" ); } - if ($this->requireLowercase && !preg_match('/[a-z]/', $value)) { + if ($this->requireLowercase && !preg_match('/\p{Ll}/u', $value)) { throw new ValidationException( "Field '$fieldName' must contain at least one lowercase letter" ); } - if ($this->requireNumbers && !preg_match('/[0-9]/', $value)) { + if ($this->requireNumbers && !preg_match('/\p{Nd}/u', $value)) { throw new ValidationException( "Field '$fieldName' must contain at least one number" ); } - if ($this->requireSpecialChars && !preg_match('/[^a-zA-Z0-9]/', $value)) { + // P (punctuation) or S (symbols); excludes whitespace/control characters + if ($this->requireSpecialChars && !preg_match('/[\p{P}\p{S}]/u', $value)) { throw new ValidationException( "Field '$fieldName' must contain at least one special character" ); }If ASCII‑only is intentional, confirm and we’ll adjust the regexes accordingly.
Also applies to: 62-66, 68-72, 74-78
src/Composite/Vector/Vec2.php (1)
47-54
: Normalize stored components to float in setValue()Keep component types consistent and avoid leaking numeric strings.
public function setValue(mixed $value): void { if (!is_array($value)) { throw new InvalidArgumentException('Value must be an array of components.'); } - $this->validateComponents($value); - $this->components = $value; + $this->validateComponents($value); + $this->components = array_map(static fn ($c) => (float) $c, $value); }src/Laravel/Models/ExampleModel.php (2)
126-130
: Don’t access $attributes directly; use accessors/casts and handle nullsAccessing $this->attributes bypasses casting and can notice on unset indexes.
public function addToScore(Int8 $points): Int8 { - $currentScore = $this->getScoreAttribute($this->attributes['score']); + $currentScore = $this->score ?? new Int8(0); return $currentScore->add($points); } @@ public function isHighScore(): bool { - $score = $this->getScoreAttribute($this->attributes['score']); + $score = $this->score ?? new Int8(0); $threshold = new Int8(100); return $score->greaterThan($threshold); }As a next step, consider a dedicated Int8 cast for score to remove the custom accessor/mutator for it.
Also applies to: 135-140
94-101
: decimal cast vs Float32 storagedecimal:2 returns strings; setBalanceAttribute stores floats, which may introduce precision drift. Prefer a dedicated Float32 cast for symmetry or store strings consistently.
Do you already have Float32Cast? If yes, switch 'balance' to that cast and drop the accessor/mutator.
src/Abstract/AbstractVector.php (3)
14-18
: Normalize component types in constructorEnsure internal storage is float to avoid surprises in typed getters and math ops.
public function __construct(array $components) { $this->validateComponents($components); - $this->components = $components; + $this->components = array_map(static fn ($c) => (float) $c, $components); }
133-140
: Harden numeric validation (reject NaN/INF and non‑scalars)is_numeric() allows numeric strings and doesn’t reject INF/NaN. Tighten checks.
protected function validateNumericComponents(array $components): void { - foreach ($components as $component) { - if (!is_numeric($component)) { - throw new InvalidArgumentException("All components must be numeric"); - } - } + foreach ($components as $component) { + if (!is_int($component) && !is_float($component) && !is_string($component)) { + throw new InvalidArgumentException("All components must be numeric scalars"); + } + $f = (float) $component; + if (is_nan($f) || is_infinite($f)) { + throw new InvalidArgumentException("Components must be finite numbers"); + } + } }
107-114
: Consider tolerance-based equality for floatsExact array equality is brittle with floating point math. Suggest adding equalsWithin(float $epsilon = 1e-9): bool or similar, keeping equals() as strict if you prefer.
src/Composite/Struct/AdvancedStruct.php (3)
90-93
: Harden toJson against json_encode failures.Ensure a string is always returned.
- public function toJson(bool $useAliases = false): string - { - return json_encode($this->toArray($useAliases)); - } + public function toJson(bool $useAliases = false): string + { + $json = json_encode( + $this->toArray($useAliases), + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR + ); + return $json === false ? '{}' : $json; + }
45-51
: Support object-based rules (ValidationRule) in addition to callables.The PR mentions a ValidationRule interface; only callables run today.
- foreach ($rules as $rule) { - if (is_callable($rule)) { - if (!$rule($value)) { - throw new ValidationException("Validation failed for field '$field'"); - } - } - } + foreach ($rules as $rule) { + if (is_callable($rule) && !$rule($value)) { + throw new ValidationException("Validation failed for field '$field'"); + } + if ($rule instanceof \Nejcc\PhpDatatypes\Composite\Struct\ValidationRule && !$rule->validate($value)) { + throw new ValidationException("Validation failed for field '$field'"); + } + }Based on learnings
101-109
: toXml does not handle nested arrays/invalid tag names.Consider recursive serialization and safe element names.
- public function toXml(bool $useAliases = false): string - { - $arr = $this->toArray($useAliases); - $xml = new \SimpleXMLElement('<struct></struct>'); - foreach ($arr as $k => $v) { - $xml->addChild($k, htmlspecialchars((string)$v)); - } - return $xml->asXML(); - } + public function toXml(bool $useAliases = false): string + { + $arr = $this->toArray($useAliases); + $xml = new \SimpleXMLElement('<struct/>'); + $this->arrayToXml($xml, $arr); + return $xml->asXML() ?: ''; + } + + private function arrayToXml(\SimpleXMLElement $xml, array $arr): void + { + foreach ($arr as $k => $v) { + $name = (is_string($k) && preg_match('/^[A-Za-z_][A-Za-z0-9_.-]*$/', $k)) ? $k : 'item'; + if (is_array($v)) { + $child = $xml->addChild($name); + $this->arrayToXml($child, $v); + } else { + $xml->addChild($name, htmlspecialchars((string)$v)); + } + } + }README.md (1)
430-433
: Fix markdownlint MD034 (bare URL/email).Wrap the security email in a mailto link.
-If you discover any security-related issues, please email [email protected] instead of using the issue tracker. +If you discover any security-related issues, please email [[email protected]](mailto:[email protected]) instead of using the issue tracker.The linter flagged a bare URL at Line 432; if another bare URL exists, convert it to a markdown link similarly.
src/Composite/Struct/ImmutableStruct.php (1)
297-300
: Harden __toString against json_encode falsejson_encode can return false; cast and use sane flags.
- return json_encode($this->toArray()); + return (string) json_encode($this->toArray(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);src/Composite/Struct/Struct.php (2)
85-87
: Broaden nested struct detectionOnly checking instanceof self misses other struct implementations. Prefer capability check.
- if ($value instanceof self) { - $value = $value->toArray($useAliases); - } + if (is_object($value) && method_exists($value, 'toArray')) { + $value = $value->toArray($useAliases); + }
103-107
: Handle invalid JSON robustlyUse JSON_THROW_ON_ERROR to avoid silent nulls.
- $data = json_decode($json, true); + $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);benchmarks/ArrayBenchmark.php (2)
8-8
: Remove unused import StringArrayNot used in this benchmark.
-use Nejcc\PhpDatatypes\Composite\Arrays\StringArray;
14-16
: Suppress PHPMD unused locals for benchmark noiseBenchmarks often keep temps intentionally; suppress at class level.
-/** - * Benchmark comparing native PHP arrays vs PHP Datatypes arrays - */ +/** + * Benchmark comparing native PHP arrays vs PHP Datatypes arrays + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */Tests/Composite/String/CompositeStringTypesTest.php (1)
32-227
: Solid coverage; consider a few edge additions
- Add IPv6 case for IpString; mixed-case MAC; css with multiple rules; UrlString with query/IDN.
- If on PHPUnit 11, you can use attributes (e.g., #[Test]) and data providers for valid/invalid pairs.
Based on learnings
src/Composite/Union/UnionType.php (1)
52-55
: Minor: unused $expectedType in foreachUse array_keys to avoid PHPMD noise.
- foreach ($typeMap as $key => $expectedType) { + foreach (array_keys($typeMap) as $key) { $this->values[$key] = $initialValues[$key] ?? null; }
matrix: | ||
os: [ubuntu-latest, windows-latest] | ||
php: [8.2, 8.3] | ||
php: [8.2, 8.3, 8.4] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Align CI matrix with PHP ^8.4 requirement to fix Composer failures.
Current matrix includes 8.2/8.3, but composer.json requires php ^8.4; jobs on 8.2/8.3 fail early.
Recommended diff:
- php: [8.2, 8.3, 8.4]
+ php: [8.4]
Optional (reduce dependency conflicts on prefer-lowest):
- run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction
+ run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction -W
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
php: [8.2, 8.3, 8.4] | |
php: [8.4] |
🤖 Prompt for AI Agents
.github/workflows/main.yml around line 12: the GitHub Actions PHP matrix
includes versions 8.2 and 8.3 which conflict with composer.json requiring php
^8.4 and cause CI failures; update the matrix to only include PHP 8.4 (or 8.4
and newer supported versions) instead of 8.2/8.3 so jobs run with a compatible
PHP, and optionally add a separate job/matrix entry to run with composer
--prefer-lowest if you want to exercise lowest-dependency compatibility.
override(\int8(0), map([ | ||
'' => \Nejcc\PhpDatatypes\Scalar\Integers\Signed\Int8::class, | ||
])); | ||
|
||
override(\int16(0), map([ | ||
'' => \Nejcc\PhpDatatypes\Scalar\Integers\Signed\Int16::class, | ||
])); | ||
|
||
override(\int32(0), map([ | ||
'' => \Nejcc\PhpDatatypes\Scalar\Integers\Signed\Int32::class, | ||
])); | ||
|
||
override(\int64(0), map([ | ||
'' => \Nejcc\PhpDatatypes\Scalar\Integers\Signed\Int64::class, | ||
])); | ||
|
||
override(\uint8(0), map([ | ||
'' => \Nejcc\PhpDatatypes\Scalar\Integers\Unsigned\UInt8::class, | ||
])); | ||
|
||
override(\uint16(0), map([ | ||
'' => \Nejcc\PhpDatatypes\Scalar\Integers\Unsigned\UInt16::class, | ||
])); | ||
|
||
override(\uint32(0), map([ | ||
'' => \Nejcc\PhpDatatypes\Scalar\Integers\Unsigned\UInt32::class, | ||
])); | ||
|
||
override(\uint64(0), map([ | ||
'' => \Nejcc\PhpDatatypes\Scalar\Integers\Unsigned\UInt64::class, | ||
])); | ||
|
||
override(\float32(0.0), map([ | ||
'' => \Nejcc\PhpDatatypes\Scalar\FloatingPoints\Float32::class, | ||
])); | ||
|
||
override(\float64(0.0), map([ | ||
'' => \Nejcc\PhpDatatypes\Scalar\FloatingPoints\Float64::class, | ||
])); | ||
|
||
override(\char(''), map([ | ||
'' => \Nejcc\PhpDatatypes\Scalar\Char::class, | ||
])); | ||
|
||
override(\byte(0), map([ | ||
'' => \Nejcc\PhpDatatypes\Scalar\Byte::class, | ||
])); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify that the uint64()
helper function exists.
The override mappings for scalar type helpers follow the correct pattern. However, the uint64()
helper referenced at lines 34-36 is not present in the provided code snippets from src/helpers.php
, while all other scalar helpers (int8, int16, int32, int64, uint8, uint16, uint32, float32, float64, char, byte) are confirmed to exist.
Run the following script to verify the uint64()
helper exists in the codebase:
🏁 Script executed:
#!/bin/bash
# Description: Check for uint64 helper function definition
# Search for uint64 function definition
rg -nP '^\s*function\s+uint64\s*\(' src/helpers.php
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Description: Broader search for uint64 function definition across entire codebase
# Search for uint64 function definition anywhere in the codebase
rg -nP 'function\s+uint64\s*\(' --type=php
# Also check if it's defined as part of namespace or differently
rg -nP '\buint64\s*\(' --type=php -A 2 -B 2 | head -50
Length of output: 233
🏁 Script executed:
#!/bin/bash
# Description: Verify that other scalar helpers exist to confirm uint64 is missing
# Search for int64 and uint32 helper definitions
rg -nP 'function\s+(int64|uint32|char|byte)\s*\(' --type=php -A 3
Length of output: 667
The uint64()
helper function is missing from the codebase but is referenced in the meta file.
Verification confirms that other scalar helpers (int64 at line 70, uint32 at line 118, char at line 149, byte at line 156) are properly defined in src/helpers.php
. However, uint64()
does not exist anywhere in the codebase, yet lines 34-36 of .phpstorm.meta.php
contain an override for \uint64(0)
.
Either remove the uint64 override from .phpstorm.meta.php
(lines 34-36) or implement the missing uint64()
helper function in src/helpers.php
.
echo "=== Performance Comparison ===\n"; | ||
echo sprintf( | ||
"Int8 Creation vs Native: %.2fx slower\n", | ||
$int8Creation['time'] / $nativeCreation['time'] | ||
); | ||
echo sprintf( | ||
"Int8 Arithmetic vs Native: %.2fx slower\n", | ||
$int8Arithmetic['time'] / $nativeArithmetic['time'] | ||
); | ||
echo sprintf( | ||
"Int8 Memory overhead: %d bytes per operation\n", | ||
($int8Creation['memory'] - $nativeCreation['memory']) / $int8Creation['iterations'] | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against potential division by zero in performance comparisons.
The performance comparison calculations divide by native benchmark times, which could theoretically be zero or near-zero for very fast operations, leading to division by zero or misleading comparisons.
Apply this diff to add safety checks:
echo "=== Performance Comparison ===\n";
- echo sprintf(
- "Int8 Creation vs Native: %.2fx slower\n",
- $int8Creation['time'] / $nativeCreation['time']
- );
- echo sprintf(
- "Int8 Arithmetic vs Native: %.2fx slower\n",
- $int8Arithmetic['time'] / $nativeArithmetic['time']
- );
- echo sprintf(
- "Int8 Memory overhead: %d bytes per operation\n",
- ($int8Creation['memory'] - $nativeCreation['memory']) / $int8Creation['iterations']
- );
+ if ($nativeCreation['time'] > 0) {
+ echo sprintf(
+ "Int8 Creation vs Native: %.2fx slower\n",
+ $int8Creation['time'] / $nativeCreation['time']
+ );
+ }
+ if ($nativeArithmetic['time'] > 0) {
+ echo sprintf(
+ "Int8 Arithmetic vs Native: %.2fx slower\n",
+ $int8Arithmetic['time'] / $nativeArithmetic['time']
+ );
+ }
+ if ($int8Creation['iterations'] > 0) {
+ echo sprintf(
+ "Int8 Memory overhead: %d bytes per operation\n",
+ ($int8Creation['memory'] - $nativeCreation['memory']) / $int8Creation['iterations']
+ );
+ }
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
|
||
- initial release | ||
## [2.0.0] - 2024-12-19 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verify the release date.
The changelog lists the release date as "2024-12-19", but this PR was created on 2025-05-30. Ensure the date reflects the actual or intended release date for v2.0.0.
🤖 Prompt for AI Agents
In CHANGELOG.md around line 8, the release date for version 2.0.0 is listed as
"2024-12-19" but that is inconsistent with the PR date; update the date to the
correct/intended release date (e.g., the actual release date or the PR merge
date 2025-05-30), or if the release date is unknown, replace the static date
with "TBD" and add a short note or PR reference indicating it should be set at
release time.
paths: | ||
- src | ||
- Tests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Path casing: use tests/ (lowercase) to match autoload and typical repo layout.
Composer autoload-dev maps Nejcc\PhpDatatypes\Tests\ to "tests". On Linux, "Tests" won’t match.
- paths:
- - src
- - Tests
+ paths:
+ - src
+ - tests
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
paths: | |
- src | |
- Tests | |
paths: | |
- src | |
- tests |
🤖 Prompt for AI Agents
In phpstan.neon around lines 3 to 5, the paths section uses "Tests" (capital T)
which does not match Composer's autoload-dev mapping to "tests" and will fail on
case-sensitive filesystems; change the entry from "Tests" to "tests" so the
phpstan paths exactly match the autoload directory name used in composer.json.
public function getUserIdAttribute($value): UInt8 | ||
{ | ||
return new UInt8($value); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Accessor return types should allow nulls (DB nulls cause TypeError)
Eloquent may pass null when column is nullable; current signatures don’t allow it.
- public function getUserIdAttribute($value): UInt8
+ public function getUserIdAttribute($value): ?UInt8
{
- return new UInt8($value);
+ return $value === null ? null : new UInt8($value);
}
@@
- public function getBalanceAttribute($value): Float32
+ public function getBalanceAttribute($value): ?Float32
{
- return new Float32((float) $value);
+ return $value === null ? null : new Float32((float) $value);
}
@@
- public function getScoreAttribute($value): Int8
+ public function getScoreAttribute($value): ?Int8
{
- return new Int8($value);
+ return $value === null ? null : new Int8($value);
}
Confirm whether these columns are non‑nullable; if so, we can keep non‑nullable returns.
Also applies to: 86-89, 106-109
I'll verify this review comment by inspecting the model file, database schema, and validating the claim about Eloquent accessor behavior.
🏁 Script executed:
#!/bin/bash
# Find and display the ExampleModel.php file
find . -name "ExampleModel.php" -type f | head -5
Length of output: 37
🏁 Script executed:
#!/bin/bash
# Search for migration files that might define the table schema
find . -path "*/migrations/*" -name "*.php" | grep -i example | head -10
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Search for UInt8, Float32, Int8 class definitions
rg -n "(class UInt8|class Float32|class Int8)" --type php
Length of output: 994
🏁 Script executed:
#!/bin/bash
# Search for nullable column definitions in migrations
rg -n "nullable\(\)" --type php | grep -E "(user_id|balance|score)" | head -20
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Look at the table structure definition
rg -nB5 "user_id|balance|score" --type php | grep -E "(Schema::|table\(|string\(|integer\(|nullable)" | head -30
Length of output: 154
🏁 Script executed:
#!/bin/bash
# Display the actual model file around the specified lines
fd -t f -e php . | xargs grep -l "getUserIdAttribute" 2>/dev/null | head -1 | xargs cat
Length of output: 3289
🏁 Script executed:
#!/bin/bash
# Search for any model factory or test file that might show column nullability
find . -name "*Factory.php" -o -name "*ModelTest.php" | head -5
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Look for database/schema files
find . -type f \( -name "schema.php" -o -name "*create_*.php" \) | head -10
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Check the Laravel migrations directory structure
find . -path "*/database/migrations/*" -type f -name "*.php" | head -20
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Search for the table definition more broadly
rg -n "Schema::create.*\(function" --type php -A 20 | head -100
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Look specifically in the model file for column definitions or cast information
rg -n "protected.*\$fillable|protected.*\$casts" --type php
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Find the model file more directly
rg -l "getUserIdAttribute\|getBalanceAttribute\|getScoreAttribute" --type php
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Search for schema blueprint calls related to nullable
rg -n "->nullable" --type php -B 2 | grep -E "(user_id|balance|score)" -B 2
Length of output: 404
🏁 Script executed:
#!/bin/bash
# Check if there's a database.sql or schema file
find . -name "*.sql" -type f | head -10
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Look for accessor definitions to understand the pattern
ast-grep --pattern 'public function $_Attribute($_): $_ { $$$ }'
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Search for cast definitions that might affect nullability
rg -n "cast" --type php | head -20
Length of output: 1638
🏁 Script executed:
#!/bin/bash
# Display table/database structure if available
find . -name "*.md" | xargs grep -l "nullable\|user_id\|balance\|score" 2>/dev/null | head -5
Length of output: 50
🏁 Script executed:
#!/bin/bash
# Check model file directory structure
find . -type d -name "Models" | head -5
Length of output: 20
🏁 Script executed:
#!/bin/bash
# Look for any Laravel test database seeding files
find . -name "*DatabaseSeeder.php" -o -name "Seeder.php" | head -5
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Search for the actual model file with a broader approach
fd ExampleModel.php
Length of output: 35
🏁 Script executed:
#!/bin/bash
# List files in common Laravel locations
ls -la src/Laravel/Models/ 2>/dev/null || ls -la app/Models/ 2>/dev/null || find . -type d -name Models 2>/dev/null | head -3
Length of output: 176
🏁 Script executed:
#!/bin/bash
# Get all PHP files in src/Laravel/Models directory if it exists
cat src/Laravel/Models/*.php 2>/dev/null | head -200
Length of output: 3289
🏁 Script executed:
#!/bin/bash
# Try to show the ExampleModel directly
cat src/Laravel/Models/ExampleModel.php 2>/dev/null || echo "File not found at expected location"
Length of output: 3289
🏁 Script executed:
#!/bin/bash
# Search all directories for ExampleModel
find . -name "ExampleModel.php" -exec cat {} \;
Length of output: 3289
🏁 Script executed:
#!/bin/bash
# Try a case-insensitive search
find . -iname "*example*model*.php"
Length of output: 37
🏁 Script executed:
#!/bin/bash
# List directory structure
ls -la src/Laravel/ 2>/dev/null || echo "src/Laravel not found"
Length of output: 541
🏁 Script executed:
#!/bin/bash
# Show repository structure at top level
ls -la | head -20
Length of output: 1195
🏁 Script executed:
#!/bin/bash
# Find any PHP files that might be the model
find . -type f -name "*.php" | grep -i "model" | head -20
Length of output: 37
🏁 Script executed:
#!/bin/bash
# Fallback: search for the accessor methods directly
rg "getUserIdAttribute|getBalanceAttribute|getScoreAttribute"
Length of output: 491
🏁 Script executed:
#!/bin/bash
# Check git to understand if file exists
git ls-files | grep -i example
Length of output: 548
🏁 Script executed:
#!/bin/bash
# Look for any PHP files at all in src
find src -name "*.php" 2>/dev/null | head -20
Length of output: 679
🏁 Script executed:
#!/bin/bash
# Check if src/Laravel/Models exists
test -d src/Laravel/Models && echo "Directory exists" || echo "Directory does not exist"
Length of output: 16
🏁 Script executed:
#!/bin/bash
# Final attempt: show directory contents
find . -type d -name Laravel -o -name Models | head -10
Length of output: 34
🏁 Script executed:
#!/bin/bash
# Check git status for any PHP files
git status --short | grep "\.php"
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Look for all tracked PHP files in repo
git ls-files "*.php" | head -30
Length of output: 1012
🏁 Script executed:
#!/bin/bash
# Try to find ExampleModel anywhere
git ls-files | grep -i "example"
Length of output: 548
🏁 Script executed:
#!/bin/bash
# Count PHP files in repo
find . -name "*.php" -type f | wc -l
Length of output: 3
🏁 Script executed:
#!/bin/bash
# Show me what's tracked
git ls-files | head -50
Length of output: 1421
🏁 Script executed:
#!/bin/bash
# Look at the current branch's file structure
ls -la
Length of output: 1895
🏁 Script executed:
#!/bin/bash
# Check if we can see any PHP content
git show HEAD:src/Laravel/Models/ExampleModel.php 2>/dev/null || echo "Not found in HEAD"
Length of output: 3289
🏁 Script executed:
#!/bin/bash
# Try to find what files changed in this PR
git diff --name-only origin/main..HEAD 2>/dev/null || echo "Cannot determine diff"
Length of output: 21
🏁 Script executed:
#!/bin/bash
# Look at all PHP files in the PR
git diff --name-only HEAD 2>/dev/null | grep "\.php" || echo "No changes found"
Length of output: 16
🏁 Script executed:
#!/bin/bash
# Find any PHP files committed
git log --name-only --pretty=format: | grep "\.php" | sort -u | head -20
Length of output: 689
🏁 Script executed:
#!/bin/bash
# Direct file check
test -f "src/Laravel/Models/ExampleModel.php" && cat "src/Laravel/Models/ExampleModel.php" || find . -type f -name "*.php" -print | head -5
Length of output: 3289
🏁 Script executed:
#!/bin/bash
# Comprehensive PHP file search
find . -type f \( -name "*.php" -o -name "*.laravel" \) 2>/dev/null | head -30
Length of output: 1049
🏁 Script executed:
#!/bin/bash
# Check the exact directory structure of the provided path
stat src/Laravel/Models/ExampleModel.php 2>/dev/null || stat src/Laravel 2>/dev/null || stat src 2>/dev/null || echo "Path doesn't exist; showing available structure:" && find . -maxdepth 3 -type d
Length of output: 1059
🏁 Script executed:
#!/bin/bash
# Just pwd and ls to understand where we are
pwd && echo "--- Contents of current dir ---" && ls -la
Length of output: 1946
🏁 Script executed:
#!/bin/bash
# Search for text that would be in the methods
rg "UInt8\|Float32\|Int8" --type php
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Try searching for the specific column names in nullable() context
rg "user_id.*nullable|balance.*nullable|score.*nullable" --type php -C 3
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Look for migrations with schema definitions
rg "->unsignedBigInteger|->decimal|->integer" --type php -C 2 | head -50
Length of output: 404
🏁 Script executed:
#!/bin/bash
# Check if the file exists by using a Python script
python3 << 'EOF'
import os
import json
# Try to find ExampleModel.php
for root, dirs, files in os.walk('.'):
for file in files:
if 'ExampleModel' in file and file.endswith('.php'):
path = os.path.join(root, file)
print(f"Found: {path}")
with open(path, 'r') as f:
lines = f.readlines()
# Print lines 66-69, 86-89, 106-109
print("\n=== Lines 66-69 ===")
for i in range(65, min(69, len(lines))):
print(f"{i+1}: {lines[i]}", end='')
print("\n=== Lines 86-89 ===")
for i in range(85, min(89, len(lines))):
print(f"{i+1}: {lines[i]}", end='')
print("\n=== Lines 106-109 ===")
for i in range(105, min(109, len(lines))):
print(f"{i+1}: {lines[i]}", end='')
EOF
Length of output: 471
Now I need to understand Eloquent's actual behavior with nullable columns and accessors, and whether the custom types can handle null values.
🌐 Web query:
Eloquent accessor null values nullable columns Laravel
💡 Result:
Short answer: Eloquent accessors receive the raw column value which can be null, so you must explicitly handle null (or let it pass through). When using the modern Attribute cast, check for null inside the get callback (or use null coalescing / optional()). Primitive casts also skip casting when value is null.
Examples:
-
Attribute cast (Laravel 8+):
protected function title(): \Illuminate\Database\Eloquent\Casts\Attribute
{
return \Illuminate\Database\Eloquent\Casts\Attribute::make(
get: fn(?string $value) => $value === null ? null : strtoupper($value),
);
} -
Old-style accessor:
public function getTitleAttribute(?string $value)
{
return $value === null ? null : strtoupper($value);
}
Notes:
- Eloquent checks for null before applying primitive casts (so a null column remains null unless you handle it). [1][2][3]
Sources:
[1] Eloquent mutators & casting (Laravel docs).
[2] Illuminate\Database\Eloquent\Casts\Attribute API (Laravel).
[3] HasAttributes (casts/null handling) — Laravel API.
🌐 Web query:
Laravel Eloquent get attribute accessor null handling
💡 Result:
Short answer — accessors get the raw stored attribute (often null) so you must explicitly handle nulls. Options:
-
Check $value (or $attributes) inside the accessor:
protected function foo(): Attribute
{
return Attribute::make(
get: fn ($value, $attrs) => $value ? transform($value) : null,
);
}
(Accessors receive the stored value and optional $attributes array.) [1] -
If you need a related model, don't rely on the attribute value — check the relation or use withDefault():
// relation with null-object default
public function author() { return $this->belongsTo(User::class)->withDefault(); }
// or in accessor, use optional() or check $this->relations
$name = optional($this->author)->name;
Or in an accessor explicitly load/check the relation. [2][3] -
Use the optional() helper or null-coalescing (??) in views/controllers to avoid "trying to get property of non-object". [2]
References:
- Eloquent accessors/mutators (Attribute API). [1]
- Helpers: optional(). [2]
- Discussion showing accessors receive column values (not relation objects) and how to handle relations. [3]
🌐 Web query:
Does Eloquent pass null to accessors for nullable database columns
💡 Result:
Yes. When a column is NULL in the database, Eloquent passes null as the original value into the accessor. The documentation states the "original value of the column is passed to the accessor" (i.e. the $value parameter), which includes null. [1][2]
Sources:
- Laravel Eloquent mutators (accessors) — docs showing original value is passed to the accessor. [1]
- Laravel Eloquent mutators (older/alternate docs). [2]
🌐 Web query:
Laravel accessor TypeError null value
💡 Result:
Short answer: the TypeError usually comes from PHP type hints — your accessor (or its get closure) is being passed a null database value but you typed the parameter or return as non-nullable (string). Make the parameter/return nullable or handle null before returning.
What to check and fix
- If using the new Attribute accessors, ensure you import and return an Attribute. Example and docs: Laravel requires accessors to return Illuminate\Database\Eloquent\Casts\Attribute. [1]
- Make the closure accept null (or remove the strict type) so PHP won’t throw when the DB value is null:
- Bad (throws when $value is null):
fn(string $value) => strtoupper($value) - Good:
fn(?string $value) => $value === null ? null : strtoupper($value)
- Bad (throws when $value is null):
- Or ensure you always return a non-null value from the accessor (e.g. return '') or coalesce: fn($v) => strtoupper($v ?? '').
- Also check you didn’t add an explicit non-nullable return type to any custom method that may return null.
Minimal examples
- Attribute accessor allowing null:
protected function title(): \Illuminate\Database\Eloquent\Casts\Attribute
{
return \Illuminate\Database\Eloquent\Casts\Attribute::make(
get: fn(?string $value) => $value === null ? null : ucfirst($value),
);
}
Why this happens (PHP behavior)
- PHP throws TypeError when a function parameter or return is declared as string but null is given/returned; use ?string (nullable) or no type-hint to accept null. [2]
References
- Laravel accessors (Attribute) docs. [1]
- PHP nullable types / TypeError behavior (nullable parameter/return rules). [2]
🌐 Web query:
PHP constructor parameter null type safety
💡 Result:
Short answer — make constructor parameters explicitly nullable (use ?Type or Type|null), enable declare(strict_types=1) when you want runtime safety, and avoid relying on implicit null defaults (they're deprecated/removed in recent PHP versions).
Key points
- Use explicit nullable types: function __construct(?User $u) or __construct(User|null $u). This is the safe, supported way. [1][2]
- If a parameter is non-nullable and you pass null, user-land code already throws a TypeError; PHP 8.1 started deprecating passing null to non-nullable internal function scalar parameters (to become an error in PHP 9). So don’t rely on silent null coercion. [3][4]
- Implicitly-nullable parameters (declaring a typed parameter with = null but without ? or |null) are deprecated (RFC implemented targeting PHP 8.4) — declare nullability explicitly. [5]
- Best practice for constructors: prefer explicit types + default values when appropriate, validate/normalize inputs (e.g., $this->x = $x ?? new Default();), and enable strict_types for stricter runtime checks. [1][5]
Examples
- Explicit nullable:
public function __construct(?string $name = null) { $this->name = $name; } [1] - Union/null:
public function __construct(string|array|null $cfg = null) { ... } [1][2]
Sources
[1] PHP manual — Type declarations.
[2] PHP.Watch — Constructor property promotion & nullable types.
[3] PHP.Watch — PHP 8.1: Passing null to non-nullable internal function parameters is deprecated.
[4] RFC: deprecate passing null to non-nullable internal scalar arguments.
[5] RFC: deprecate implicitly nullable parameter types (implemented).
🌐 Web query:
Laravel attribute accessor best practices nullable
💡 Result:
Short best-practices for nullable Eloquent attribute accessors
- Preserve nulls: if the stored column may be NULL, return null early from the accessor (if $value === null) so you don’t accidentally convert a missing value into a non-null type. Laravel also documents that attributes that are null will not be cast. [1]
- Explicit null checks: write your get callback as get: fn($value) => $value === null ? null : (transform($value)). This keeps semantics clear and avoids runtime errors. [2]
- Use nullable type hints when appropriate (e.g., fn(?string $value) => …) so static analyzers/IDE understand nullability.
- Don’t duplicate relationship names: avoid defining an attribute / cast with the same name as a relationship (can cause surprising null/override issues). [1]
- Use casts for standard behavior: rely on built-in casts (date, boolean, AsStringable, custom cast classes) instead of manual transformations when possible — casts already skip null values. [1]
- Keep accessors side-effect free: avoid writing to other attributes or performing DB writes inside accessors (makes null behavior predictable).
- Document & test: add unit tests for null, empty, and valid values to assert accessor behavior.
References
- Laravel docs — Eloquent: Mutators & Casting (notes that attributes that are null will not be cast and general accessor guidance). [1]
- Illuminate\Database\Eloquent\Casts\Attribute API (Attribute::make / get signature / examples). [2]
Sources:
[1] Laravel — Eloquent: Mutators & Casting. (laravel.com)
[2] Laravel API — Illuminate\Database\Eloquent\Casts\Attribute. (api.laravel.com)
Citations:
- 1: https://laravel.com/docs/9.x/eloquent-mutators?utm_source=openai
- 2: https://api.laravel.com/docs/9.x/Illuminate/Database/Eloquent/Casts/Attribute.html?utm_source=openai
🌐 Web query:
Eloquent getAttributeValue null handling
💡 Result:
Short answer
- getAttributeValue returns the raw attribute (or the accessor result) — if the stored value is null, Laravel will return null and will not run the normal cast logic. [1][2]
Details you may need
- The HasAttributes trait implements getAttributeValue/getAttribute; attribute access first checks for an accessor, then for casts/dates/etc. [1][3]
- cast logic explicitly short‑circuits on null (casts are skipped and null is returned). So a null in $attributes remains null unless an accessor produces something else. [2]
- Note: Eloquent can be configured to throw on missing attributes, but that’s a different behavior (missing vs. present-but-null). [1]
References
[1] HasAttributes / Model docs (Laravel API).
[2] laravel/framework issue showing castAttribute returns input when is_null($value).
[3] Explainer: “Under the hood: How model attributes work in Laravel.”
🌐 Web query:
Laravel mutator accessor null safety typing
💡 Result:
Use nullable types in your accessor/mutator closures and handle null explicitly. Eloquent Attribute accessors/mutators accept typed closures, so mark the parameter and return types as nullable (e.g. ?string) and return null when appropriate.
Example:
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function title(): Attribute
{
return Attribute::make(
// incoming DB value may be null
get: fn(?string $value): ?string => $value === null ? null : ucfirst($value),
// allow setting null
set: fn(?string $value): ?string => $value === null ? null : strtolower($value),
);
}
Notes:
- Attributes that are null are not cast by Laravel casts, so your accessor may receive null and should handle it. [1][2]
Sources:
[1] Laravel — Eloquent: Mutators & Casting (accessors/mutators).
[2] Laravel — Eloquent: Mutators & Casting (attribute casting — "Attributes that are null will not be cast").
🌐 Web query:
Laravel 8 9 10 accessor null handling breaking changes
💡 Result:
Short answer: when Laravel switched to the new Attribute API (Laravel 9→10) the framework passes the raw column value (which may be null) into your accessor/get closure — so any non‑nullable type hints (e.g. fn(string $value)) will now cause TypeErrors when the column is null. That’s the common breaking change people hit when migrating from the old getXAttribute style to Attribute::make. See the docs for the Attribute/get closure behavior. [1][2]
What to do
- Accept nullable/mixed types: use ?string, ?int or mixed in your get closure: fn(?string $value, array $attributes) => ...
- Guard against null inside the closure: if (is_null($value)) return null or a fallback.
- If returning value objects, be aware of accessor object‑caching features (docs). [1][2]
References
- Laravel 9 Eloquent Mutators & Casting (Attribute/get behavior). [1]
- Laravel 10 Eloquent Mutators & Casting (Attribute/get examples). [2]
[1] https://laravel.com/docs/9.x/eloquent-mutators
[2] https://laravel.com/docs/10.x/eloquent-mutators
🌐 Web query:
Illuminate Database Eloquent Casts handling nulls
💡 Result:
Short answer
- By design Eloquent does not cast attributes that are null — "Attributes that are null will not be cast." (Laravel docs). [1]
- If you want non-null defaults for casts (e.g. empty array, false, empty string) you must handle that yourself (custom cast, accessors/Attribute, or a base Model override). [1][2]
How to handle it (examples)
- Use an accessor (Attribute) to return a default when null
- Minimal example:
protected function options(): \Illuminate\Database\Eloquent\Casts\Attribute
{
return \Illuminate\Database\Eloquent\Casts\Attribute::make(
get: fn ($value) => $value === null ? [] : $value
);
}
(Keeps the cast but ensures callers see [] when DB value is null.) [1]
- Custom Cast that returns defaults when $value === null
- Implement Illuminate\Contracts\Database\Eloquent\CastsAttributes::get and check for null, returning your default ([], false, '', etc.). [1][5]
- Global/base Model override (only if you want app-wide behavior)
- Create App\Models\Model extends Illuminate\Database\Eloquent\Model and override castAttribute (or wrap cast logic) to return defaults for nulls, then have your models extend that base. Example approach shown in community posts that switches on cast type and returns defaults when $value is null. [2]
References
- Laravel docs — Eloquent: Mutators & Casting (notes that null attributes are not cast). [1]
- Community example showing how to return defaults for null-cast attributes by overriding behavior. [2]
If you want, I can:
- paste a ready-to-use custom Cast class or base Model override tailored to your preferred defaults, or
- show a complete accessor + cast example for your specific cast types.
🌐 Web query:
PHP strict_types constructor null parameter
💡 Result:
Short answer
- With declare(strict_types=1), a typed constructor parameter will raise a TypeError if you pass null unless the parameter type explicitly allows null (e.g. ?Type or Type|null). [1][3]
Why and how to fix it
- Parameters must be declared nullable to accept null: use ?string $name or string|null $name. Example:
public function __construct(?string $name = null) { ... }.[2][4] - Note: PHP historically allowed a bare typed parameter to have = null as a default in some older versions, but that is not equivalent to an explicit nullable type and is disallowed/unsafe for property promotion and newer PHP behavior. Use the ?T or union form to be correct. [1][2][8]
References
- PHP behavior for default null and typed parameters/property promotion. [1][2]
- Deprecation/TypeError rules for passing null to non-nullable params (PHP 8.1+ / strict rules). [3][4]
🌐 Web query:
UInt8 Int8 Float32 PHP constructor accept null
💡 Result:
Do you mean PHP typed parameters/properties named UInt8/Int8/Float32 (e.g. constructor parameter or typed property) — or are you referring to some library (FFI, extension, ClickHouse/Arrow bindings, or a custom class)? Which PHP version? Provide a short code example if possible.
🌐 Web query:
Typed property null safe pattern PHP
💡 Result:
Short answer: declare the property nullable (use ?Type) and/or initialize it in the constructor; then use the nullsafe operator (?->) for safe reads. Remember you cannot use ?-> in write context and non‑nullable typed properties must be initialized before access.
Examples
-
Make property nullable and read with nullsafe:
class User { public ?Profile $profile = null; }
$city = $user->profile?->address?->city; [PHP manual, typed properties; nullsafe operator]. [1][2] -
Keep property non‑nullable but ensure initialization (constructor) to avoid “must not be accessed before initialization” errors:
class User { public Profile $profile; public function __construct(Profile $p) { $this->profile = $p; } } [1][3]
Notes
- ?-> only works for reading (short‑circuits to null); it cannot be used on the left side of an assignment. [2][4]
- Typed (non‑nullable) properties throw if uninitialized; use ?Type or initialize in constructor or at declaration. [1][3]
Sources
[1] PHP manual — Properties / Type declarations.
[2] PHP RFC — Nullsafe operator (implementation in PHP 8.0).
[3] PHP RFC / Typed properties (v2) and non‑nullable property checks.
[4] Error docs: “Can’t use nullsafe operator in write context.”
🌐 Web query:
Laravel accessor return type nullable
💡 Result:
Short answer: use PHP nullable types for old-style accessors, and return an Attribute whose get callback may return null for the new-style Attribute accessors.
Examples:
-
Old (getFooAttribute) — nullable return:
public function getFooAttribute($value): ?string
{
return $value === null ? null : (string) $value;
} -
New (Attribute) — method must return Attribute; the get closure can return null:
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function foo(): Attribute
{
return Attribute::make(
get: fn ($value) => $value === null ? null : (string) $value,
);
}
Reference: Laravel Eloquent Mutators & Casting (accessor examples and Attribute usage). [1]
Sources
[1] Laravel Docs — Eloquent: Mutators & Casting.
🌐 Web query:
Eloquent accessor nullable return best practice
💡 Result:
Short answer — prefer returning a true null for missing/absent values (and use nullable types/casts), not an empty string or placeholder. Reasons and practical rules:
- Return null when the underlying attribute is null so JSON/array output, comparisons and type checks behave correctly (keep domain semantics). Use nullable return types in your code (e.g. ?string) or return null from the Attribute accessor get callback. [1][2]
- Use Eloquent casts / Attribute objects for conversion instead of manually coercing to '' — casts preserve null and centralize behavior (Attribute::make get callbacks, and the casts system, do not cast null values). [1][2]
- If you need a presentation-layer default (empty string, "—", etc.), apply it in the view/transformer/Resource layer (API Resource, Presenter) — keep model accessors pure and domain-typed. This avoids surprises when saving or serializing models. [1]
Example (concise):
- Model accessor returning nullable:
protected function nickname(): \Illuminate\Database\Eloquent\Casts\Attribute
{
return \Illuminate\Database\Eloquent\Casts\Attribute::make(
get: fn ($value) => $value === null ? null : (string) $value,
);
}
References:
[1] Laravel Eloquent — Mutators & Casting (Attribute docs).
[2] Illuminate\Database\Eloquent\Casts\Attribute API docs.
🌐 Web query:
Laravel accessor TypeErrors null column values
💡 Result:
Cause
- Your accessor/getter is declared to accept/return a non-nullable type (e.g. fn(string $value) => ...) but the DB column contains NULL → PHP throws a TypeError because null was passed/returned where a non-nullable type was declared. (laravel.com)
Fixes (pick one)
- Allow null in the accessor signature and handle it:
- Attribute API: get: fn (?string $value) => $value === null ? null : ucfirst($value). (laravel.com)
- Return a non-null default instead of null:
- get: fn ($value) => $value === null ? '' : $value. (laravel.com)
- Use classic untyped accessor method (no strict type hint) so null is allowed:
- public function getFooAttribute($value) { ... } . (laravel.com)
- Change DB column to NOT NULL / provide a default so nulls are never returned (schema change).
- Use casts / nullable PHP types when appropriate (declare ?string if null is valid). (typeerror.org)
Minimal examples
- Attribute with nullable input:
- protected function title(): Attribute { return Attribute::make(get: fn (?string $value) => $value === null ? null : ucfirst($value)); } (laravel.com)
- Classic accessor:
- public function getTitleAttribute($value) { return $value ? ucfirst($value) : null; } (laravel.com)
If you paste your accessor code I’ll point out exactly which type hint to change.
Citations:
- 1: https://laravel.com/docs/11.x/eloquent-mutators?utm_source=openai
- 2: https://laravel.com/docs/11.x/eloquent-mutators?utm_source=openai
- 3: https://laravel.com/docs/11.x/eloquent-mutators?utm_source=openai
- 4: https://laravel.com/docs/5.5/eloquent-mutators?utm_source=openai
- 5: https://www.typeerror.org/docs/php/language.types.declarations?utm_source=openai
- 6: https://laravel.com/docs/11.x/eloquent-mutators?utm_source=openai
- 7: https://laravel.com/docs/5.5/eloquent-mutators?utm_source=openai
🌐 Web query:
Eloquent attribute casting with nullable columns
💡 Result:
Short answer: by default Eloquent does NOT apply casts to attributes whose DB value is null — you must handle nulls yourself (via an attribute accessor or a custom cast), or use a cast implementation that provides a "default/nullable" option.
Why: Laravel docs state "Attributes that are null will not be cast." [1]. Options:
-
Use an Attribute accessor to return a default when null:
- Example:
protected function settings(): \Illuminate\Database\Eloquent\Casts\Attribute
{
return \Illuminate\Database\Eloquent\Casts\Attribute::make(
get: fn ($value) => $value === null ? [] : json_decode($value, true),
set: fn ($val) => json_encode($val)
);
}
(Attribute docs). [2]
- Example:
-
Implement a custom cast (implements CastsAttributes) and handle null in get()/set():
- get($model, $key, $value, $attributes) { return $value === null ? $default : /* casted value */; }
- Laravel docs cover custom casts. [1][3]
-
If you use third‑party cast helpers (e.g., Spatie/laravel-data or other castable DTO packages) they often provide a "default" or "nullable" option so the caster is instantiated even when DB value is null. Example: ArtistData::class . ':default' or Settings::class . ':nullable'. [4][5]
-
Quick alternative: cast JSON columns to 'array' or 'object' — note these still return null when DB is null, so use an accessor or custom cast to convert null → [] if you want a non‑null default. [1]
References
- Laravel Eloquent mutators & casting — "Attributes that are null will not be cast." (docs). [1]
- Illuminate\Database\Eloquent\Casts\Attribute (Attribute accessor API). [2]
- Laravel docs — Custom Casts (CastsAttributes interface). [1]
- Spatie laravel-data — Eloquent casting with "default" option. [4]
- Example package docs showing ':nullable' cast parameter. [5]
If you want, tell me which cast/column you’re using (built‑in cast, custom CastsAttributes, AsArrayObject, Spatie data, etc.) and I’ll give the exact code for returning a safe default when the DB value is null.
🌐 Web query:
Laravel mutators accessors strict typing null
💡 Result:
Short answer: make your accessor/mutator closures (or old-style get*/set* methods) accept nullable types and return nullable types — e.g. use ?string, ?int, ?YourValueObject — because Eloquent may pass null for DB NULLs. Also remember attribute casting rules: "Attributes that are null will not be cast" so your code must handle nulls explicitly. [1][2]
Example (Attribute API):
protected function title(): \Illuminate\Database\Eloquent\Casts\Attribute
{
return \Illuminate\Database\Eloquent\Casts\Attribute::make(
get: fn (?string $value) => $value === null ? null : ucfirst($value),
set: fn (?string $value) => $value === null ? null : strtolower($value),
);
}
Notes:
- For PHP strict typing, use nullable type hints (?Type) on parameters/returns when null is a valid value.
- If you use old getXAttribute / setXAttribute signatures, accept/return null there as well.
- If you rely on casts, read that null attributes are skipped by casts — handle nulls before/after casting if needed. [1][2]
Sources:
[1] Laravel — Eloquent: Mutators & Casting (accessors/mutators, Attribute API).
[2] Laravel — Eloquent: Mutators & Casting (casting: "Attributes that are null will not be cast").
🌐 Web query:
PHP constructor null parameter strict types
💡 Result:
Short answer: mark the parameter nullable. strict_types only changes scalar coercion behavior — it does not allow passing null to a non‑nullable type.
How to do it:
- PHP 7.1+: use the nullable shorthand ?Type (or Type|null in PHP 8+). Example:
- With union syntax (PHP 8+): function __construct(string|null $s = null) { ... } is equivalent to ?string.
Notes:
- If the parameter is declared as string (no ? or |null) then passing null will throw a TypeError. strict_types controls scalar coercion, not nullability. [1][2][3]
Sources:
[1] PHP Manual — Type declarations.
[2] PHP Manual — Deprecated: implicitly nullable parameter (migration notes).
[3] PHP RFC / docs — strict_types behavior and type errors.
🌐 Web query:
Eloquent documented behavior passing null to accessors
💡 Result:
Short answer: Laravel documents that when an attribute's stored value is null the accessor/mutator/cast system will receive null (and casts are not applied to null). In other words, accessors get the raw value (which can be null) and the built-in casts intentionally skip nulls. See the official docs: Eloquent accessors receive the original value, and "Attributes that are null will not be cast." [1][2].
Sources:
- Eloquent Mutators / Accessors (Laravel docs) — accessors receive the attribute value. [1]
- Eloquent Mutators & Casting (Laravel docs) — "Attributes that are null will not be cast." [2]
🌐 Web query:
Laravel changelog accessor null handling behavior
💡 Result:
Short answer: Eloquent returns null for attributes that are not present in the model's internal $attributes array, and an accessor (getXAttribute / Attribute::get) is only invoked when Eloquent's getAttribute/getAttributeValue path finds a value (or when the attribute is a mutated/appended attribute). In other words, missing (not-hydrated) DB columns will yield null and won't trigger the usual mutation/casting unless you either include them in the query, populate them, or define the accessor as a computed/appended attribute. [1][2]
What to do
- Include the column in your select/hydration so it's present in $attributes.
- Or make the property a computed/appended attribute (put it in $appends or implement an Attribute-returning accessor) so Eloquent will call the accessor for it.
- Or guard against null inside your accessor (check hasAttribute / array_key_exists on $this->attributes or use $this->getAttributeValue('field') / $this->getAttributeFromArray). [1][2]
Sources
- Laravel Eloquent HasAttributes (getAttribute/getAttributeFromArray, mutators, appends). [Laravel API]. [1]
- Community notes / Q&A showing $appends/accessor null behavior and why missing attributes return null (StackOverflow / forum threads). [2]
🌐 Web query:
Illuminate contracts casting attributes interface nullable
💡 Result:
Short answer: Laravel will skip casting when the raw attribute value is null, and your custom caster/getter may return null — declare nullable return types (e.g. ?MyValue|MyValue|null or MyValue|null) and handle null in get/set.
Details + example:
- Docs: "Attributes that are null will not be cast." [1].
- Contract: CastsAttributes get() may return TGet|null (i.e. nullable). See API. [2].
Example custom cast (PHP 8+):
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class MoneyCast implements CastsAttributes
{
// Return Money|null when attribute is null
public function get(Model $model, string $key, mixed $value, array $attributes): Money|null
{
if (is_null($value)) {
return null;
}
return new Money((int) $value);
}
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
if (is_null($value)) {
return null; // or ['column' => null] depending on structure
}
return $value instanceof Money ? $value->toInt() : (int) $value;
}
}
If you implement a Castable value object, have its caster return nullable types similarly and check for null on inbound/outbound. [1][2]
Sources:
- Laravel Eloquent mutators & casting docs. [1]
- Illuminate\Contracts\Database\Eloquent\CastsAttributes API. [2]
🌐 Web query:
Laravel attribute accessor null value type coercion
💡 Result:
Short answer: If your accessor/get closure is type-hinted for a concrete non‑nullable type (e.g. string, int) and the stored column is NULL, PHP will receive null and you'll get a TypeError — so either accept null/mixed or handle it inside the accessor.
Why and how to fix (concise):
- The Attribute get closure can receive null; the docs show the closure may be declared as mixed (or accept a nullable type). (laravel.com)
- Fix options:
- Use mixed or nullable type: get: fn (?string $value) => $value === null ? null : ucfirst($value)
- Remove the strict type hint and handle null in the closure: get: fn ($value) => $value ? ucfirst($value) : null
- If you need typed objects, return them only when value is not null (or throw/convert as appropriate).
Example:
- Safe nullable string:
protected function firstName(): Attribute
{
return Attribute::make(
get: fn (?string $value) => $value === null ? null : ucfirst($value),
);
}
(Use ?int, ?float, etc. for other types as needed.) (laravel.com)
If you want, paste your accessor and I’ll suggest the exact fix.
Citations:
- 1: https://laravel.com/docs/12.x/eloquent-mutators?utm_source=openai
- 2: https://laravel.com/docs/12.x/eloquent-mutators?utm_source=openai
🌐 Web query:
Accessing nullable columns Laravel Eloquent model
💡 Result:
Short answer
- Accessing a nullable DB column on an Eloquent model is the same as any attribute: $value = $model->column; it will be null if the column is NULL in the DB. [1][2]
Common patterns
-
Read raw attribute (returns null when DB NULL):
$val = $model->nullable_column; // null if DB NULL. [1][2] -
Use getAttribute (explicit):
$val = $model->getAttribute('nullable_column'); // same behaviour. [1] -
Provide a default with null coalescing:
$val = $model->nullable_column ?? 'default'; -
Use optional() when chaining relations or nested properties:
$val = optional($model->relation)->nullable_column; // safe if relation is null. [3] -
Use an accessor or Attribute mutator to transform/null => default:
public function getNullableColumnAttribute($value)
{
return $value ?? 'default';
}
Or using the Attribute class for Laravel 8+ return types. [2][3]
Notes
- Casting (e.g., protected $casts = ['col' => 'string']) does not convert a DB NULL to a non-null value — it keeps null unless your accessor transforms it. [2]
- If you need to detect whether the attribute existed vs. was null, inspect $model->getAttributes() or array_key_exists('col', $model->getAttributes()).
References
[1] Laravel Eloquent Model API (getAttribute/getAttributeValue).
[2] Laravel HasAttributes trait (attribute/cast behavior, mutators).
[3] StackOverflow example showing accessors/default values and optional() usage.
🌐 Web query:
Eloquent accessor strict return type null safe
💡 Result:
Short answer: accessors must return an Attribute object (method return type Illuminate\Database\Eloquent\Casts\Attribute), and the get callback can return nullable/typed values — use PHP nullable types and the null-safe operator when reading relationships. Example:
-
Define accessor (method returns Attribute):
use Illuminate\Database\Eloquent\Casts\Attribute;
public function phone(): Attribute
{
return Attribute::make(
get: fn (?string $value) : ?string => $value ? format_phone($value) : null,
set: fn (?string $value) : ?string => $value ? normalize_phone($value) : null,
);
} -
Read safely from a possibly-missing relation/attribute:
$formatted = $user->profile?->phone ?? null;
References: Laravel's Eloquent Attribute accessor docs and HasAttributes API (accessor must return Attribute; get callback may return null), plus examples. [1][2]
Sources
[1] Laravel API — HasAttributes / Model attribute accessors.
[2] Examples / guide to Eloquent Accessors & Mutators (Attribute) (Laravel 9+).
🌐 Web query:
Laravel typed accessors nullable attributes
💡 Result:
Short answer — Laravel accessors (the Attribute return type) always return an Attribute object, but the value that Attribute::make(get: ...) returns can be nullable. Handle nullable attributes inside the accessor getter and return null when appropriate.
Examples:
- Nullable primitive (string/int/etc.)
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function nickname(): Attribute
{
return Attribute::make(
get: fn ($value) => $value === null ? null : (string) $value,
set: fn ($value) => $value,
);
}
- Nullable value object
protected function address(): Attribute
{
return Attribute::make(
get: fn ($value, $attributes) => $value === null
? null
: new Address($attributes['address_line_one'], $attributes['address_line_two']),
)->withoutObjectCaching();
}
- Using PHP union/nullable types when you expose a method that constructs/returns an object outside Eloquent:
public function getAddressObject(): ?Address
{
return $this->address; // may be null because accessor may return null
}
Notes:
- The accessor method itself must return an Illuminate\Database\Eloquent\Casts\Attribute instance (per Laravel docs). The closure passed to Attribute::make may return null. [1][2]
- If you return objects from accessors and want Eloquent to keep the same instance across accesses (so modifications sync back), use the default behavior; call withoutObjectCaching() to disable that. [1][3]
References:
[1] Laravel Eloquent Mutators & Casting (accessors) — docs. (laravel.com)
[2] Illuminate\Database\Eloquent\Casts\Attribute API. (api.laravel.com)
[3] Eloquent accessors object caching details. (laravel.com)
If you want, tell me the specific attribute/code you’re working with and I’ll show the exact accessor.
Citations:
- 1: https://laravel.com/docs/11.x/eloquent-mutators?utm_source=openai
- 2: https://api.laravel.com/docs/9.x/Illuminate/Database/Eloquent/Casts/Attribute.html?utm_source=openai
- 3: https://laravel.com/docs/9.x/eloquent-mutators?utm_source=openai
🌐 Web query:
PHP constructor with null parameter type declaration
💡 Result:
Use an explicit nullable type: prefix the type with ? and (optionally) give a default of null.
Example:
a = $a; } } new C(new A()); new C(null); ?>Notes
- The ?T syntax (T|null) was introduced in PHP 7.1 and is the recommended way to allow null. [1]
- Relying on a non-nullable type plus a default null (e.g. A $a = null) creates an implicit nullable and is deprecated (PHP 8.4) and discouraged (it can also cause compatibility issues with child classes and property promotion). Use ?A instead. [2][3]
Sources
[1] PHP manual — Nullable types / migration notes.
[2] PHP manual — Type declarations (nullable and union types).
[3] php.watch — Implicitly nullable parameter declarations deprecated (PHP 8.4).
🌐 Web query:
Eloquent trait attribute behavior null values
💡 Result:
Short answer: Eloquent will not apply casts to attributes whose value is null — trait-defined accessors/mutators still work (traits' methods are part of the model class), but casts aren't invoked for null values, so you must handle null explicitly in your Attribute/getter or cast. See the docs: "Attributes that are null will not be cast" and the Attribute accessor signature that receives the raw value (which can be null). [1][2]
How to handle it (quick examples)
- In an Attribute accessor (works in a trait or on the model), handle null: return Attribute::make(get: fn($value, $attrs) => $value === null ? null : /* transform */);. [2]
- Or use the null coalescing/default inside get: fn($v) => $v === null ? 'default' : transform($v).
- If you need casting even for null, do the null-handling yourself in the accessor/mutator.
Sources
- Laravel docs — Mutators & Casting: "Attributes that are null will not be cast." [https://laravel.com/docs/12.x/eloquent-mutators] [1]
- Laravel docs — Accessors (Attribute::make signature / get receives the raw value and attributes). [https://laravel.com/docs/12.x/eloquent-mutators] [2]
🌐 Web query:
Laravel model attribute nullability casting
💡 Result:
Short summary — how Laravel handles nulls with attribute casting and how to make casts nullable:
-
Built-in casts (array/json/datetime/AsCollection/AsArrayObject/etc.) will return null when the DB column is null by default. Use the cast in $casts and a nullable DB column to get null in PHP. (api.laravel.com)
-
Custom cast classes (implementing CastsAttributes or the Castable contract) receive the raw DB value; you must decide how to handle null in your get/set methods. Many packages support a cast-parameter like ":nullable" so the caster will be instantiated or treat null appropriately. Example: some DTO casts accept Class::class . ':nullable'. (packagist.org)
-
Backed PHP enums cast by Eloquent can be problematic when the DB value is null. Strategies:
- Append ':nullable' to the cast if the cast implementation supports it (works for some enum/cast libraries). (packagist.org)
- Otherwise implement a small NullableEnum custom cast (or handle null in your Castable::castUsing / CastsAttributes implementation) to return null for null DB values and an enum instance for non-null values. Community examples and fixes are on StackOverflow and packages. (stackoverflow.com)
Quick examples
-
Default JSON/array nullable:
protected $casts = ['meta' => 'array']; // DB column nullable => $model->meta is null when DB is null. (slingacademy.com) -
Custom cast with nullable parameter:
protected $casts = ['settings' => SettingsCast::class . ':nullable']; // many caster packages use this. (packagist.org) -
Nullable enum (if not supported natively): add a NullableEnum cast that returns null when $value is null and Enum::from($value) otherwise (or use a package that adds ':nullable'). (stackoverflow.com)
If you want, tell me which Laravel version and the specific cast (built-in, PHP enum, custom cast/DTO) and I’ll give exact code for the proper nullable implementation.
Citations:
- 1: https://api.laravel.com/docs/11.x/doc-index.html?utm_source=openai
- 2: https://packagist.org/packages/jessarcher/laravel-castable-data-transfer-object?utm_source=openai
- 3: https://packagist.org/packages/mindtwo/native-enum?utm_source=openai
- 4: https://stackoverflow.com/questions/78032787/laravel-eloquent-model-casts-to-enum-allow-nulls?utm_source=openai
- 5: https://www.slingacademy.com/article/laravel-eloquent-storing-arrays-nested-arrays-database/?utm_source=openai
- 6: https://packagist.org/packages/jessarcher/laravel-castable-data-transfer-object?utm_source=openai
- 7: https://stackoverflow.com/questions/78032787/laravel-eloquent-model-casts-to-enum-allow-nulls?utm_source=openai
🌐 Web query:
Eloquent raw attributes null values accessors
💡 Result:
Short answer — use the model's raw attribute helpers instead of the accessor:
-
To read the current raw attribute value (the value in the model's attributes array, unaffected by accessors/casts):
- $raw = $model->getAttributes()['column'] ?? null;
- or $raw = $model->getAttributeFromArray('column');
(both return null if the stored value is null). [1][2]
-
To read the original/raw value loaded from the database (also bypasses accessors/casts):
- $rawOriginal = $model->getRawOriginal('column'); // or pass null to get all originals. [1][2]
-
Reminder: $model->column invokes accessors/casts (mutators). If you need the DB-stored null preserved, use getAttributes()/getAttributeFromArray() or getRawOriginal(). [3]
Sources:
[1] Laravel API — HasAttributes (getRawOriginal, getAttributes, getAttributeFromArray).
[2] Laravel API — Model / HasAttributes docs.
[3] Laravel docs — Eloquent Mutators & Accessors.
🌐 Web query:
PHP nullable parameter constructor typed
💡 Result:
Short answer — make the parameter type explicitly nullable and give it a null default (or include null in a union). Examples:
-
Ordinary constructor parameter:
public function __construct(?string $name = null) { ... } -
Using union types:
public function __construct(string|array|null $value = null) { ... } -
With PHP 8 constructor property promotion:
class User {
public function __construct(public ?string $name = null, private int|null $id = null) {}
}
Notes:
- The ?T shorthand is equivalent to T|null. Use ?T or include |null explicitly. [1][3]
- For promoted properties the nullability must be declared explicitly; implicit nullable-by-default (e.g., string $s = null without ?) is not allowed and is deprecated/error. [1][2]
Sources:
[1] PHP constructor property promotion (PHP 8) — php.watch.
[2] Implicitly marking parameter type nullable deprecated (PHP 8.4) — php.watch.
[3] PHP union types / nullable types (RFC / docs).
🌐 Web query:
Eloquent accessor called null column value
💡 Result:
Short answer: Eloquent will still call your accessor when the underlying column value is NULL — the accessor receives null, so handle that inside it (or check attribute existence on the model first).
Example (traditional accessor):
public function getFooAttribute($value)
{
if ($value === null) {
return null; // or return default/computed value
}
return /* transform $value */;
}
Example (new Attribute cast):
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function foo(): Attribute
{
return Attribute::get(fn ($value) => $value === null ? null : /* transform $value */);
}
If you need to detect whether the column exists on the model (distinguish missing attribute vs present-but-null), use array_key_exists('foo', $this->attributes) or isset($this->attributes['foo']) (note: isset returns false for null). See Laravel docs on accessors/mutators for details. [1]
Sources:
[1] Laravel Eloquent Accessors & Mutators docs. [https://laravel.com/docs/eloquent-mutators]
🌐 Web query:
Laravel attribute accessor return null handling
💡 Result:
Common causes and fixes when an Eloquent accessor returns null
- Understand what value your accessor receives
- The Attribute get callback receives the stored column value (and optionally all attributes). If the column is missing or null the first argument will be null. Use the second $attributes array when you need other columns. [1].
Example:- protected function fullName(): Attribute
{
return Attribute::make(
get: fn (mixed $value, array $attributes) => ($attributes['first_name'] ?? '') . ' ' . ($attributes['last_name'] ?? '')
);
} [1]
- protected function fullName(): Attribute
- Common mistakes to check
- You imported the wrong Attribute class (use Illuminate\Database\Eloquent\Casts\Attribute). If you import the wrong class the accessor won’t run. [3]
- The accessor name must match the attribute (camelCase method for snake_case column). [1]
- Some third‑party drivers/packages override getAttribute (e.g., some MongoDB packages) and can interfere with Laravel’s new Attribute API. If using such a package verify compatibility. [4]
- Accessors aren’t always applied
- Query-builder methods that operate at the database level (pluck, upsert, toBase()/toQuery results) don’t run model accessors — they work on raw DB results. Use collection mapping or fetch models (get()) to apply accessors. [2][4]
- Other techniques if you need defaults or raw values
- Coalesce inside accessor: return $value ?? 'default'.
- Read raw DB value: $this->getRawOriginal('field') or $attributes['field'] inside the get callback. [1]
References
- Laravel docs — Eloquent mutators / Attribute accessors. [1]
- Q/A: class-based accessors not applied to pluck (explanation). [2]
- Community note about wrong Attribute import causing accessors to not run. [3]
- Example issue where third‑party MongoDB model overrides getAttribute. [4]
🌐 Web query:
Eloquent get attribute hook null safety
💡 Result:
Short answer: implement your accessor via Eloquent's accessors (Attribute or getXAttribute) and make the code null‑safe using PHP's nullsafe operator (->?), null coalescing (??)/optional(), or defensive checks.
Why: Eloquent calls getAttribute/getAttributeValue which in turn calls your accessor; if your accessor dereferences a null (relation or value) it will throw — so guard inside the accessor.
Examples
-
Attribute class (Laravel 9+):
protected function profile(): \Illuminate\Database\Eloquent\Casts\Attribute
{
return \Illuminate\Database\Eloquent\Casts\Attribute::make(
get: fn (mixed $value) => $value?->format('Y-m-d') ?? 'n/a'
);
}
(uses PHP nullsafe + null coalescing). [1][2] -
Classic accessor:
public function getFullNameAttribute(): ?string
{
return ($this->first_name ?? '') . ' ' . ($this->last_name ?? '');
}
Or when using a relation:
public function getManagerEmailAttribute(): ?string
{
return $this->manager?->email; // nullsafe operator
}
Or: return optional($this->manager)->email ?? null; for older PHP. [2][3]
References
- Eloquent attribute internals (getAttribute / getAttributeValue). [1]
- Eloquent accessors (Attribute::make and getFooAttribute patterns). [2]
- Safe navigation / nullsafe operator (PHP optional chaining). [3]
If you want, tell me your Laravel and PHP versions and I’ll give the exact accessor code tailored to them.
🌐 Web query:
PHP constructor parameter null type check
💡 Result:
Use an explicit nullable type and then check with strict comparison or the null-coalescing operator.
Examples:
-
explicit nullable type + strict null check
name = 'fallback'; } else { $this->name = $name; } } } -
null-coalescing to set a default
name = $name ?? 'fallback'; }
Notes:
- Prefer ?Type (or Type|null) instead of using a non-nullable type with a default null — implicitly nullable parameter types are deprecated in recent PHP versions; use explicit nullable declarations. [1][2]
Sources:
[1] PHP manual: Nullable types (migration notes).
[2] php.watch / RFC: deprecate implicitly nullable parameter types.
🌐 Web query:
Laravel static analysis accessing nullable attributes
💡 Result:
Short answer — make the nullability explicit for your static analyzer and/or change the accessor so the analyzer can prove the type. Common approaches:
-
Add a phpdoc property that includes nullability (best quick fix)
- Example: /** @Property string|null $foo */ class User extends Model {}. This tells PHPStan/Psalm/IDE that $user->foo may be null. [1]
-
Generate model phpdocs automatically
- Use barryvdh/laravel-ide-helper or similar to generate @Property annotations for all model attributes so analyzers and IDEs know types. [2]
-
Use typed accessors / casts or explicit return types
- Implement Attribute accessors (or casts) that return a concrete type (or ?type) so the analyzer can infer the right type from $model->foo. Also annotate accessors if needed. [1]
-
Narrow the type in code where you use it
- Use null checks, the nullsafe operator (?->), instanceof, or explicit assertions (e.g. assert($model->foo !== null)) so PHPStan stops warning about accessing a nullable value. PHPStan enforces this at higher levels. [3]
Recommended workflow
- If you want global, long-term correctness: add proper casts/Attribute accessors + run ide-helper to generate docblocks + use Larastan (PHPStan plugin for Laravel). For quick fixes, add @Property Type|null to the model. [1][2][3]
Sources
- Laravel Eloquent attributes / accessors docs. [1]
- Discussion / examples for using @Property and generated model phpdocs (ide-helper). [2]
- PHPStan notes on null-safety and why you must narrow nullable types (explains errors and required null checks). [3]
🌐 Web query:
Eloquent accessor return null or instance type
💡 Result:
Yes — an Eloquent accessor can return either null or an object instance. Use the Attribute accessor (modern API) and simply return null when the underlying data is absent, or return your value object when present. Eloquent will retain (cache) instances returned by accessors unless you disable object caching.
Example:
protected function address(): \Illuminate\Database\Eloquent\Casts\Attribute
{
return \Illuminate\Database\Eloquent\Casts\Attribute::make(
get: fn ($value, array $attributes) => $attributes['address_line_one'] === null
? null
: new \App\ValueObjects\Address(
$attributes['address_line_one'],
$attributes['address_line_two']
)
);
}
If you do not want Eloquent to keep and reuse the same returned instance, call withoutObjectCaching() on the Attribute. See Laravel docs for details. [1][2]
Sources
- Laravel: Eloquent mutators & casting (accessors, value objects, object caching / withoutObjectCaching). [1]
- Laravel (latest): Eloquent mutators & casting (same topics). [2]
🌐 Web query:
Laravel accessing nullable database columns in accessors
💡 Result:
Short answer: check for null inside the accessor and return an appropriate fallback (null, an empty value, or computed value). Use null coalescing or conditional logic in the old get{Attr}Attribute style or the new Attribute cast.
Examples:
- Classic accessor (older style)
public function getPhoneAttribute($value)
{
return $value === null ? null : format_phone($value);
}
- New Attribute class (Laravel 8/9+)
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function phone(): Attribute
{
return Attribute::make(
get: fn ($value) => $value === null ? null : format_phone($value),
);
}
- If you want an empty array instead of null for JSON/nullable columns:
protected function meta(): Attribute
{
return Attribute::make(
get: fn ($value) => $value === null ? [] : json_decode($value, true),
);
}
Tips:
- Don’t call methods on $value without checking for null (avoid $value->something when $value may be null).
- Use the original $value passed into the accessor; Eloquent supplies the raw DB value.
- For many cases null coalescing is enough: get: fn($v) => $v ? transform($v) : null.
References: Laravel accessors / Attribute docs. [1][2]
Sources
[1] Laravel docs — Eloquent: Mutators & Casting (accessors).
[2] Laravel API — Illuminate\Database\Eloquent\Casts\Attribute.
🌐 Web query:
PHP typed properties null strict mode
💡 Result:
Quick summary — how PHP typed properties, nullability and strict mode interact:
-
Typed properties were added in PHP 7.4. A declared property has an "uninitialized" state (not the same as null) and accessing it before initialization throws an Error. [1][2]
-
To allow null you must declare the property nullable: use ?Type and (if you want) set = null. Without the ? the property cannot have a null default and assigning null (or declaring = null) is disallowed. Example: public ?string $name = null; vs public string $name; (uninitialized). [1][2]
-
declare(strict_types=1) affects type enforcement/coercion on assignments: in coercive mode PHP will attempt scalar casts (e.g. int -> string), whereas in strict mode mismatched scalar assignments throw a TypeError. The strictness is applied at the write-location. This applies to typed properties as well. [3][2]
References:
- PHP manual — Properties / typed properties. [1]
- php.watch — Typed properties (uninitialized state, nullable behavior, examples). [2]
- PHP manual — Type declarations / strict typing. [3]
🌐 Web query:
Eloquent real world null handling accessors
💡 Result:
Here are pragmatic patterns for handling nulls in real-world Eloquent accessors, with short examples.
- Use relationship defaults (Null Object) — removes null checks for related models.
- Define relation with withDefault():
- public function user(): BelongsTo { return $this->belongsTo(User::class)->withDefault(); }
- You can pass attributes or a closure to withDefault(). [1]
- Return safe defaults in attribute accessors (Attribute class).
- Modern accessor pattern:
- protected function title(): Attribute { return Attribute::make(get: fn($v) => $v ?? 'Untitled'); }
- Works for attributes stored as null. [2][9]
- Use optional() / null-safe chaining at call sites when you need to access possibly-null objects.
- $name = optional($post->user)->name ?? 'Guest';
- Or PHP 8 null-safe: $post->user?->name ?? 'Guest'. [3]
- Apply the Null Object pattern or value objects for richer defaults.
- Return a lightweight object instead of null (e.g., new EmptyProfile) so callers can call methods without checks. Use withDefault for relations or cast attributes to value objects. [1][2]
- Use accessor logic for presentation vs. domain data separation.
- Keep DB values unchanged; accessor returns presentation defaults (e.g., '—' or formatted output). Use mutators/casts when persisting. [2]
- Quick fallback in older Eloquent accessor style (still valid):
- public function getImageAttribute($value) { return $value ?? 'default.jpg'; } [4]
When to use which:
- Use withDefault for relations you always want an object for.
- Use Attribute accessors or value-object casts when attribute needs normalization or richer behavior.
- Use optional()/?-> for ad-hoc, one-off safe reads.
Sources: Laravel docs on default models/withDefault (relations) and accessors/Attribute; optional() helper examples; community examples. [1][2][3][4]
References
[1] Laravel Eloquent relationships — Default Models / withDefault.
[2] Laravel Eloquent Mutators & Casting — Accessors (Attribute class).
[3] optional() helper usage (Laravel).
[4] Community example: default attribute in getXAttribute() (StackOverflow).
🌐 Web query:
Typed accessor return null value Laravel
💡 Result:
Common causes and fixes when a typed accessor (the new Attribute-based accessor) returns null in Laravel:
- Wrong/missing return type or import
- The accessor must return an Illuminate\Database\Eloquent\Casts\Attribute instance and you must import it: use Illuminate\Database\Eloquent\Casts\Attribute;. If you forget the import or return the wrong type the accessor won’t be applied. [1]
- Method name mismatch / old vs new syntax
- New syntax (Laravel 9+): public function foo(): Attribute { return Attribute::make(get: fn($value,$attrs)=>...); }.
- Old syntax (Laravel 8 and earlier): function getFooAttribute($value) { ... } — using the wrong form for your Laravel version can result in null. [1]
- Your get/set callback must return a value
- The get and set callbacks must return the transformed value (or array for set). If they don’t return anything the result is null. [2]
- Attribute is being treated as a relation / driver incompatibility
- Third‑party Eloquent drivers (e.g., jenssegers/laravel-mongodb) or packages that override getAttribute() can cause Laravel’s Attribute accessors not to be executed (the package may treat the name as a relation), producing null. If you use such a package check its Model::getAttribute implementation or switch to a compatible package/version. [3]
- Name collision with actual DB column / casts / appends / relation
- If you have a real column, cast, appended attribute, or a relation with the same name the accessor’s behavior can be affected. Verify $casts, $appends and relations don’t conflict with the accessor name. [1]
Quick debug checklist
- Add dd() or a simple return inside the accessor to confirm it’s executed.
- Confirm method signature and import: public function myAttr(): Attribute { return Attribute::make(get: fn($v,$a)=> 'test'); } and then dd($model->myAttr).
- Check for packages overriding getAttribute() (e.g., mongodb driver).
- Ensure get/set callbacks return values (set should return the stored value or array).
References
- Laravel HasAttributes / Attribute accessors (docs): Laravel docs on Attribute accessors and HasAttributes. [1]
- Note about get/set callbacks needing to return values (community discussions). [2]
- Example issue where a MongoDB driver caused the accessor not to run (third‑party driver overriding getAttribute). [3]
Sources
[1] Laravel docs — HasAttributes / Attribute accessors.
[2] Community notes about get/set callbacks returning values (discussion threads).
[3] Example report where jenssegers/laravel-mongodb overwrote getAttribute and broke Laravel 9 accessors.
🌐 Web query:
UInt8 Int8 Float32 typed constructor PHP null
💡 Result:
Short answer: PHP has no built-in UInt8/Int8/Float32 scalar types. Use int/float for numeric values and make parameters/properties nullable with ?type or a union that includes null (e.g. int|null or ?int). If you need true 8‑bit or 32‑bit float semantics, use FFI (C types / CData), pack()/unpack(), or wrap/validate values in a value-object.
References:
- PHP union/nullable/typed properties (PHP 7.4/8.0+). [1]
- PHP typed properties / scalar types overview. [2]
- PHP FFI / C type mappings (for int8/uint8 etc.). [3]
Sources:
[1] PHP 8 union/nullable types summary. (php.watch)
[2] Typed properties / scalar types in PHP 7.4/8.0. (infoq.com)
[3] FFI type list (int8/uint8 etc.). (msp-greg.github.io)
If you want, tell me which approach you prefer (simple nullable int/float, FFI, pack/unpack, or a value object) and I’ll show example code.
Citations:
- 1: https://php.watch/versions/8.0/union-types?utm_source=openai
- 2: https://www.infoq.com/articles/php7-new-type-features/?utm_source=openai
- 3: https://msp-greg.github.io/ffi/file.Types.html?utm_source=openai
🌐 Web query:
Eloquent casting null values attributes
💡 Result:
Short answer: by design Eloquent does not run casts when the DB value is null — nulls are returned as null. See Laravel docs: "Attributes that are null will not be cast." [1]
What to do if you need a non-null value
- Provide a default in the model attributes: protected $attributes = ['options' => []]; (will be used for new models).
- Use an accessor to return a default when null:
public function getOptionsAttribute($value) { return $value === null ? [] : $this->castAttribute('options', $value); } - Create a custom cast (implements Castable or CastsAttributes) and handle null inside its get() method (or use packages like Spatie Data which allow a default parameter to instantiate a cast even for null DB values). [2][3]
- For package-specific casts (Spatie laravel-data), you can pass a default flag to instantiate the caster for nulls. [3]
References
[1] Laravel docs — Eloquent: Mutators & Casting. [2] Laravel docs (earlier versions) — same note that nulls are ignored. [3] Spatie laravel-data docs — using defaults for null database values.
🌐 Web query:
Laravel accessor null type safety best practices 2024
💡 Result:
Short best-practices for null-safe Eloquent accessors (2024)
-
Always use the Attribute return type on accessors. Laravel requires accessor methods to return Illuminate\Database\Eloquent\Casts\Attribute — this ensures Eloquent recognizes the accessor. [1][4]
-
Make the accessor’s get callback explicitly handle null/nullable types. Type the parameter and return value (or use nullable unions) so static analysis and runtime behaviour are clear. Example:
protected function title(): Attribute
{
return Attribute::make(
get: fn(?string $value): ?string => $value === null ? null : trim($value)
);
}
This avoids unexpected TypeErrors and documents that null is allowed. [1] -
Prefer casts / typed properties for primitive conversions, and use accessors for transformation logic only. Use Eloquent casts (dates, arrays, enums) where appropriate to centralize null/typing behaviour. [1]
-
Protect against partially-hydrated models (missing attributes) in production — enable Laravel’s prevention so missing attributes throw instead of silently returning null:
Model::preventAccessingMissingAttributes();
This catches cases where null comes from not selecting the column (not the DB value). [2][3] -
Use static analysis / PHPDoc + nullable union types for tools (PHPStan/PSalm) and keep runtime checks for user-input/DB values. Example PHPDoc: /** @return string|null */ and/or use ?string in closures. This helps CI catch unsafe assumptions.
References
- Laravel docs — Eloquent mutators & accessors. [1]
- HasAttributes trait / API (how missing attributes and getAttribute behave). [2]
- Discussion of silent-null danger and Model::preventAccessingMissingAttributes(). [3]
- Community notes on required :Attribute return type. [4]
🌐 Web query:
Eloquent accessor null column values best practice
💡 Result:
Short best-practice summary — how to handle nullable DB columns inside Eloquent accessors:
-
Prefer the Attribute cast API and accept nullable values (use null coalescing or return types that allow null). Example:
protected function title(): Attribute
{
return Attribute::make(
get: fn (?string $value, array $attributes) => $value ?? 'Untitled'
);
}
This uses the $value passed from the column and falls back to a safe default. [1][2] -
For accessors that read relationships or other objects (which can be null), be defensive — use PHP nullsafe (?->) or Laravel optional() so your accessor won’t throw when the related model is missing:
return optional($this->author)->name;
// or (PHP 8+)
return $this->author?->name;
This avoids errors when relations are null. [3] -
Don’t rely on accessors to magically populate relationship data — accessors receive the raw column value; if you need relationship data, explicitly check/load the relation inside the accessor or in the getter. If you compute complex objects, consider returning value objects (and be aware of Eloquent’s object-caching behavior). [1][2][4]
-
Use casts / database defaults when appropriate:
- Use casts (array, datetime, custom casts) to normalize DB values automatically.
- Use DB-level defaults for sensical default values so accessors get predictable input. [1]
References
- Laravel Accessors & Mutators (Attribute API). [1]
- Eloquent mutators/casting notes (get callback signature, object caching). [2]
- Using optional() / nullsafe operator to guard against null relations (community guidance). [3]
If you want, tell me which Laravel version you use and I’ll give an exact code example tailored to it.
🌐 Web query:
Strict types PHP constructor null parameter handling
💡 Result:
Short answer: In strict mode (declare(strict_types=1)), a constructor parameter declared with a non-nullable type will throw a TypeError if passed null. To accept null, explicitly declare the parameter nullable (e.g. ?Type $x or Type|null $x). Relying on a default = null without ? (implicit nullable) is deprecated (PHP 8.4) — use the explicit nullable syntax instead. [1][2][3]
Examples:
- Disallow null (throws TypeError in strict mode)
public function __construct(string $s) { … } - Allow null
public function __construct(?string $s) { … }
// or in PHP 8+: public function __construct(string|null $s) { … }
References:
- PHP manual — Type declarations / strict typing & nullable types. [1]
- PHP manual — Nullable types examples / old implicit-null caveat. [2]
- PHP.Watch — implicitly nullable parameter deprecation (PHP 8.4). [3]
🌐 Web query:
Laravel Eloquent attribute casting null safety 2025
💡 Result:
Short answer
- Laravel's built‑in casting will skip casting for null database values: "Attributes that are null will not be cast." [1].
- If you need "null‑safe" casting (i.e., transform nulls or return a specific object/value when DB value is null), implement that in a custom cast (CastsAttributes) or in an Attribute accessor — the cast/get callback receives the raw value (which may be null) and can return null or any other value. [2][3]
How to do it (minimal examples)
-
Built‑in casts: null values are left as-is (no cast). Use only when you want null to stay null. [1]
-
Custom cast (make:cast) — handle null explicitly:
-
Attribute accessor (PHP/laravel Attribute) — also receives nullable $value, so you can guard:
public function options(): Attribute
{
return Attribute::get(fn($value) => is_null($value) ? collect() : json_decode($value, true));
} -
PHP nullsafe operator (?->) can help when chaining on casted objects that may be null (useful client‑side when using the cast result): $user->profile?->phone. See PHP 8's nullsafe operator for chaining semantics. [3]
References
- Laravel docs — Mutators & Casting (notes: "Attributes that are null will not be cast."). [1]
- Laravel API / CastsAttributes contract (get may return TGet|null; implement custom casts to handle null). [2]
- PHP nullsafe operator (?->) RFC / docs (useful for safely accessing possibly‑null cast results). [3]
Sources
[1] Laravel — Eloquent: Mutators & Casting. (laravel.com)
[2] Illuminate\Contracts\Database\Eloquent\CastsAttributes (interface / docs). (api.laravel.com)
[3] PHP nullsafe operator (RFC / PHP 8 release notes). (wiki.php.net)
Citations:
- 1: https://laravel.com/docs/12.x/eloquent-mutators?utm_source=openai
- 2: https://api.laravel.com/docs/12.x/Illuminate/Contracts/Database/Eloquent/CastsAttributes.html?utm_source=openai
- 3: https://wiki.php.net/rfc/nullsafe_operator?utm_source=openai
🌐 Web query:
PHP constructor null type declaration modern
💡 Result:
Short summary — modern PHP nullable constructor types
-
Use the explicit nullable syntax ?Type for constructor parameters and typed properties (introduced in PHP 7.1 and used in constructors/property promotion). Example:
public function __construct(?string $name) { $this->name = $name; }. [1][2] -
Since PHP 8.0 you may also use union syntax Type|null (equivalent to ?Type) and constructor property promotion to declare and assign properties in one line:
public function __construct(private ?string $name, private int|null $id = null) {}. [3][4] -
Do NOT rely on implicit nullability (e.g., public function __construct(string $s = null)) — that has been deprecated/removed in modern PHP and will emit deprecation/errors; always use ?Type or Type|null. [4][5]
References
- PHP Manual — Migration to 7.1 (nullable types).
- PHP Manual — Typed properties (usage in constructors).
- RFC / docs — Constructor property promotion (PHP 8.0).
- PHP 8.0 union types / nullable via Type|null.
- RFC — deprecate implicitly nullable types (implementation notes).
🌐 Web query:
Eloquent typed attribute accessor null value
💡 Result:
If your accessor is typed like get: fn(string $value) => ... and the DB value can be NULL, PHP will throw a TypeError. Make the accessor accept a nullable type and handle null explicitly.
Example:
protected function title(): Attribute
{
return Attribute::make(
get: fn (?string $value) => $value === null ? null : ucfirst($value),
);
}
Notes:
- Laravel passes the raw column value to the accessor (so it may be null). [1]
- Also: attributes that are null are not cast by Eloquent's casts, so expect nulls to reach your accessor. [2]
Sources:
[1] Laravel docs — Eloquent Mutators & Accessors.
[2] Laravel docs — Attribute casting behavior (nulls are not cast).
🌐 Web query:
Laravel nullable database columns accessing in model
💡 Result:
Short answer
- A nullable DB column (->nullable() in migrations) becomes null on the model when empty; access it with $model->column which will be null. Use PHP null-coalescing ($model->col ?? 'default') or optional() for relations. [1]
Key details and examples
- Make column nullable (migration)
- $table->string('phone')->nullable();. This allows NULL in the DB. [1]
- Accessing the nullable attribute
- If the DB value is NULL, $user->phone === null.
- Provide a default inline: $phone = $user->phone ?? '—';
- Casting and null behavior
- You can define casts on the model (protected $casts or casts() method). Attributes that are null will not be cast (they remain null). So casting doesn't convert null to another type; handle defaults yourself. [2]
Example:
protected $casts = [
'is_admin' => 'boolean',
];
- Use accessors / Attribute cast to normalize nulls
- Create an accessor to return a default or transformed value when attribute is null:
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function phone(): Attribute
{
return Attribute::make(
get: fn($value) => $value ?? 'no-phone'
);
}
This returns 'no-phone' when DB value is null. [3]
- Relations that may be null
- For nullable foreign keys, $model->relation may be null. Use optional($model->relation)->name or $model->relation?->name (PHP 8) or $model->relation ?? 'default'.
References
- Migrations (nullable columns). [1]
- Eloquent mutators & casting (null casting behavior). [2]
- Attribute accessor API (custom accessors to handle null). [3]
🌐 Web query:
PHP constructor parameter null type declaration safe
💡 Result:
Short answer: Yes — but prefer explicit nullable types.
Details and safe practice:
- Use ?Type $param = null or Type|null $param = null (both explicit) to allow null. [1]
- Implicitly making a type nullable by writing Type $param = null (without ? or |null) is deprecated as of PHP 8.4 — convert these to explicit nullable forms. [2][3]
- Note: a nullable type does not by itself make a parameter optional — you still need the = null default to allow callers to omit the argument; otherwise omitting it throws ArgumentCountError. [1][4]
Recommendation: audit code for signatures like Foo $x = null and change to ?Foo $x = null or Foo|null $x = null to avoid deprecation notices and future issues. [2][3]
🌐 Web query:
Eloquent accessor null type error prevention
💡 Result:
Short answer: the error comes from a PHP return-type mismatch — your accessor/cast is returning null but the function signature (or PHP doc) requires a non-null type. Fixes: make the return type nullable or explicitly return a non-null value, and handle null inside the accessor/cast.
How to fix (pick whichever fits your case):
-
Attribute accessors (recommended Laravel approach)
- Accept null and return null when appropriate in the get callback:
- protected function foo(): Attribute
{
return Attribute::make(
get: fn ($value) => $value === null ? null : (string) $value,
);
}
- protected function foo(): Attribute
- Make sure you import Illuminate\Database\Eloquent\Casts\Attribute. [1]
- Accept null and return null when appropriate in the get callback:
-
Custom Casts (CastsAttributes)
- Use nullable return types in your get signature and return null when DB value is null:
- public function get(Model $model, string $key, mixed $value, array $attributes): ?array
{
return $value === null ? null : json_decode($value, true);
}
- public function get(Model $model, string $key, mixed $value, array $attributes): ?array
- Or keep signature mixed and ensure you return the declared type or null. This prevents "Return value must be of type ..." errors. [2][3]
- Use nullable return types in your get signature and return null when DB value is null:
-
Other tips
- If you declare typed callbacks (e.g., fn(string $value) => ...), change to allow null (fn($value) => ...) or use ?string in PHP 8.0+ where applicable.
- Ensure you’re using the correct accessor syntax for your Laravel version and not shadowing methods (wrong import or third‑party driver). [1]
References:
- Laravel docs — Eloquent mutators & casting (accessors using Attribute). [1]
- Stack Overflow discussion showing CastsAttributes get() return-type error and solution (handle null / correct type). [2]
Sources:
[1] Laravel — Eloquent: Mutators & Casting (Accessors & Attribute::make). (laravel.com)
[2] StackOverflow — App\Casts\AsJson::get(): Return value must be of type ?array, string returned. (stackoverflow.com)
Citations:
- 1: https://laravel.com/docs/9.x/eloquent-mutators?utm_source=openai
- 2: https://stackoverflow.com/questions/71936813/how-to-automatically-cast-a-model-attribute-before-inserting-or-updating-in-lara?utm_source=openai
🌐 Web query:
Laravel model attribute null safety typing patterns
💡 Result:
Short summary — patterns for null-safe typing of Eloquent model attributes:
-
Use Eloquent casts for primitive/structured types (array, collection, datetime, custom Cast classes). Note: attributes that are null are not cast — handle nulls in code or cast class. [1][2]
-
Use custom Cast classes (implements CastsAttributes) or Laravel’s built-in AsCollection/AsArrayObject/AsStringable to return typed objects (and explicitly allow nulls in the cast result). Good for value objects / domain types. [1][5][8]
-
Use Attribute accessors/mutators (Illuminate\Database\Eloquent\Casts\Attribute) to centralize conversion and null handling. Inside the accessor you can return null or a typed value (and annotate with PHPDoc/return types as needed). [6][7]
-
Surface PHP-level null-safety with union/nullable types in your model-level helpers / value objects:
- In PHP 8+, prefer ?string or string|null for method and value-object properties. Use typed DTOs/value objects returned by casts/accessors so callers get correct nullable types. (PHP union/nullable types reference). [2]
-
Defensive patterns:
- Always check for null when reading attributes that may be null, or provide a non-null default in the accessor/cast.
- For collections/arrays/datetime, return an empty collection/nullable DateTimeImmutable or null consistently from your cast/accessor.
- Document attribute nullability in PHPDoc on model methods and value objects for static analysis (phpstan/psalm).
Minimal examples
-
Built-in cast (returns null if DB is null):
protected $casts = ['options' => 'array'];. Access may be null; handle or convert in accessor. [2][3] -
Custom cast that returns a ValueObject|null:
class PriceCast implements CastsAttributes { public function get($model,$key,$value,$attrs){ return $value===null ? null : new Price((int)$value); } } then protected $casts = ['price'=>'App\Casts\PriceCast'];. [1][8] -
Attribute accessor for defaulting null:
protected function title(): Attribute { return Attribute::get(fn($v)=> $v === null ? '' : (string)$v); }. Use ?string on helpers that return the attribute. [6]
References
- Laravel Eloquent Mutators & Casting (docs). [1][6]
- PHP union / nullable types (PHP 8). [2]
- Laravel built-in casts (AsCollection / AsArrayObject / custom casts). [5][8]
🌐 Web query:
Eloquent casting nullable columns return type
💡 Result:
Short answer
- Eloquent does not cast attributes when the stored value is null — you'll get null back for nullable columns, not the cast type. [1]
- For custom casts/accessors, explicitly handle/return null and annotate your model properties/return types as nullable (e.g. Type|null). [1][2]
Examples
-
Built-in casts (nullable column)
protected $casts = [
'age' => 'integer',
];
// If DB has age = NULL, $model->age === null (not 0). [1] -
Custom cast (handle null)
// Integer validation rules | ||
Validator::extend('int8', Int8Rule::class); | ||
Validator::extend('int16', Int16Rule::class); | ||
Validator::extend('int32', Int32Rule::class); | ||
Validator::extend('int64', Int64Rule::class); | ||
Validator::extend('uint8', UInt8Rule::class); | ||
Validator::extend('uint16', UInt16Rule::class); | ||
Validator::extend('uint32', UInt32Rule::class); | ||
Validator::extend('uint64', UInt64Rule::class); | ||
|
||
// Float validation rules | ||
Validator::extend('float32', Float32Rule::class); | ||
Validator::extend('float64', Float64Rule::class); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix Validator::extend registration (must be Closure or Class@method)
Passing the Rule class name directly will not work. Wrap each rule in a closure and provide the default message via the third parameter.
- Validator::extend('int8', Int8Rule::class);
- Validator::extend('int16', Int16Rule::class);
- Validator::extend('int32', Int32Rule::class);
- Validator::extend('int64', Int64Rule::class);
- Validator::extend('uint8', UInt8Rule::class);
- Validator::extend('uint16', UInt16Rule::class);
- Validator::extend('uint32', UInt32Rule::class);
- Validator::extend('uint64', UInt64Rule::class);
- // Float validation rules
- Validator::extend('float32', Float32Rule::class);
- Validator::extend('float64', Float64Rule::class);
+ Validator::extend('int8', fn($a,$v,$p,$val) => (new Int8Rule())->passes($a,$v), (new Int8Rule())->message());
+ Validator::extend('int16', fn($a,$v,$p,$val) => (new Int16Rule())->passes($a,$v), (new Int16Rule())->message());
+ Validator::extend('int32', fn($a,$v,$p,$val) => (new Int32Rule())->passes($a,$v), (new Int32Rule())->message());
+ Validator::extend('int64', fn($a,$v,$p,$val) => (new Int64Rule())->passes($a,$v), (new Int64Rule())->message());
+ Validator::extend('uint8', fn($a,$v,$p,$val) => (new UInt8Rule())->passes($a,$v), (new UInt8Rule())->message());
+ Validator::extend('uint16', fn($a,$v,$p,$val) => (new UInt16Rule())->passes($a,$v), (new UInt16Rule())->message());
+ Validator::extend('uint32', fn($a,$v,$p,$val) => (new UInt32Rule())->passes($a,$v), (new UInt32Rule())->message());
+ Validator::extend('uint64', fn($a,$v,$p,$val) => (new UInt64Rule())->passes($a,$v), (new UInt64Rule())->message());
+ // Float validation rules
+ Validator::extend('float32', fn($a,$v,$p,$val) => (new Float32Rule())->passes($a,$v), (new Float32Rule())->message());
+ Validator::extend('float64', fn($a,$v,$p,$val) => (new Float64Rule())->passes($a,$v), (new Float64Rule())->message());
With messages supplied here, the custom replacers become optional.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// Integer validation rules | |
Validator::extend('int8', Int8Rule::class); | |
Validator::extend('int16', Int16Rule::class); | |
Validator::extend('int32', Int32Rule::class); | |
Validator::extend('int64', Int64Rule::class); | |
Validator::extend('uint8', UInt8Rule::class); | |
Validator::extend('uint16', UInt16Rule::class); | |
Validator::extend('uint32', UInt32Rule::class); | |
Validator::extend('uint64', UInt64Rule::class); | |
// Float validation rules | |
Validator::extend('float32', Float32Rule::class); | |
Validator::extend('float64', Float64Rule::class); | |
// Integer validation rules | |
Validator::extend('int8', fn($a,$v,$p,$val) => (new Int8Rule())->passes($a,$v), (new Int8Rule())->message()); | |
Validator::extend('int16', fn($a,$v,$p,$val) => (new Int16Rule())->passes($a,$v), (new Int16Rule())->message()); | |
Validator::extend('int32', fn($a,$v,$p,$val) => (new Int32Rule())->passes($a,$v), (new Int32Rule())->message()); | |
Validator::extend('int64', fn($a,$v,$p,$val) => (new Int64Rule())->passes($a,$v), (new Int64Rule())->message()); | |
Validator::extend('uint8', fn($a,$v,$p,$val) => (new UInt8Rule())->passes($a,$v), (new UInt8Rule())->message()); | |
Validator::extend('uint16', fn($a,$v,$p,$val) => (new UInt16Rule())->passes($a,$v), (new UInt16Rule())->message()); | |
Validator::extend('uint32', fn($a,$v,$p,$val) => (new UInt32Rule())->passes($a,$v), (new UInt32Rule())->message()); | |
Validator::extend('uint64', fn($a,$v,$p,$val) => (new UInt64Rule())->passes($a,$v), (new UInt64Rule())->message()); | |
// Float validation rules | |
Validator::extend('float32', fn($a,$v,$p,$val) => (new Float32Rule())->passes($a,$v), (new Float32Rule())->message()); | |
Validator::extend('float64', fn($a,$v,$p,$val) => (new Float64Rule())->passes($a,$v), (new Float64Rule())->message()); |
🤖 Prompt for AI Agents
In src/Laravel/PhpDatatypesServiceProvider.php around lines 46 to 59, the
Validator::extend calls register class names directly which is invalid because
extend expects a Closure or Class@method and an optional message; update each
registration to pass a Closure that instantiates the corresponding Rule and
calls its passes method (signature: function($attribute, $value, $parameters,
$validator) { return (new XxxRule())->passes($attribute, $value); }), and supply
the default error message as the third parameter (e.g., 'validation.int8'), so
the custom replacers become optional.
3. Publish the configuration file (optional): | ||
```bash | ||
php artisan vendor:publish --provider="Nejcc\PhpDatatypes\Laravel\PhpDatatypesServiceProvider" | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Confirm publishable assets exist.
Ensure the service provider exposes a publish group/config; otherwise this command fails.
🏁 Script executed:
#!/bin/bash
rg -nP "publishes\(" -C3 src/Laravel | sed -n '1,120p'
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Find the ServiceProvider file
fd -t f "ServiceProvider" src/Laravel
Length of output: 43
🏁 Script executed:
#!/bin/bash
# List all PHP files in src/Laravel to understand structure
fd -t f "\.php$" src/Laravel
Length of output: 693
🏁 Script executed:
#!/bin/bash
# Check if config directory exists and what's in it
fd -t d "config" src/Laravel && find src/Laravel -type d -name "config" -exec ls -la {} \;
Length of output: 197
🏁 Script executed:
#!/bin/bash
# Search for 'publishes' more broadly (case-insensitive, without escaping)
rg -i "publishes" src/Laravel -n
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Inspect the ServiceProvider file content
cat src/Laravel/PhpDatatypesServiceProvider.php
Length of output: 3244
Add $this->publishes()
to the ServiceProvider or remove the vendor:publish instruction from the README.
The ServiceProvider (src/Laravel/PhpDatatypesServiceProvider.php) does not expose publishable assets. While a config file exists at src/Laravel/config/php-datatypes.php
, the boot()
method never calls $this->publishes()
to register it. The vendor:publish command in the README will fail without this configuration.
Either add the publish group to the ServiceProvider:
$this->publishes([
__DIR__ . '/config/php-datatypes.php' => config_path('php-datatypes.php'),
], 'php-datatypes');
Or remove/correct the vendor:publish instruction in the README if publishing is not intended.
🤖 Prompt for AI Agents
In src/Laravel/README.md around lines 20 to 23, the README instructs using php
artisan vendor:publish but the ServiceProvider
(src/Laravel/PhpDatatypesServiceProvider.php) does not register any publishable
assets; either add a publishes() call in the ServiceProvider's boot() to
register src/Laravel/config/php-datatypes.php to
config_path('php-datatypes.php') with a tag (e.g. 'php-datatypes'), or
remove/update the vendor:publish instruction in the README to avoid referencing
a non-registered publish group.
protected $casts = [ | ||
'age' => Int8Cast::class, | ||
'user_id' => 'uint8', // Custom cast | ||
'balance' => 'float32', // Custom cast | ||
]; | ||
} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Validate that string-based casts ('uint8', 'float32') are registered.
If these are custom cast aliases, ensure they’re mapped (e.g., via a caster registry or attribute). Otherwise, Eloquent will treat them as built-ins and error at runtime.
🏁 Script executed:
#!/bin/bash
rg -nP "(register|map).*(uint8|float32)|class\s+.*Cast" -C3 src | sed -n '1,200p'
Length of output: 365
I need to investigate whether these string-based casts are properly registered in Laravel. Let me search for cast registration patterns and service provider configurations.
---
🏁 Script executed:
```shell
#!/bin/bash
# Search for service providers that might register casts
fd -type f -name "*.php" src | xargs rg -l "ServiceProvider|CastResolver|casts" | head -20
Length of output: 332
🏁 Script executed:
#!/bin/bash
# Look for any configuration files or cast mappings
fd -type f -name "*.php" src | xargs rg -nP "uint8|float32" -C2
Length of output: 22917
🏁 Script executed:
#!/bin/bash
# Search for Laravel's cast registration mechanism (e.g., in bootstrap, config, or provider)
fd -type f \( -name "*.php" -o -name "*.config" \) src | xargs rg -nP "register.*cast|map.*cast|castUsing" -i -C2
Length of output: 736
I need to examine the service provider more closely to see if it actually registers the Eloquent casts.
---
🏁 Script executed:
```shell
#!/bin/bash
# Examine the full PhpDatatypesServiceProvider to see cast registration
cat src/Laravel/PhpDatatypesServiceProvider.php
Length of output: 3244
String-based casts 'uint8' and 'float32' are not registered in the service provider.
The README example at lines 109-110 uses these as string-based casts, but the PhpDatatypesServiceProvider
only registers validation rules—not Eloquent cast aliases. The configuration flag 'auto_register_casts' => true
is declared but not implemented. Without cast manager registration, Eloquent will fail at runtime since these are not Laravel built-in casts.
Either implement cast registration in PhpDatatypesServiceProvider::boot()
or update the README example to use class-based casts exclusively (e.g., 'user_id' => UInt8Cast::class
).
🤖 Prompt for AI Agents
In src/Laravel/README.md around lines 107 to 113, the example uses string-based
casts ('uint8' and 'float32') which are not actually registered; update the
README to use the class-based cast names instead (e.g., 'user_id' =>
UInt8Cast::class, 'balance' => Float32Cast::class) so the example matches the
current implementation, or alternatively implement cast alias registration in
PhpDatatypesServiceProvider::boot() by checking the 'auto_register_casts' config
flag and registering the string-to-class aliases there; pick the README update
if you want the minimal change, otherwise implement the provider registration to
support string aliases.
final class Str8Test extends TestCase | ||
{ | ||
public function testValidStr8(): void | ||
{ | ||
$str = new Str8('deadbeef'); | ||
$this->assertEquals('deadbeef', $str->getValue()); | ||
} | ||
|
||
public function testInvalidLength(): void | ||
{ | ||
$this->expectException(InvalidArgumentException::class); | ||
$this->expectExceptionMessage('Str8 must be exactly 8 characters long'); | ||
new Str8('deadbee'); | ||
} | ||
|
||
public function testInvalidHex(): void | ||
{ | ||
$this->expectException(InvalidArgumentException::class); | ||
$this->expectExceptionMessage('Str8 must be a valid hex string'); | ||
new Str8('deadbeeg'); | ||
} | ||
} No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
LGTM with a recommendation.
The test suite correctly validates Str8 behavior, including both length and hex format validation. The tests are well-structured and verify exception messages.
Since Str64 was added in this PR, ensure that a corresponding Str64Test exists with similar coverage (valid construction and invalid length scenarios).
Run this script to check if Str64Test exists:
🏁 Script executed:
#!/bin/bash
# Check for Str64Test file
echo "=== Looking for Str64Test ==="
fd -t f 'Str64Test\.php$'
echo -e "\n=== If found, show test methods ==="
fd -t f 'Str64Test\.php$' --exec cat {}
Length of output: 66
🏁 Script executed:
#!/bin/bash
# Check if Str64.php exists and show the directory structure
echo "=== Looking for Str64.php implementation ==="
fd -t f 'Str64\.php$'
echo -e "\n=== Looking for any Str64-related files ==="
fd -t f 'Str64'
echo -e "\n=== Show directory structure of Tests/Composite/String ==="
find Tests/Composite/String -type f -name '*.php' 2>/dev/null | head -20
echo -e "\n=== Show directory structure of source (likely src or lib) ==="
find . -path ./node_modules -prune -o -type d -name Composite -print 2>/dev/null
Length of output: 325
🏁 Script executed:
#!/bin/bash
# List all test files in the directory
echo "=== Files in Tests/Composite/String ==="
ls -la Tests/Composite/String/ 2>/dev/null || echo "Directory may not exist or is empty"
echo -e "\n=== Str64.php implementation preview ==="
head -50 src/Composite/String/Str64.php 2>/dev/null || echo "File not found"
echo -e "\n=== Str8.php implementation preview for comparison ==="
head -50 src/Composite/String/Str8.php 2>/dev/null || echo "File not found"
Length of output: 2465
Str64Test is missing and must be added for complete test coverage.
The test suite for Str8 is well-structured, but verification confirms that Str64 was added to the codebase without a corresponding test file. All other string types (Str8, Str16, Str32) have dedicated test files in Tests/Composite/String/.
Str64Test should include:
- Valid construction test
- Invalid length test
Note: Unlike Str8, Str64 only validates length (not hex format), so only two test methods are needed.
🤖 Prompt for AI Agents
In Tests/Composite/String/Str64Test.php around lines 1 to 40, add a new PHPUnit
test class Str64Test that mirrors the pattern of other string tests: include
testValidStr64 which constructs a Str64 with a 64-character string and asserts
getValue() returns that string, and include testInvalidLength which expects
InvalidArgumentException with the message "Str64 must be exactly 64 characters
long" and attempts to construct a Str64 with a shorter (or longer) string to
trigger the length validation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 43
🧹 Nitpick comments (72)
src/Exceptions/ImmutableException.php (1)
7-9
: LGTM! Consider adding documentation.The implementation is clean and follows best practices by marking the class as
final
. The empty class body is a valid pattern for domain-specific exceptions, providing semantic meaning and type specificity for error handling.Consider adding a docblock to document when this exception should be thrown:
+/** + * Exception thrown when an attempt is made to modify an immutable object. + */ final class ImmutableException extends \Exception { }phpstan.neon (1)
13-13
: Enable unmatched ignore reporting for maintainability.Disabling
reportUnmatchedIgnoredErrors
makes it difficult to detect when ignored errors become obsolete (e.g., after code fixes). This leads to an unmaintained ignore list.- reportUnmatchedIgnoredErrors: false + reportUnmatchedIgnoredErrors: truesrc/Abstract/AbstractNativeInteger.php (1)
84-97
: Consider using or removing the$operationName
parameter.The
$operationName
parameter is accepted but never used within the method. Either incorporate it into error messages for better debugging (e.g., "Result is out of bounds for operation: $operationName") or remove it if not needed.Apply this diff to enhance error messages:
protected function performOperation( NativeIntegerInterface $other, callable $operation, string $operationName ): static { $result = $operation($this->value, $other->getValue()); if ($result < static::MIN_VALUE || $result > static::MAX_VALUE) { $exceptionClass = $result > static::MAX_VALUE ? \OverflowException::class : \UnderflowException::class; - throw new $exceptionClass('Result is out of bounds.'); + throw new $exceptionClass("Result is out of bounds for operation: $operationName."); } return new static($result); }infection.json (1)
2-2
: Consider increasing the timeout value.A 10-second timeout per test may be too aggressive and could lead to false failures, especially for tests involving complex operations or edge cases. The default Infection timeout is 60 seconds. Consider increasing this to at least 20-30 seconds to provide adequate headroom.
{ - "timeout": 10, + "timeout": 20, "source": {src/Abstract/AbstractBigInteger.php (1)
90-103
: Consider using or removing the$operationName
parameter.Similar to
AbstractNativeInteger::performOperation
, the$operationName
parameter is accepted but never used. For consistency and better error reporting, consider incorporating it into the exception message.Apply this diff to improve error messages:
protected function performOperation( BigIntegerInterface|NativeIntegerInterface $other, callable $operation, string $operationName ): static { $result = $operation($this->value, (string)$other->getValue()); if (bccomp($result, (string)static::MIN_VALUE) < 0 || bccomp($result, (string)static::MAX_VALUE) > 0) { $exceptionClass = bccomp($result, (string)static::MAX_VALUE) > 0 ? \OverflowException::class : \UnderflowException::class; - throw new $exceptionClass('Result is out of bounds.'); + throw new $exceptionClass("Result is out of bounds for operation: $operationName."); } return new static($result); }src/Composite/String/SqlString.php (1)
9-13
: Avoid implying SQL safety; clarify in docs and confirm empty-string policy.This type does not escape/sanitize; the name could suggest safety. Add a prominent note and, if desired, allow empty strings (regex
+
→*
) for parity with other wrappers or explicitly document the non-empty invariant.Suggested doc tweak and optional empty-string allowance:
/** - * SqlString - A string type for SQL strings + * SqlString - A string type for SQL strings. + * + * Note: This class does NOT perform SQL escaping or prevent SQL injection. + * Always use parameterized queries / bound parameters with your DB driver. */ @@ - // Relaxed SQL validation: allow printable characters, whitespace, and common SQL symbols - if (!preg_match('/^[\x20-\x7E\t\n\r]+$/', $value)) { + // Relaxed SQL validation: allow printable ASCII, whitespace, and common SQL symbols + // Change `+` to `*` if empty strings should be allowed. + if (!preg_match('/^[\x20-\x7E\t\n\r]+$/', $value)) { throw new InvalidArgumentException('Invalid SQL string format'); }Also applies to: 24-29
src/Composite/String/Str64.php (1)
16-28
: Enforce token charset (no whitespace) and clarify byte vs character length.Right now only length is checked; 64 spaces or multibyte code points pass. Tokens typically disallow whitespace and often restrict to ASCII/hex/base64url. If the intent is “64 bytes”,
strlen
is correct; if “64 characters”, considermb_strlen
. Recommend at least forbidding whitespace.Option A (generic non‑whitespace, 64 chars):
- if (strlen($value) !== 64) { - throw new InvalidArgumentException('Str64 must be exactly 64 characters long'); - } + if (!preg_match('/^\\S{64}$/', $value)) { + throw new InvalidArgumentException('Str64 must be exactly 64 non-whitespace characters'); + }Option B (hex token):
- if (strlen($value) !== 64) { - throw new InvalidArgumentException('Str64 must be exactly 64 characters long'); - } + if (!preg_match('/^[A-Fa-f0-9]{64}$/', $value)) { + throw new InvalidArgumentException('Str64 must be a 64-character hexadecimal string'); + }CHANGELOG.md (1)
69-70
: Editorial: hyphenate “Floating‑point”.Minor language fix.
-- Floating point types: Float32, Float64 +- Floating-point types: Float32, Float64src/Composite/Result.php (1)
331-341
: Optional: tame __toString() output to avoid leaking sensitive data.
var_export
on arbitrary values may reveal secrets or be very verbose. Consider summarizing non‑scalars.- return $this->isOk() - ? sprintf('Ok(%s)', var_export($this->value, true)) - : sprintf('Err(%s)', var_export($this->value, true)); + if ($this->isOk()) { + $repr = is_scalar($this->value) ? var_export($this->value, true) : get_debug_type($this->value); + return sprintf('Ok(%s)', $repr); + } + $repr = is_scalar($this->value) ? var_export($this->value, true) : get_debug_type($this->value); + return sprintf('Err(%s)', $repr);src/Composite/Union/UnionType.php (2)
46-55
: Validate initial values and optionally set activeType.Constructor stores $initialValues without type checks; a mismatched value fails later. Validate now and (optionally) auto‑activate when a non‑null initial value is provided.
public function __construct(array $typeMap, array $initialValues = []) { if (empty($typeMap)) { throw new InvalidArgumentException('Union type must have at least one possible type'); } $this->typeMap = $typeMap; - foreach ($typeMap as $key => $expectedType) { - $this->values[$key] = $initialValues[$key] ?? null; - } + foreach ($typeMap as $key => $expectedType) { + $value = $initialValues[$key] ?? null; + $this->validateType($value, $expectedType, $key); + $this->values[$key] = $value; + if ($this->activeType === null && $value !== null) { + $this->activeType = $key; + } + } }
255-263
: Confirm castTo semantics (key vs expected type).Allowing castTo('success') when expected type is 'array' may be surprising. If intended, document it; otherwise restrict to expected type only.
Tests/Composite/Union/UnionTypeTest.php (2)
216-226
: Remove unnecessary DateTime aliasing.DateTime exists in core; the alias line is redundant.
- class_exists('DateTime') || class_alias(\DateTime::class, 'DateTime'); - $union = new UnionType([ 'DateTime' => 'DateTime' ]); $union->setValue('DateTime', new \DateTime());
328-339
: Add round‑trip tests for alias keys (‘success’/’error’) across JSON/XML/Binary.Current serialization paths fail when key ≠ expected type. Add tests that set 'success' => ['data' => 'x'], assert toJson→fromJson, toXml→fromXml, toBinary→fromBinary preserve activeType and value. This will guard the fixes proposed in UnionType.
Also applies to: 348-359, 428-435
src/Composite/Option.php (1)
266-286
: Harden JSON (de)serialization with JSON_THROW_ON_ERROR.Avoid silent false returns; normalize errors into InvalidArgumentException.
- public function toJson(): string - { - return json_encode($this->toArray()); - } + public function toJson(): string + { + return json_encode($this->toArray(), JSON_THROW_ON_ERROR); + } - public static function fromJson(string $json): self - { - $data = json_decode($json, true); - if (!is_array($data)) { - throw new InvalidArgumentException('Invalid JSON format for Option'); - } - - return self::fromArray($data); - } + public static function fromJson(string $json): self + { + try { + $data = json_decode($json, true, flags: JSON_THROW_ON_ERROR); + } catch (\JsonException) { + throw new InvalidArgumentException('Invalid JSON format for Option'); + } + return self::fromArray($data); + }.github/workflows/main.yml (3)
18-19
: Update checkout action to v4.- uses: actions/checkout@v2 + uses: actions/checkout@v4
21-27
: Composer cache for faster CI.Add caching to speed up the matrix.
- name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo coverage: none + - name: Cache Composer + uses: actions/cache@v4 + with: + path: | + ~/.composer/cache/files + vendor + key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ matrix.stability }}-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer-${{ matrix.php }}-${{ matrix.stability }}-
11-13
: Remove pcntl from Windows workflow runs—it's unused and creates CI noise.
- pcntl is not a project dependency or used in the codebase; requesting it on Windows is unnecessary since the extension is Unix/Linux-only.
- Modify line 25 to conditionally include pcntl only for ubuntu-latest, or remove it entirely if not needed:
extensions: dom, curl, libxml, mbstring, zip, ${{ matrix.os == 'ubuntu-latest' && 'pcntl,' || '' }}pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
- phpunit ^11.4.2 (composer.json line 26) already satisfies the PHP 8.4 alignment requirement.
README.md (1)
52-54
: PHP 8.4 array functions verified as core; qualify the "15–30% improvement" claim with methodology or link.PHP 8.4 includes array_find(), array_find_key(), array_any(), and array_all() as core functions. Since composer.json requires
^8.4
, no polyfill is needed.However, README lines 123 and 381 state "15-30% performance improvement" and "Results show 15-30% improvement" without linking to actual benchmark output or documenting the test methodology. Benchmark infrastructure exists (
benchmarks/
directory,composer benchmark
script) but results are not documented. Either:
- Link to benchmark output/results in README, or
- Qualify the claim as "internal benchmarks show" or adjust to "can achieve" language
Applies to: lines 52, 75–96, 123–128, 257–269, 546–559
src/Composite/Dictionary.php (1)
147-170
: Consolidate duplicate methods.
toArray()
andgetAll()
have identical implementations, both returning$this->elements
. This duplication creates API confusion about which method consumers should use and increases maintenance burden.Consider one of these approaches:
- Keep only
toArray()
(aligns with other composite types likeArrayAbstraction
andStruct
).- If both are needed for backward compatibility or different semantic meanings, have one delegate to the other and add clear documentation explaining when to use each.
Apply this diff to remove the duplication:
public function toArray(): array { return $this->elements; } /** * Check if the dictionary is empty. * * @return bool */ public function isEmpty(): bool { return empty($this->elements); } /** - * Get a copy of the dictionary with all elements. + * Alias for toArray(). Get all dictionary elements. * * @return array<string, mixed> */ public function getAll(): array { - return $this->elements; + return $this->toArray(); }benchmarks/run_benchmarks.php (2)
5-5
: Consider graceful handling of missing autoloader.If
vendor/autoload.php
is missing, the script will fatal error. For a better developer experience, consider checking file existence and providing a helpful message.Apply this diff:
-require_once __DIR__ . '/../vendor/autoload.php'; +if (!file_exists(__DIR__ . '/../vendor/autoload.php')) { + echo "Error: Composer autoloader not found. Please run 'composer install' first.\n"; + exit(1); +} +require_once __DIR__ . '/../vendor/autoload.php';
16-28
: Add error handling for benchmark execution.If benchmarks throw exceptions (e.g., due to memory limits, invalid data, or implementation bugs), the script will crash without a clear error message. Consider wrapping benchmark execution in try-catch blocks for more resilient execution and clearer error reporting.
Apply this diff:
// Run integer benchmarks echo "Running Integer Benchmarks...\n"; -$integerBenchmark = new IntegerBenchmark(); -$integerResults = $integerBenchmark->runAllBenchmarks(); -$integerBenchmark->printResults($integerResults); +try { + $integerBenchmark = new IntegerBenchmark(); + $integerResults = $integerBenchmark->runAllBenchmarks(); + $integerBenchmark->printResults($integerResults); +} catch (\Throwable $e) { + echo "Error running integer benchmarks: " . $e->getMessage() . "\n"; + exit(1); +} echo "\n" . str_repeat("=", 50) . "\n\n"; // Run array benchmarks echo "Running Array Benchmarks...\n"; -$arrayBenchmark = new ArrayBenchmark(); -$arrayResults = $arrayBenchmark->runAllBenchmarks(); -$arrayBenchmark->printResults($arrayResults); +try { + $arrayBenchmark = new ArrayBenchmark(); + $arrayResults = $arrayBenchmark->runAllBenchmarks(); + $arrayBenchmark->printResults($arrayResults); +} catch (\Throwable $e) { + echo "Error running array benchmarks: " . $e->getMessage() . "\n"; + exit(1); +}src/Attributes/Regex.php (1)
15-17
: Validate regex pattern in constructor.The constructor accepts any string as a pattern without validation. Invalid regex patterns will cause runtime errors when the attribute is used for validation, making debugging harder. Validating the pattern at construction time follows the fail-fast principle and provides better error messages.
Apply this diff:
public function __construct( public readonly string $pattern, - ) {} + ) { + // Validate regex pattern + if (@preg_match($pattern, '') === false) { + throw new \InvalidArgumentException( + sprintf('Invalid regex pattern: %s', $pattern) + ); + } + }src/Composite/Arrays/IntArray.php (1)
13-16
: Consider edge cases for json_encode in error messages.The error message uses
json_encode($invalidItem)
which may fail or produce unexpected output for resource types, circular references, or objects without proper serialization. Consider using a safer alternative likevar_export($invalidItem, true)
or adding error handling for json_encode.Apply this diff if you prefer var_export:
- throw new InvalidArgumentException("All elements must be integers. Invalid value: " . json_encode($invalidItem)); + throw new InvalidArgumentException("All elements must be integers. Invalid value: " . var_export($invalidItem, true));src/Attributes/Range.php (1)
15-18
: Consider validating that min ≤ max in the constructor.The Range attribute accepts min and max values but doesn't validate their relationship. If a developer accidentally passes
new Range(max: 10, min: 1)
or constructs an invalid range where max < min, it will create a logically invalid constraint that could cause confusing validation failures later.Apply this diff to add validation:
public function __construct( public readonly int|float $min, public readonly int|float $max, -) {} +) { + if ($this->max < $this->min) { + throw new \InvalidArgumentException("Range max ($max) must be greater than or equal to min ($min)"); + } +}src/Composite/Struct/Rules/PatternRule.php (2)
14-17
: Consider validating the regex pattern in the constructor.The pattern is stored without validation. If an invalid regex is provided, it will only fail during validation, making debugging harder.
Apply this diff to add pattern validation:
public function __construct(string $pattern) { + if (@preg_match($pattern, '') === false) { + throw new \InvalidArgumentException("Invalid regex pattern: $pattern"); + } $this->pattern = $pattern; }
27-31
: Handle preg_match errors explicitly.
preg_match()
returnsfalse
on error (invalid regex), which is currently treated the same as "no match". While the pattern should be validated in the constructor (see previous comment), defensive error handling here improves robustness.- if (!preg_match($this->pattern, $value)) { + $result = @preg_match($this->pattern, $value); + if ($result === false) { + throw new ValidationException( + "Field '$fieldName' validation failed due to invalid pattern" + ); + } + if ($result === 0) { throw new ValidationException( "Field '$fieldName' does not match the required pattern" ); }src/Composite/String/HexString.php (1)
12-49
: LGTM! Valid hex string validation.The regex
/^[0-9A-Fa-f]+$/
correctly validates hexadecimal strings of any length. The implementation is clean and consistent with other string value objects.Optional observation: There's minor duplication between
HexString
(usespreg_match
) andStr16
/Str32
(usectype_xdigit
) for hex validation. Both approaches are valid, but you might consider extracting shared hex validation logic into a helper trait or method if more Str* classes are added in the future.src/Composite/String/PathString.php (1)
22-29
: Consider more robust path validation.The current regex
/^[a-zA-Z0-9_\-\.\/\\:]+$/
permits many invalid path patterns (e.g.,:::
,...
, or sequences of special characters). While this flexibility may be intentional, consider:
- Using platform-specific validation or a well-tested path library
- Adding validation for path traversal sequences if security is a concern
- Rejecting obviously invalid patterns like consecutive special characters
Alternatively, if paths are always used in controlled contexts and flexibility is needed, document the intentionally permissive validation in the class docblock.
src/Composite/Struct/Rules/CustomRule.php (1)
28-39
: Strengthen type safety of the validator closure result.The validation logic assumes the closure returns a boolean but doesn't enforce this. If the closure returns a non-boolean value (e.g., string, array), the
!$isValid
check may produce unexpected results due to PHP's type juggling.Apply this diff to add explicit type checking:
public function validate(mixed $value, string $fieldName): bool { $isValid = ($this->validator)($value); + if (!is_bool($isValid)) { + throw new \LogicException( + "Custom validator must return a boolean value, got " . gettype($isValid) + ); + } + if (!$isValid) { throw new ValidationException( "Field '$fieldName': {$this->errorMessage}" ); } return true; }src/Attributes/Length.php (1)
15-18
: Consider validating min/max relationship.For defensive coding, consider adding validation to ensure
$min <= $max
when both are provided. This would catch configuration errors at attribute construction time rather than during validation.Apply this diff:
public function __construct( public readonly ?int $min = null, public readonly ?int $max = null, -) {} +) { + if ($this->min !== null && $this->max !== null && $this->min > $this->max) { + throw new \InvalidArgumentException("Minimum length cannot be greater than maximum length"); + } +}src/Composite/Struct/Rules/MinLengthRule.php (1)
14-17
: Consider validating minLength is positive.For defensive coding, validate that
$minLength
is greater than zero to prevent nonsensical configurations.Apply this diff:
public function __construct(int $minLength) { + if ($minLength <= 0) { + throw new \InvalidArgumentException("Minimum length must be greater than zero"); + } $this->minLength = $minLength; }src/Composite/String/SlugString.php (1)
22-28
: Slug validation is fine; consider stricter semantics or normalize.Optional hardening:
- Disallow leading/trailing hyphens and double “--”.
- Optionally lowercase/trim before validate to be user‑friendly.
Example (pattern only):
- if (!preg_match('/^[a-z0-9-]+$/', $value)) { + if (!preg_match('/^(?!-)(?!.*--)[a-z0-9-]+(?<!-)$/', $value)) {Or normalize then validate:
- public function __construct(string $value) + public function __construct(string $value) { - if (!preg_match('/^[a-z0-9-]+$/', $value)) { + $value = strtolower(trim($value)); + if (!preg_match('/^(?!-)(?!.*--)[a-z0-9-]+(?<!-)$/', $value)) {src/Composite/String/IpString.php (1)
22-28
: Clarify IP version intent; add tests accordingly.Current validation accepts IPv4 and IPv6. If that’s intended, add IPv6 test cases (e.g., “2001:db8::1”). If you want IPv4‑only (or IPv6‑only), use flags:
- if (!filter_var($value, FILTER_VALIDATE_IP)) { + if (!filter_var($value, FILTER_VALIDATE_IP /* | FILTER_FLAG_IPV4 */)) {Please confirm whether both versions are intended so tests can reflect it.
Tests/Composite/Arrays/TypeSafeArrayTest.php (1)
65-65
: Optional: silence PHPMD UnusedFormalParameter in map test.If PHPMD runs on tests and warns about unused params, either rename unused variables to “$_” (as above) or suppress for this method:
+ /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ public function testMapOperation(): voidsrc/Composite/String/TrimmedString.php (1)
22-29
: Unicode‑aware trim (optional).PHP’s trim() may not remove NBSP/Unicode spaces. If you want stricter trimming, consider:
- $trimmed = trim($value); + $trimmed = preg_replace('/^\s+|\s+$/u', '', $value); + $trimmed = $trimmed === null ? '' : $trimmed;Keeps current behavior while handling Unicode whitespace.
Tests/Composite/String/Str8Test.php (2)
15-17
: Use strict assertion for strings.Prefer assertSame for exact string match.
- $this->assertEquals('deadbeef', $str->getValue()); + $this->assertSame('deadbeef', $str->getValue());
13-17
: Optional: also assert __toString() and uppercase acceptance.Covers string casting and hex case-insensitivity.
public function testValidStr8(): void { $str = new Str8('deadbeef'); - $this->assertEquals('deadbeef', $str->getValue()); + $this->assertSame('deadbeef', $str->getValue()); + $this->assertSame('deadbeef', (string) $str); + + $upper = new Str8('DEADBEEF'); + $this->assertSame('DEADBEEF', $upper->getValue()); }src/Composite/String/MacString.php (2)
28-29
: Normalize representation (lowercase, ':') for stable equality.Ensures equivalent addresses with different cases/separators have a single canonical form.
- $this->value = $value; + $normalized = strtolower(str_replace('-', ':', $value)); + $this->value = $normalized;
25-27
: Optional: consider FILTER_VALIDATE_MAC.If PHP runtime allows, built-in validation is simpler.
if (filter_var($value, FILTER_VALIDATE_MAC) === false) { throw new InvalidArgumentException('Invalid MAC address format'); }Tests/Composite/Arrays/DynamicArrayTest.php (2)
18-18
: Prefer assertCount over count() in assertions.Improves readability and PHPUnit failure messages.
- $this->assertEquals(0, count($array)); + $this->assertCount(0, $array)- $this->assertEquals(2, count($array)); + $this->assertCount(2, $array)- $this->assertEquals(3, count($array)); + $this->assertCount(3, $array)- $this->assertEquals(3, count($array)); + $this->assertCount(3, $array)- $this->assertEquals(3, count($array)); + $this->assertCount(3, $array)Also applies to: 32-32, 42-42, 79-79, 105-105
14-15
: Add return type hints (: void) to test methods for consistency.Aligns with strict types and modern PHPUnit style.
- public function testCreateDynamicArray() + public function testCreateDynamicArray(): void @@ - public function testCreateWithInvalidCapacity() + public function testCreateWithInvalidCapacity(): void @@ - public function testCreateWithInitialData() + public function testCreateWithInitialData(): void @@ - public function testCreateWithExcessiveInitialData() + public function testCreateWithExcessiveInitialData(): void @@ - public function testReserveCapacity() + public function testReserveCapacity(): void @@ - public function testShrinkToFit() + public function testShrinkToFit(): void @@ - public function testDynamicResizingOnAppend() + public function testDynamicResizingOnAppend(): void @@ - public function testDynamicResizingOnOffsetSet() + public function testDynamicResizingOnOffsetSet(): void @@ - public function testSetInvalidType() + public function testSetInvalidType(): void @@ - public function testSetValueAdjustsCapacity() + public function testSetValueAdjustsCapacity(): void @@ - public function testIteration() + public function testIteration(): voidAlso applies to: 21-25, 27-34, 36-44, 46-53, 55-65, 67-79, 81-88, 90-95, 97-106, 108-122
Tests/Composite/Vector/Vec4Test.php (1)
37-37
: Use deltas for float assertions.Avoid flakiness on FP ops; use assertEqualsWithDelta.
Run once locally to confirm all pass on your PHP versions.
- $this->assertEquals(sqrt(13), $vec->magnitude()); + $this->assertEqualsWithDelta(sqrt(13), $vec->magnitude(), 1e-12); @@ - $this->assertEquals(1.0, $normalized->magnitude()); - $this->assertEquals(1 / sqrt(13), $normalized->getX()); - $this->assertEquals(2 / sqrt(13), $normalized->getY()); - $this->assertEquals(2 / sqrt(13), $normalized->getZ()); - $this->assertEquals(2 / sqrt(13), $normalized->getW()); + $this->assertEqualsWithDelta(1.0, $normalized->magnitude(), 1e-12); + $this->assertEqualsWithDelta(1 / sqrt(13), $normalized->getX(), 1e-12); + $this->assertEqualsWithDelta(2 / sqrt(13), $normalized->getY(), 1e-12); + $this->assertEqualsWithDelta(2 / sqrt(13), $normalized->getZ(), 1e-12); + $this->assertEqualsWithDelta(2 / sqrt(13), $normalized->getW(), 1e-12); @@ - $this->assertEquals(sqrt(13), $vec1->distance($vec2)); + $this->assertEqualsWithDelta(sqrt(13), $vec1->distance($vec2), 1e-12);Based on learnings.
Also applies to: 44-48, 162-162
src/Laravel/Casts/Int8Cast.php (3)
49-54
: Validate range on set; reject out-of-range inputs early.Construct Int8 for non-Int8 values to enforce domain invariants.
- if ($value instanceof Int8) { - return $value->getValue(); - } - - return (int) $value; + if ($value instanceof Int8) { + return $value->getValue(); + } + // Validate and normalize by constructing Int8 + return (new Int8((int) $value))->getValue();
25-26
: Consider matching the interface’smixed
return type to reduce analysis friction.Laravel’s CastsAttributes::get/set return mixed; narrowing is usually OK, but some tools prefer exact matches.
- public function get($model, $key, $value, $attributes): ?Int8 + public function get($model, $key, $value, $attributes): mixed @@ - public function set($model, $key, $value, $attributes): ?int + public function set($model, $key, $value, $attributes): mixedAlso applies to: 43-44
16-24
: Suppress PHPMD unused parameter warnings (interface signature).These params are required by the contract.
/** * Eloquent cast for Int8 values */ final class Int8Cast implements CastsAttributes { /** * Cast the given value. * + * @SuppressWarnings("UnusedFormalParameter") * @param Model $model * @param string $key * @param mixed $value * @param array $attributes * @return Int8|null */ @@ /** * Prepare the given value for storage. * + * @SuppressWarnings("UnusedFormalParameter") * @param Model $model * @param string $key * @param Int8|mixed $value * @param array $attributes * @return int|null */Also applies to: 34-42
src/Attributes/Validator.php (1)
74-93
: Consider usingmb_strlen()
for multi-byte string support.The current implementation uses
strlen()
, which counts bytes rather than characters. For multi-byte UTF-8 strings, this could produce incorrect length validation. Consider usingmb_strlen()
to accurately count characters regardless of encoding.Apply this diff:
- $strLength = strlen($value); + $strLength = mb_strlen($value);Tests/Composite/Arrays/FixedSizeArrayTest.php (2)
16-22
: UseassertCount()
for clarity and better diffs.Switch count assertions to
assertCount()
for consistency.Apply this diff:
- $this->assertEquals(0, count($array)); + $this->assertCount(0, $array) ... - $this->assertEquals(2, count($array)); + $this->assertCount(2, $array) ... - $this->assertEquals(3, count($array)); + $this->assertCount(3, $array) ... - $this->assertEquals(2, count($array)); + $this->assertCount(2, $array) ... - $this->assertEquals(0, count($empty)); + $this->assertCount(0, $empty)Also applies to: 36-40, 57-60, 95-101, 120-123, 138-142, 160-164
72-78
: Add a test for negative index assignment (currently not guarded).
FixedSizeArray::offsetSet()
checks upper bound but not negative indices. Capture the expected behavior with a test.Proposed addition:
+ public function testSetElementNegativeIndex() + { + $array = new FixedSizeArray(\stdClass::class, 2); + $this->expectException(InvalidArgumentException::class); + $array[-1] = new \stdClass(); + }src/Composite/Vector/Vec3.php (1)
36-54
: Optional: add afromXYZ(float $x, float $y, float $z)
named constructor.Improves readability at call sites constructing vectors.
Proposed addition:
+ public static function fromXYZ(float $x, float $y, float $z): self + { + return new self([$x, $y, $z]); + }src/Laravel/Models/ExampleModel.php (2)
94-101
: Decimal precision risk insetBalanceAttribute
.Overriding the mutator bypasses
'decimal:2'
enforcement; floats may persist with binary rounding. Either keep the cast and drop the mutator or format explicitly.Example:
- } else { - $this->attributes['balance'] = $value; + } else { + $this->attributes['balance'] = number_format((float)$value, 2, '.', ''); }
126-130
:addToScore()
name suggests mutation but only returns a value.Either persist the result or rename to clarify intent.
Persisting variant:
public function addToScore(Int8 $points): Int8 { - $currentScore = $this->getScoreAttribute($this->attributes['score']); - return $currentScore->add($points); + $current = $this->getScoreAttribute($this->attributes['score']); + $next = $current->add($points); + $this->setScoreAttribute($next); + return $next; }examples/comprehensive_example.php (1)
154-155
: Ensure XML is a string before substrStruct::toXml may return a SimpleXMLElement; substr on that is unreliable. Convert to string first.
-$xml = $userStruct->toXml(); -echo "Struct XML: " . substr($xml, 0, 100) . "...\n\n"; +$xml = $userStruct->toXml(); +$xmlString = is_string($xml) ? $xml : $xml->asXML(); +echo "Struct XML: " . substr($xmlString, 0, 100) . "...\n\n";src/Composite/Struct/Rules/PasswordRule.php (2)
43-49
: Use mb_strlen when availableASCII strlen undercounts multibyte strings.
- $length = strlen($value); + $length = function_exists('mb_strlen') ? mb_strlen($value) : strlen($value);
56-78
: Consider Unicode-aware character classesASCII classes miss non-Latin scripts. Optionally switch to Unicode properties: \p{Lu}, \p{Ll}, \p{Nd}, and “special” via negated \p{L}\p{Nd}. Use /u.
src/Composite/Arrays/DynamicArray.php (2)
59-64
: Validate reserve() inputReject non-positive capacities to surface misuse early.
- public function reserve(int $capacity): void + public function reserve(int $capacity): void { - if ($capacity > $this->capacity) { + if ($capacity <= 0) { + throw new InvalidArgumentException('Capacity must be a positive integer'); + } + if ($capacity > $this->capacity) { $this->capacity = $capacity; } }
79-97
: Disallow negative explicit indexesNegative offsets create sparse keys; inconsistent with capacity semantics.
public function offsetSet($offset, $value): void { if (!$this->isValidType($value)) { throw new TypeMismatchException( "Value must be of type {$this->getElementType()}" ); } if (is_null($offset)) { // Appending if (count($this->getValue()) >= $this->capacity) { $this->capacity = max(1, $this->capacity * 2); } } else { + if ($offset < 0) { + throw new InvalidArgumentException('Index cannot be negative'); + } if ($offset >= $this->capacity) { $this->capacity = $offset + 1; } } parent::offsetSet($offset, $value); }src/Composite/Arrays/FixedSizeArray.php (1)
96-103
: Disallow negative explicit indexesAlign with bounds enforcement for >= size.
- if (!is_null($offset) && $offset >= $this->size) { + if (!is_null($offset) && ($offset < 0 || $offset >= $this->size)) { throw new InvalidArgumentException( "Index {$offset} is out of bounds (size: {$this->size})" ); }src/Laravel/Http/Controllers/PhpDatatypesController.php (1)
73-75
: Avoid potential TypeError in Option mapRequest input may not be string; guard before strtoupper.
- $result = $optionalValue - ->map(fn($value) => strtoupper($value)) - ->unwrapOr('DEFAULT_VALUE'); + $result = $optionalValue + ->map(fn($value) => is_string($value) ? strtoupper($value) : $value) + ->unwrapOr('DEFAULT_VALUE');src/Composite/Struct/AdvancedStruct.php (2)
54-64
: Tighten type hints on isValidTypeMinor consistency with other structs.
- protected function isValidType($value, $type): bool + protected function isValidType(mixed $value, string $type): bool
66-69
: Consider throwing on unknown field accessOptional: align with Struct::get() by throwing when field not in schema.
- public function get(string $field) - { - return $this->data[$field] ?? null; - } + public function get(string $field) + { + if (!array_key_exists($field, $this->schema)) { + throw new InvalidArgumentException("Field '$field' does not exist in the struct."); + } + return $this->data[$field] ?? null; + }benchmarks/IntegerBenchmark.php (3)
7-12
: Remove unused imports.Int32, UInt8, and UInt32 are not used here; keep only Int8 and Int64 to reduce noise.
use Nejcc\PhpDatatypes\Scalar\Integers\Signed\Int8; -use Nejcc\PhpDatatypes\Scalar\Integers\Signed\Int32; use Nejcc\PhpDatatypes\Scalar\Integers\Signed\Int64; -use Nejcc\PhpDatatypes\Scalar\Integers\Unsigned\UInt8; -use Nejcc\PhpDatatypes\Scalar\Integers\Unsigned\UInt32;
25-27
: Consume results to avoid PHPMD “unused” and keep loops meaningful.Either drop the temp assignments or route them through a blackhole helper used consistently across native and typed benches.
+ private static int $sink = 0; + private static function blackhole(mixed $v): void + { + self::$sink ^= is_object($v) ? (spl_object_id($v) & 1) : (strlen((string)$v) & 1); + } @@ - for ($i = 0; $i < self::ITERATIONS; $i++) { - $int = new Int8(42); - } + for ($i = 0; $i < self::ITERATIONS; $i++) { + self::blackhole(new Int8(42)); + } @@ - for ($i = 0; $i < self::ITERATIONS; $i++) { - $int = 42; - } + for ($i = 0; $i < self::ITERATIONS; $i++) { + self::blackhole(42); + } @@ - $result = $int1->add($int2); - $result = $int1->subtract($int2); - $result = $int1->multiply($int2); + self::blackhole($int1->add($int2)); + self::blackhole($int1->subtract($int2)); + self::blackhole($int1->multiply($int2)); @@ - $result = $int1 + $int2; - $result = $int1 - $int2; - $result = $int1 * $int2; + self::blackhole($int1 + $int2); + self::blackhole($int1 - $int2); + self::blackhole($int1 * $int2); @@ - $result = $int1->add($int2); - $result = $int1->subtract($int2); + self::blackhole($int1->add($int2)); + self::blackhole($int1->subtract($int2));Also applies to: 46-47, 69-72, 94-97, 119-121
145-179
: Guard comparisons and prefer hrtime/peak memory (optional).
- Avoid divide-by-zero if a timer underflows.
- hrtime(true) has better resolution; memory_get_peak_usage(true) is more stable for “memory” figures.
- echo sprintf( - "%s:\n Time: %.6f seconds\n Memory: %d bytes\n Iterations: %d\n Time per iteration: %.9f seconds\n\n", + echo sprintf( + "%s:\n Time: %.6f seconds\n Memory(peak): %d bytes\n Iterations: %d\n Time/iter: %.9f seconds\n\n", $result['type'], $result['time'], $result['memory'], $result['iterations'], - $result['time'] / $result['iterations'] + $result['iterations'] > 0 ? $result['time'] / $result['iterations'] : 0.0 ); @@ - echo sprintf( - "Int8 Creation vs Native: %.2fx slower\n", - $int8Creation['time'] / $nativeCreation['time'] - ); + $den = max($nativeCreation['time'], 1e-12); + echo sprintf("Int8 Creation vs Native: %.2fx slower\n", $int8Creation['time'] / $den); @@ - echo sprintf( - "Int8 Arithmetic vs Native: %.2fx slower\n", - $int8Arithmetic['time'] / $nativeArithmetic['time'] - ); + $den2 = max($nativeArithmetic['time'], 1e-12); + echo sprintf("Int8 Arithmetic vs Native: %.2fx slower\n", $int8Arithmetic['time'] / $den2);If you want, I can factor timing/memory into a private measure(string $type, int $iterations, callable $fn) helper for all methods.
benchmarks/ArrayBenchmark.php (3)
7-10
: Drop unused import.StringArray is not referenced here.
use Nejcc\PhpDatatypes\Composite\Arrays\IntArray; -use Nejcc\PhpDatatypes\Composite\Arrays\StringArray; use Nejcc\PhpDatatypes\Composite\Dictionary;
26-28
: Consume temporaries to avoid “unused variable” and keep benches fair.Use a shared blackhole like in IntegerBenchmark or avoid assigning when not needed.
+ private static int $sink = 0; + private static function blackhole(mixed $v): void + { + self::$sink ^= is_object($v) ? (spl_object_id($v) & 1) : (strlen((string)$v) & 1); + } @@ - $array = new IntArray($data); + self::blackhole(new IntArray($data)); @@ - $array = $data; + self::blackhole($data); @@ - $array->toArray(); - $array->getValue(); + self::blackhole($array->toArray()); + self::blackhole($array->getValue()); @@ - $copy = $data; - $count = count($data); + self::blackhole($data); + self::blackhole(count($data)); @@ - $dict->toArray(); - $dict->size(); - $dict->getKeys(); + self::blackhole($dict->toArray()); + self::blackhole($dict->size()); + self::blackhole($dict->getKeys()); @@ - $copy = $data; - $count = count($data); - $keys = array_keys($data); + self::blackhole($data); + self::blackhole(count($data)); + self::blackhole(array_keys($data));Also applies to: 49-50, 71-74, 95-97, 121-125, 149-152
177-211
: Add zero-division guards and consider hrtime/peak memory like integer benchmarks.Prevents rare division by zero; improves timing/memory accuracy.
- "IntArray Creation vs Native: %.2fx slower\n", - $intArrayCreation['time'] / $nativeArrayCreation['time'] + "IntArray Creation vs Native: %.2fx slower\n", + $intArrayCreation['time'] / max($nativeArrayCreation['time'], 1e-12) ); @@ - "IntArray Operations vs Native: %.2fx slower\n", - $intArrayOps['time'] / $nativeArrayOps['time'] + "IntArray Operations vs Native: %.2fx slower\n", + $intArrayOps['time'] / max($nativeArrayOps['time'], 1e-12) ); @@ - "IntArray Memory overhead: %d bytes per operation\n", - ($intArrayCreation['memory'] - $nativeArrayCreation['memory']) / $intArrayCreation['iterations'] + "IntArray Memory overhead: %d bytes per operation\n", + (int) (max(0, $intArrayCreation['memory'] - $nativeArrayCreation['memory']) / max(1, $intArrayCreation['iterations'])) );If desired, I can extract a shared BenchmarkTrait with measure() used by both benchmark classes.
src/Composite/Arrays/TypeSafeArray.php (1)
61-64
: Harden __toString against JSON failures.json_encode can return false; __toString must return string.
- public function __toString(): string - { - return json_encode($this->data); - } + public function __toString(): string + { + try { + return json_encode($this->data, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + return '[]'; + } + }src/Composite/Struct/Struct.php (1)
98-101
: Use JSON_THROW_ON_ERROR for toJson (optional).Avoid silent failures and ensure deterministic output.
- public function toJson(bool $useAliases = false): string - { - return json_encode($this->toArray($useAliases)); - } + public function toJson(bool $useAliases = false): string + { + return json_encode($this->toArray($useAliases), JSON_THROW_ON_ERROR); + }src/Abstract/AbstractVector.php (2)
46-56
: Use component-count equality instead of class equality.Class checks block valid ops between vectors of equal dimension but different concrete types; compare counts instead and improve messages.
- if (get_class($this) !== get_class($other)) { - throw new InvalidArgumentException("Cannot calculate dot product of vectors with different dimensions"); - } + if (count($this->components) !== count($other->components)) { + throw new InvalidArgumentException("Cannot calculate dot product of vectors with different dimensions"); + } @@ - if (get_class($this) !== get_class($other)) { - throw new InvalidArgumentException("Cannot add vectors with different dimensions"); - } + if (count($this->components) !== count($other->components)) { + throw new InvalidArgumentException("Cannot add vectors with different dimensions"); + } @@ - if (get_class($this) !== get_class($other)) { - throw new InvalidArgumentException("Cannot subtract vectors with different dimensions"); - } + if (count($this->components) !== count($other->components)) { + throw new InvalidArgumentException("Cannot subtract vectors with different dimensions"); + } @@ - if (get_class($this) !== get_class($other)) { - throw new InvalidArgumentException("Cannot calculate distance between vectors with different dimensions"); - } + if (count($this->components) !== count($other->components)) { + throw new InvalidArgumentException("Cannot calculate distance between vectors with different dimensions"); + }Also applies to: 59-72, 74-87, 116-129
107-114
: Float equality is brittle. Consider epsilon-based comparison.For vectors with floats, strict array equality often fails post-ops.
- public function equals(DataTypeInterface $other): bool + public function equals(DataTypeInterface $other): bool { if (!$other instanceof self) { return false; } - - return $this->components === $other->components; + if (count($this->components) !== count($other->components)) { + return false; + } + $eps = 1e-9; + foreach ($this->components as $i => $v) { + if (abs($v - $other->components[$i]) > $eps) { + return false; + } + } + return true; }Tests/Composite/String/CompositeStringTypesTest.php (1)
32-227
: Reduce duplication with data providers; add a few edge cases.
- Consider data providers to DRY repetitive “valid/invalid” patterns and improve readability (PHPUnit 11 supports attributes).
- Add small edge cases: Base64String invalid padding/length (e.g., "SGVsbG8" without "=" using strict decode), IpString with IPv6 (e.g., "::1"), HtmlString negative case if you decide to treat libxml warnings as errors explicitly.
src/Composite/Struct/ImmutableStruct.php (1)
186-195
: Unify immutability error messages.
setField()
previously said “immutable struct” whileset()
says “frozen struct”. Delegating toset()
(see diff above) keeps messaging consistent with tests.Also applies to: 459-470
for ($i = 0; $i < self::ITERATIONS / 100; $i++) { // Fewer iterations for big ints | ||
$result = $int1->add($int2); | ||
$result = $int1->subtract($int2); | ||
} | ||
|
||
$end = microtime(true); | ||
$memoryEnd = memory_get_usage(); | ||
|
||
return [ | ||
'time' => $end - $start, | ||
'memory' => $memoryEnd - $memoryStart, | ||
'iterations' => self::ITERATIONS / 100, | ||
'type' => 'Int64 Arithmetic' | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure iterations is an integer.
Division yields float; report an int to keep result schema consistent.
- for ($i = 0; $i < self::ITERATIONS / 100; $i++) { // Fewer iterations for big ints
+ $iters = intdiv(self::ITERATIONS, 100); // Fewer iterations for big ints
+ for ($i = 0; $i < $iters; $i++) {
@@
- 'iterations' => self::ITERATIONS / 100,
+ 'iterations' => $iters,
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
for ($i = 0; $i < self::ITERATIONS / 100; $i++) { // Fewer iterations for big ints | |
$result = $int1->add($int2); | |
$result = $int1->subtract($int2); | |
} | |
$end = microtime(true); | |
$memoryEnd = memory_get_usage(); | |
return [ | |
'time' => $end - $start, | |
'memory' => $memoryEnd - $memoryStart, | |
'iterations' => self::ITERATIONS / 100, | |
'type' => 'Int64 Arithmetic' | |
]; | |
$iters = intdiv(self::ITERATIONS, 100); // Fewer iterations for big ints | |
for ($i = 0; $i < $iters; $i++) { | |
$result = $int1->add($int2); | |
$result = $int1->subtract($int2); | |
} | |
$end = microtime(true); | |
$memoryEnd = memory_get_usage(); | |
return [ | |
'time' => $end - $start, | |
'memory' => $memoryEnd - $memoryStart, | |
'iterations' => $iters, | |
'type' => 'Int64 Arithmetic' | |
]; |
🧰 Tools
🪛 PHPMD (2.15.0)
119-119: Avoid unused local variables such as '$result'. (undefined)
(UnusedLocalVariable)
🤖 Prompt for AI Agents
In benchmarks/IntegerBenchmark.php around lines 118 to 131, the code uses
self::ITERATIONS / 100 which produces a float; change it to an integer count
(e.g., $iterations = intdiv(self::ITERATIONS, 100) or (int)(self::ITERATIONS /
100)), use $iterations in the for loop and return 'iterations' => $iterations so
the reported iterations value is an integer and the loop boundary is consistent.
], | ||
"require": { | ||
"php": "^8.2", | ||
"php": "^8.4", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Critical: PHP 8.4 requirement breaks CI and may be premature.
The PHP requirement was bumped from ^8.2 to ^8.4, but:
- CI pipeline fails because it still runs PHP 8.2.29
- PHP 8.4 is very recent (released November 2024) and may have limited adoption
- This is a major breaking change for all users
Verify whether PHP 8.4-specific features are actually required:
🏁 Script executed:
#!/bin/bash
# Search for PHP 8.4-specific features in the codebase
echo "=== Searching for PHP 8.4 features ==="
# PHP 8.4 introduced array_find, array_find_key, array_any, array_all
echo -e "\n--- New array functions (PHP 8.4) ---"
rg -nC3 '\b(array_find|array_find_key|array_any|array_all)\s*\(' --type=php
# Property hooks (PHP 8.4)
echo -e "\n--- Property hooks ---"
rg -nC3 '(get|set)\s*\{' --type=php -g '!vendor/**'
# Asymmetric visibility (PHP 8.4)
echo -e "\n--- Asymmetric visibility ---"
rg -nC3 'public\s+private\(set\)' --type=php
# New HTML5 support
echo -e "\n--- HTML5 parsing ---"
rg -nC3 'Dom\\HTMLDocument' --type=php
echo -e "\n=== Check composer.lock for actual PHP version used ==="
cat composer.lock | jq '.platform.php // "not set"' 2>/dev/null || echo "composer.lock not found"
Length of output: 5808
🏁 Script executed:
#!/bin/bash
# Verify the original PHP version and check CI configuration
echo "=== Check git diff for composer.json changes ==="
git diff HEAD~1 composer.json | head -40
echo -e "\n=== Look for CI configuration files ==="
find . -maxdepth 3 -type f \( -name "*.yml" -o -name "*.yaml" \) -path "*/.github/workflows/*" -o -path "*/gitlab-ci*" -o -path "*/.circleci/*" 2>/dev/null | head -20
echo -e "\n=== Check GitHub Actions workflow for PHP version ==="
find .github/workflows -name "*.yml" -o -name "*.yaml" 2>/dev/null | xargs grep -l "php-version\|PHP" 2>/dev/null | head -5
echo -e "\n=== Display relevant workflow content ==="
find .github/workflows -name "*.yml" 2>/dev/null | head -1 | xargs cat 2>/dev/null | grep -A5 -B5 "php-version\|8\.2\|8\.4" || echo "No PHP version config found"
Length of output: 1023
🏁 Script executed:
#!/bin/bash
# Verify the original PHP version and check CI configuration
echo "=== Check git diff for composer.json changes ==="
git diff HEAD~1 composer.json | head -40
echo -e "\n=== Look for CI configuration files ==="
find .github/workflows -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null
echo -e "\n=== Check GitHub Actions workflow for PHP version ==="
find .github/workflows -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null -exec cat {} \; | grep -A2 -B2 "php-version\|8\.2\|8\.4" || echo "No workflow files or PHP version config found"
Length of output: 641
🏁 Script executed:
#!/bin/bash
# Verify actual composer.json requirement and check if CI needs update
echo "=== Current composer.json PHP requirement ==="
cat composer.json | jq '.require.php'
echo -e "\n=== Lines 19 of composer.json ==="
sed -n '19p' composer.json
echo -e "\n=== Full require section ==="
cat composer.json | jq '.require'
echo -e "\n=== Check if main.yml was modified in this PR ==="
git log --oneline -1 .github/workflows/main.yml 2>/dev/null || echo "Cannot determine git history"
echo -e "\n=== Show last few lines of main.yml matrix config ==="
grep -A10 "matrix:" .github/workflows/main.yml | head -15
Length of output: 714
Critical: PHP 8.4 requirement incompatible with CI matrix—will cause test failures.
The PHP requirement was changed to ^8.4
, and the code actively uses PHP 8.4-only functions (array_all
, array_find
, array_find_key
). However, the CI workflow matrix still tests PHP 8.2 and 8.3:
- On PHP 8.2/8.3, Composer will reject the dependency installation due to the
^8.4
requirement - If installation bypasses this, the code will fatal error on undefined functions
- CI jobs for PHP 8.2 and 8.3 will fail
Required action: Either update the CI matrix to test only PHP 8.3/8.4 (removing 8.2), or revert the PHP requirement to ^8.2
and replace the 8.4-only functions with compatible alternatives. The current configuration creates an incompatible state.
🤖 Prompt for AI Agents
In composer.json around line 19 the PHP requirement was changed to "^8.4" which
conflicts with the CI matrix that still runs 8.2 and 8.3 and with code that uses
PHP 8.4-only functions; choose one remedial path: either (A) update the CI
workflow matrix (e.g., .github/workflows/ci.yml) to remove PHP 8.2 and include
only supported versions (8.3 and 8.4), update any references to install PHP 8.4
in runners, and re-run tests; or (B) revert composer.json to "php": "^8.2",
replace all usages of PHP 8.4-only functions (array_all, array_find,
array_find_key) with compatible alternatives or small local helper/polyfill
functions, run composer update and ensure tests pass; also regenerate
composer.lock if you change composer.json.
"phpunit/phpunit": "^11.4.2", | ||
"phpstan/phpstan": "^1.10", | ||
"infection/infection": "^0.27" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify dependency version compatibility with PHP 8.4.
The added dev dependencies (PHPUnit ^11.4.2, PHPStan ^1.10, Infection ^0.27) need verification for PHP 8.4 compatibility, especially since PHP 8.4 is very recent.
🌐 Web query:
PHPUnit 11.4 PHP 8.4 compatibility
💡 Result:
Short answer: Yes — PHPUnit 11.4 is compatible with PHP 8.4.
Details:
- PHPUnit 11 requires PHP >= 8.2 (so it runs on 8.3 and 8.4). [1][2]
- PHPUnit 11.4 was released Oct 5, 2024; projects (e.g., Drupal) state PHPUnit 11 is the minimum for testing on PHP 8.4. [2][3]
Sources:
[1] PHPUnit supported versions.
[2] PHPUnit 11.4 release announcement (Oct 5, 2024).
[3] Drupal note: “PHP 8.4 requires PHPUnit 11 as a minimum.”
Continuing verification with the remaining dependencies:
Need to verify compatibility more specifically for the exact versions mentioned:
PHPStan ^1.10 should be upgraded to ^2.0 for full PHP 8.4 syntax support.
Full support for PHP 8.4 syntax (particularly property hooks) required PHPStan 2.0, which was released on November 11th. PHPStan 1.10 was released in February 2023, predating PHP 8.4's formal release and lacks native support for PHP 8.4's latest syntax features.
While PHPStan ^1.10 can technically run on PHP 8.0+, it will not fully understand or validate PHP 8.4-specific syntax. PHPStan 2.0+ is required to properly handle PHP 8.4's property hooks and syntax.
- PHPUnit ^11.4.2: Compatible — PHPUnit 11 requires PHP >= 8.2 ✓
- PHPStan ^1.10: Upgrade to ^2.0 for complete PHP 8.4 support
- Infection ^0.27: Compatible (recent versions actively support PHP 8.4)
🤖 Prompt for AI Agents
composer.json lines 26-28: PHPStan is pinned to ^1.10 which lacks full PHP 8.4
syntax support; update the phpstan requirement to ^2.0 in composer.json, run
composer update to install the new version, and then run your static analysis
and test suite to verify there are no new issues from the upgrade (adjust
phpstan config if any rule changes are required).
ignoreErrors: | ||
# Ignore errors in test files for now | ||
- '#Call to an undefined method.*#' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overly broad error suppression undermines type safety.
The pattern '#Call to an undefined method.*#'
suppresses all undefined method errors across the entire codebase. At analysis level 9, this defeats the purpose of strict static analysis and can hide legitimate bugs. Consider narrowing the scope to specific test classes or methods, or address the underlying issues.
Apply a more targeted approach:
ignoreErrors:
- # Ignore errors in test files for now
- - '#Call to an undefined method.*#'
+ -
+ message: '#Call to an undefined method.*#'
+ path: Tests/*
+ count: 10 # Track and reduce over time
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
ignoreErrors: | |
# Ignore errors in test files for now | |
- '#Call to an undefined method.*#' | |
ignoreErrors: | |
- | |
message: '#Call to an undefined method.*#' | |
path: Tests/* | |
count: 10 # Track and reduce over time |
🤖 Prompt for AI Agents
In phpstan.neon around lines 8-10, the ignoreErrors entry using the broad regex
'#Call to an undefined method.*#' suppresses all undefined method errors;
replace this global suppression with a targeted ignoreErrors entry that matches
the specific message and restricts it to test files only (e.g., add an entry
with "message" => 'Call to an undefined method.*' and "paths" =>
['%rootDir%/../../tests', 'tests/'] or an equivalent path pattern), or remove
the suppression entirely and fix the underlying undefined method issues in
source code; ensure any remaining ignores are narrowly scoped to exact
classes/files or specific test helpers to avoid hiding real errors.
## Security | ||
|
||
If you discover any security related issues, please email [email protected] instead of using the issue tracker. | ||
If you discover any security-related issues, please email [email protected] instead of using the issue tracker. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix markdownlint MD034 (bare email).
Use a mailto link to satisfy tooling.
-If you discover any security-related issues, please email [email protected] instead of using the issue tracker.
+If you discover any security-related issues, please email <mailto:[email protected]> instead of using the issue tracker.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
If you discover any security-related issues, please email [email protected] instead of using the issue tracker. | |
If you discover any security-related issues, please email <mailto:[email protected]> instead of using the issue tracker. |
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
432-432: Bare URL used
(MD034, no-bare-urls)
🤖 Prompt for AI Agents
In README.md around line 432, the line contains a bare email address which
triggers markdownlint MD034; replace the plain email with a mailto link (e.g.,
use <mailto:[email protected]> syntax) so the address is wrapped as a
clickable mailto hyperlink, ensuring the tooling no longer flags a bare email.
public function getUserIdAttribute($value): UInt8 | ||
{ | ||
return new UInt8($value); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Null-safety for accessors.
Current signatures are non-nullable but DB values can be null. Guard and return nullable wrappers or enforce non-null via DB constraints.
Example for user_id
:
- public function getUserIdAttribute($value): UInt8
+ public function getUserIdAttribute($value): ?UInt8
{
- return new UInt8($value);
+ return $value === null ? null : new UInt8($value);
}
Apply similarly to getBalanceAttribute
and getScoreAttribute
.
Also applies to: 86-89, 106-109
🤖 Prompt for AI Agents
In src/Laravel/Models/ExampleModel.php around lines 66-69 (and similarly 86-89,
106-109), the accessor signatures and returns are non-nullable but database
columns can be null; change the return types to nullable (e.g., UInt8|null or
appropriate wrapper|null), guard against null input (if $value is null return
null), and only construct and return the wrapper when $value is not null; apply
the same null-check and nullable return type pattern to getBalanceAttribute and
getScoreAttribute.
private function addValidationMessages(): void | ||
{ | ||
$messages = [ | ||
'int8' => 'The :attribute must be a valid 8-bit signed integer (-128 to 127).', | ||
'int16' => 'The :attribute must be a valid 16-bit signed integer (-32,768 to 32,767).', | ||
'int32' => 'The :attribute must be a valid 32-bit signed integer.', | ||
'int64' => 'The :attribute must be a valid 64-bit signed integer.', | ||
'uint8' => 'The :attribute must be a valid 8-bit unsigned integer (0 to 255).', | ||
'uint16' => 'The :attribute must be a valid 16-bit unsigned integer (0 to 65,535).', | ||
'uint32' => 'The :attribute must be a valid 32-bit unsigned integer.', | ||
'uint64' => 'The :attribute must be a valid 64-bit unsigned integer.', | ||
'float32' => 'The :attribute must be a valid 32-bit floating point number.', | ||
'float64' => 'The :attribute must be a valid 64-bit floating point number.', | ||
]; | ||
|
||
foreach ($messages as $rule => $message) { | ||
Validator::replacer($rule, function ($message, $attribute, $rule, $parameters) { | ||
return str_replace(':attribute', $attribute, $message); | ||
}); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The message registration doesn't actually work as intended.
The addValidationMessages()
method and Validator::replacer()
calls don't achieve their apparent purpose:
- The messages array (lines 69-80) is never registered with Laravel's validator—
Validator::replacer()
only registers a replacer function, not messages. - Laravel automatically replaces
:attribute
placeholders in validation messages, making the replacer redundant. - Each Rule class already defines its own
message()
method, which Laravel uses automatically.
The entire addValidationMessages()
method can be removed without affecting functionality, as the error messages are already properly defined in each Rule class's message()
method.
Apply this diff to remove the ineffective code:
private function registerValidationRules(): void
{
// Integer validation rules
Validator::extend('int8', Int8Rule::class);
Validator::extend('int16', Int16Rule::class);
Validator::extend('int32', Int32Rule::class);
Validator::extend('int64', Int64Rule::class);
Validator::extend('uint8', UInt8Rule::class);
Validator::extend('uint16', UInt16Rule::class);
Validator::extend('uint32', UInt32Rule::class);
Validator::extend('uint64', UInt64Rule::class);
// Float validation rules
Validator::extend('float32', Float32Rule::class);
Validator::extend('float64', Float64Rule::class);
-
- // Add custom error messages
- $this->addValidationMessages();
}
-
- /**
- * Add custom validation error messages
- */
- private function addValidationMessages(): void
- {
- $messages = [
- 'int8' => 'The :attribute must be a valid 8-bit signed integer (-128 to 127).',
- 'int16' => 'The :attribute must be a valid 16-bit signed integer (-32,768 to 32,767).',
- 'int32' => 'The :attribute must be a valid 32-bit signed integer.',
- 'int64' => 'The :attribute must be a valid 64-bit signed integer.',
- 'uint8' => 'The :attribute must be a valid 8-bit unsigned integer (0 to 255).',
- 'uint16' => 'The :attribute must be a valid 16-bit unsigned integer (0 to 65,535).',
- 'uint32' => 'The :attribute must be a valid 32-bit unsigned integer.',
- 'uint64' => 'The :attribute must be a valid 64-bit unsigned integer.',
- 'float32' => 'The :attribute must be a valid 32-bit floating point number.',
- 'float64' => 'The :attribute must be a valid 64-bit floating point number.',
- ];
-
- foreach ($messages as $rule => $message) {
- Validator::replacer($rule, function ($message, $attribute, $rule, $parameters) {
- return str_replace(':attribute', $attribute, $message);
- });
- }
- }
Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 PHPMD (2.15.0)
83-83: Avoid unused parameters such as '$parameters'. (undefined)
(UnusedFormalParameter)
🤖 Prompt for AI Agents
In src/Laravel/PhpDatatypesServiceProvider.php around lines 67 to 87, the
addValidationMessages() method is ineffective because it only registers
replacers (not messages), duplicates Laravel's built-in :attribute replacement,
and duplicates Rule::message() definitions; remove the entire
addValidationMessages() method and any calls to it (if present) so validation
messages rely on the existing Rule classes' message() implementations and
Laravel's default behavior.
public function passes($attribute, $value): bool | ||
{ | ||
if (!is_numeric($value)) { | ||
return false; | ||
} | ||
|
||
try { | ||
new Float32((float) $value); | ||
return true; | ||
} catch (\OutOfRangeException) { | ||
return false; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silence PHPMD for unused $attribute
and keep the interface contract.
Laravel requires the parameter; PHPMD flags it. Add a no-op to appease the linter.
Apply this diff:
public function passes($attribute, $value): bool
{
+ // Intentionally unused (required by Illuminate\Contracts\Validation\Rule)
+ (void) $attribute;
if (!is_numeric($value)) {
return false;
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
public function passes($attribute, $value): bool | |
{ | |
if (!is_numeric($value)) { | |
return false; | |
} | |
try { | |
new Float32((float) $value); | |
return true; | |
} catch (\OutOfRangeException) { | |
return false; | |
} | |
} | |
public function passes($attribute, $value): bool | |
{ | |
// Intentionally unused (required by Illuminate\Contracts\Validation\Rule) | |
(void) $attribute; | |
if (!is_numeric($value)) { | |
return false; | |
} | |
try { | |
new Float32((float) $value); | |
return true; | |
} catch (\OutOfRangeException) { | |
return false; | |
} | |
} |
🧰 Tools
🪛 PHPMD (2.15.0)
22-22: Avoid unused parameters such as '$attribute'. (undefined)
(UnusedFormalParameter)
🤖 Prompt for AI Agents
In src/Laravel/Validation/Rules/Float32Rule.php around lines 22 to 34, the
$attribute parameter is required by the Laravel interface but PHPMD flags it as
unused; add a no-op reference to the parameter at the start of passes (for
example (void)$attribute; or unset($attribute);) to silence the linter while
keeping the method signature and existing validation logic unchanged.
$mapped = $array->map(function ($item) { | ||
$new = new \stdClass(); | ||
$new->mapped = true; | ||
return $new; | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix callback signature for PHP 8.2 (map passes 2 args).
TypeSafeArray::map invokes callback as ($value, $key). The closure here accepts only one param, which will throw ArgumentCountError on PHP 8.2. Accept two and ignore the key:
- $mapped = $array->map(function ($item) {
+ $mapped = $array->map(function ($item, $_) {
$new = new \stdClass();
$new->mapped = true;
return $new;
});
Based on learnings
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
$mapped = $array->map(function ($item) { | |
$new = new \stdClass(); | |
$new->mapped = true; | |
return $new; | |
}); | |
$mapped = $array->map(function ($item, $_) { | |
$new = new \stdClass(); | |
$new->mapped = true; | |
return $new; | |
}); |
🧰 Tools
🪛 PHPMD (2.15.0)
65-65: Avoid unused parameters such as '$item'. (undefined)
(UnusedFormalParameter)
🤖 Prompt for AI Agents
In Tests/Composite/Arrays/TypeSafeArrayTest.php around lines 65 to 70, the
closure passed to TypeSafeArray::map only accepts one parameter but map invokes
callbacks as ($value, $key) on PHP 8.2+, causing an ArgumentCountError; update
the closure signature to accept two parameters (e.g., function ($item, $key) {
... }) and simply ignore the $key inside the body, leaving the rest of the
mapping logic unchanged.
$filtered = $array->filter(function ($item) { | ||
return $item->value === 1; | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same issue in filter: accept 2 params to avoid ArgumentCountError.
- $filtered = $array->filter(function ($item) {
- return $item->value === 1;
- });
+ $filtered = $array->filter(function ($item, $_) {
+ return $item->value === 1;
+ });
Based on learnings
🤖 Prompt for AI Agents
In Tests/Composite/Arrays/TypeSafeArrayTest.php around lines 87-90, the filter
callback currently accepts only one parameter which can cause an
ArgumentCountError when the implementation passes two arguments; update the test
callback to accept two parameters (e.g., function($item, $key = null) { return
$item->value === 1; }) so it matches the implementation signature and remains
backward-compatible if a second arg is passed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review continued from previous batch...
/** | ||
* @var array<string, mixed> The struct data | ||
*/ | ||
private array $data; | ||
|
||
/** | ||
* @var array<string, ValidationRule[]> The validation rules for each field | ||
*/ | ||
private array $rules; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Consolidated cleanup: drop $data/$rules
and the unused validate().
These are vestigial and lead to dead/buggy paths. Removing them simplifies the class and resolves PHPMD’s warning.
Apply:
@@
- /**
- * @var array<string, mixed> The struct data
- */
- private array $data;
-
- /**
- * @var array<string, ValidationRule[]> The validation rules for each field
- */
- private array $rules;
+ // All state is carried in $fields; validation is performed per-set.
@@
- private function validate(): void
- {
- // Validate parent struct if it exists
- if ($this->parent !== null) {
- $this->parent->validate();
- }
-
- // Validate current struct
- foreach ($this->rules as $field => $fieldRules) {
- if (!isset($this->data[$field])) {
- throw new ValidationException("Field '{$field}' is required");
- }
- foreach ($fieldRules as $rule) {
- $rule->validate($this->data[$field]);
- }
- }
- }
+ // Removed unused private validate() method.
Also applies to: 87-108
🤖 Prompt for AI Agents
In src/Composite/Struct/ImmutableStruct.php around lines 34-43 (and also remove
the vestigial block at lines 87-108), drop the private properties $data and
$rules and remove the unused validate() method; update the constructor and any
methods in this class to stop referencing those properties, remove related
phpdoc comments, and delete any dead code paths that depended on them so the
class compiles with no unused-property/method warnings; ensure any external
callers are using the class's intended API (or adapt internal logic) and run
static analysis/tests to confirm no remaining references.
Uninitialized typed properties $data
and $rules
cause fatal accesses.
These are never assigned but are used by multiple methods. Remove them or wire everything to $fields
(the actual source of truth).
Apply this cleanup as part of the larger diff below.
🤖 Prompt for AI Agents
In src/Composite/Struct/ImmutableStruct.php around lines 34 to 43, the typed
properties $data and $rules are declared but never initialized causing fatal
accesses; remove these properties and update the class to use the existing
$fields property as the single source of truth (or, alternatively, initialize
$data and $rules from $fields in the constructor if you prefer keeping them).
Ensure all methods that currently reference $this->data or $this->rules are
changed to reference the equivalent data within $this->fields (or populated
$this->data/$this->rules) so there are no uninitialized typed property accesses.
private function validate(): void | ||
{ | ||
// Validate parent struct if it exists | ||
if ($this->parent !== null) { | ||
$this->parent->validate(); | ||
} | ||
|
||
// Validate current struct | ||
foreach ($this->rules as $field => $fieldRules) { | ||
if (!isset($this->data[$field])) { | ||
throw new ValidationException("Field '{$field}' is required"); | ||
} | ||
foreach ($fieldRules as $rule) { | ||
$rule->validate($this->data[$field]); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Broken and unused validate(): wrong data source and wrong rule call signature.
- Uses
$this->data
/$this->rules
(unassigned). - Calls
$rule->validate($this->data[$field])
with one argument; rules expect(value, fieldName)
.
Remove this method or re-implement correctly. Static analysis already flags it.
As per static analysis hints.
🧰 Tools
🪛 PHPMD (2.15.0)
92-108: Avoid unused private methods such as 'validate'. (undefined)
(UnusedPrivateMethod)
🤖 Prompt for AI Agents
In src/Composite/Struct/ImmutableStruct.php around lines 92-108, the validate()
method incorrectly references unassigned properties $this->data and $this->rules
and calls rule->validate with the wrong signature; update the method to obtain
the data and rules via the class' proper accessors (e.g. $this->getData() and
$this->getRules() or the actual getter names used in this class), iterate over
the resulting rules, check existence against the same data source, and call each
rule as $rule->validate($value, $field) (passing both value and field name);
alternatively remove the broken method if validation is handled elsewhere—ensure
parent validation call stays intact and static analysis errors are resolved.
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) { | ||
return; | ||
} | ||
$baseType = $this->stripNullable($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 '$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); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type validation can throw and misclassify types.
is_subclass_of($value, $baseType)
on non-objects can raise TypeError; primitives shouldn’t flow through subclass checks. Also general class/interface types aren’t robustly handled.
Harden validateValue()
:
@@
- $baseType = $this->stripNullable($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 '$actualType'"
- );
- }
- return;
- }
- // Handle primitive types
- if ($actualType !== $baseType && !is_subclass_of($value, $baseType)) {
- throw new InvalidArgumentException(
- "Field '$name' expects type '$type', but got '$actualType'"
- );
- }
+ $baseType = $this->stripNullable($type);
+ // Class/interface types (including StructInterface implementors)
+ if (class_exists($baseType) || interface_exists($baseType)) {
+ if (!$value instanceof $baseType) {
+ throw new InvalidArgumentException("Field '$name' expects type '$type', but got '$actualType'");
+ }
+ } else {
+ // Primitive/builtin checks
+ $ok = match ($baseType) {
+ '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),
+ 'mixed' => true,
+ default => true, // unknown type string: be permissive
+ };
+ if (!$ok) {
+ throw new InvalidArgumentException("Field '$name' expects type '$type', but got '$actualType'");
+ }
+ }
@@
- foreach ($this->fields[$name]['rules'] as $rule) {
- $rule->validate($value, $name);
- }
+ foreach ($this->fields[$name]['rules'] as $rule) {
+ $rule->validate($value, $name);
+ }
Summary by CodeRabbit
New Features
Documentation
Development