From add013de24a08743834382bc4e3f0e8ed945de7c Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Fri, 10 Oct 2025 15:06:28 +0200 Subject: [PATCH 1/3] feat: add class SafeCast --- README.md | 25 ++++ src/FluidXPlate/FluidXScanner.php | 2 +- src/SafeCast.php | 184 ++++++++++++++++++++++++++++++ tests/SafeCastTest.php | 173 ++++++++++++++++++++++++++++ 4 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 src/SafeCast.php create mode 100644 tests/SafeCastTest.php diff --git a/README.md b/README.md index 3cc88c1..ee9fc77 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,31 @@ composer require mll-lab/php-utils See [tests](tests). +### SafeCast + +PHP's native type casts like `(int)` and `(float)` can produce unexpected results, especially when casting from strings. +The `SafeCast` utility provides safe alternatives that validate input before casting: + +```php +use MLL\Utils\SafeCast; + +// Safe integer casting +SafeCast::toInt(42); // 42 +SafeCast::toInt('42'); // 42 +SafeCast::toInt('hello'); // throws InvalidArgumentException + +// Safe float casting +SafeCast::toFloat(3.14); // 3.14 +SafeCast::toFloat('3.14'); // 3.14 +SafeCast::toFloat('abc'); // throws InvalidArgumentException + +// Safe string casting +SafeCast::toString(42); // '42' +SafeCast::toString(null); // '' +``` + +See [tests](tests/SafeCastTest.php) for more examples. + ### Holidays You can add custom holidays by registering a method that returns a map of holidays for a given year. diff --git a/src/FluidXPlate/FluidXScanner.php b/src/FluidXPlate/FluidXScanner.php index 166ddf5..664a394 100644 --- a/src/FluidXPlate/FluidXScanner.php +++ b/src/FluidXPlate/FluidXScanner.php @@ -54,7 +54,7 @@ public static function parseRawContent(string $rawContent): FluidXPlate $barcodes = []; $id = null; foreach ($lines as $line) { - if ($line === '' || $line === self::READING || $line === self::XTR_96_CONNECTED) { + if (in_array($line, ['', self::READING, self::XTR_96_CONNECTED], true)) { continue; } $content = explode(', ', $line); diff --git a/src/SafeCast.php b/src/SafeCast.php new file mode 100644 index 0000000..a1b507d --- /dev/null +++ b/src/SafeCast.php @@ -0,0 +1,184 @@ + 5) + if (is_float($value)) { + if ($value === floor($value) && is_finite($value)) { + return (int) $value; + } + + throw new \InvalidArgumentException('Float value "' . $value . '" cannot be safely cast to int (not a whole number or not finite)'); + } + + if (is_string($value)) { + $trimmed = trim($value); + + // Empty string is not a valid integer + if ($trimmed === '') { + throw new \InvalidArgumentException('Empty string cannot be cast to int'); + } + + // Check if the string represents a valid integer + if (! self::isIntegerString($trimmed)) { + throw new \InvalidArgumentException('String value "' . $value . '" is not a valid integer format'); + } + + return (int) $trimmed; + } + + throw new \InvalidArgumentException('Cannot cast value of type "' . gettype($value) . '" to int'); + } + + /** + * Safely cast a value to a float. + * + * Only accepts: + * - Floats (returned as-is) + * - Integers (cast to float) + * - Numeric strings that represent valid floats + * + * @param mixed $value The value to cast + * + * @throws \InvalidArgumentException If the value cannot be safely cast to a float + */ + public static function toFloat($value): float + { + if (is_float($value)) { + return $value; + } + + if (is_int($value)) { + return (float) $value; + } + + if (is_string($value)) { + $trimmed = trim($value); + + // Empty string is not a valid float + if ($trimmed === '') { + throw new \InvalidArgumentException('Empty string cannot be cast to float'); + } + + // Check if the string represents a valid numeric value + if (! self::isNumericString($trimmed)) { + throw new \InvalidArgumentException('String value "' . $value . '" is not a valid numeric format'); + } + + return (float) $trimmed; + } + + throw new \InvalidArgumentException('Cannot cast value of type "' . gettype($value) . '" to float'); + } + + /** + * Safely cast a value to a string. + * + * Only accepts: + * - Strings (returned as-is) + * - Integers and floats (converted to string) + * - Objects with __toString() method + * - null (converted to empty string) + * + * @param mixed $value The value to cast + * + * @throws \InvalidArgumentException If the value cannot be safely cast to a string + */ + public static function toString($value): string + { + if (is_string($value)) { + return $value; + } + + if (is_int($value) || is_float($value)) { + return (string) $value; + } + + if ($value === null) { + return ''; + } + + if (is_object($value) && method_exists($value, '__toString')) { + return (string) $value; + } + + throw new \InvalidArgumentException('Cannot cast value of type "' . gettype($value) . '" to string'); + } + + /** + * Check if a string represents a valid integer. + * + * Accepts optional leading/trailing whitespace, optional sign, and digits only. + */ + private static function isIntegerString(string $value): bool + { + try { + return preg_match('/^[+-]?\d+$/', $value) === 1; + } catch (PcreException $ex) { + return false; + } + } + + /** + * Check if a string represents a valid numeric value (integer or float). + * + * Accepts scientific notation, decimals with optional sign. + */ + private static function isNumericString(string $value): bool + { + // Use is_numeric() but verify it's not in a weird format + if (! is_numeric($value)) { + return false; + } + + // is_numeric accepts some formats we might want to reject + // like hexadecimal (0x1F) or binary (0b1010) + // Check for these and reject them for stricter validation + try { + $hasHexOrBinary = preg_match('/^0[xXbB]/', $value) === 1; + + return ! $hasHexOrBinary; + } catch (PcreException $ex) { + return false; + } + } +} diff --git a/tests/SafeCastTest.php b/tests/SafeCastTest.php new file mode 100644 index 0000000..ff8716f --- /dev/null +++ b/tests/SafeCastTest.php @@ -0,0 +1,173 @@ + */ + public static function validIntProvider(): iterable + { + yield [42, 42]; + yield [-123, -123]; + yield [0, 0]; + yield [42, '42']; + yield [-123, '-123']; + yield [0, '0']; + yield [999, ' 999 ']; + yield [5, 5.0]; + yield [-10, -10.0]; + yield [0, 0.0]; + yield [42, '042']; + yield [0, '000']; + yield [42, '+42']; + } + + /** + * @dataProvider invalidIntProvider + * + * @param mixed $input can be anything + */ + #[DataProvider('invalidIntProvider')] + public function testToIntThrowsExceptionForInvalidInput(string $expectedMessage, $input): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage($expectedMessage); + SafeCast::toInt($input); + } + + /** @return iterable */ + public static function invalidIntProvider(): iterable + { + yield ['String value "hello" is not a valid integer format', 'hello']; + yield ['String value "123abc" is not a valid integer format', '123abc']; + yield ['String value "12.34" is not a valid integer format', '12.34']; + yield ['Empty string cannot be cast to int', '']; + yield ['Float value "5.5" cannot be safely cast to int', 5.5]; + yield ['cannot be safely cast to int (not a whole number or not finite)', INF]; + yield ['Cannot cast value of type "array" to int', []]; + } + + /** + * @dataProvider validFloatProvider + * + * @param mixed $input can be anything + */ + #[DataProvider('validFloatProvider')] + public function testToFloatWithValidInput(float $expected, $input): void + { + self::assertSame($expected, SafeCast::toFloat($input)); + } + + /** @return iterable */ + public static function validFloatProvider(): iterable + { + yield [3.14, 3.14]; + yield [-2.5, -2.5]; + yield [0.0, 0.0]; + yield [42.0, 42]; + yield [-123.0, -123]; + yield [0.0, 0]; + yield [3.14, '3.14']; + yield [-2.5, '-2.5']; + yield [42.0, '42']; + yield [1.23, ' 1.23 ']; + yield [1.5e3, '1.5e3']; + yield [2.5e-2, '2.5e-2']; + } + + /** + * @dataProvider invalidFloatProvider + * + * @param mixed $input can be anything + */ + #[DataProvider('invalidFloatProvider')] + public function testToFloatThrowsExceptionForInvalidInput(string $expectedMessage, $input): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage($expectedMessage); + SafeCast::toFloat($input); + } + + /** @return iterable */ + public static function invalidFloatProvider(): iterable + { + yield ['String value "hello" is not a valid numeric format', 'hello']; + yield ['String value "3.14abc" is not a valid numeric format', '3.14abc']; + yield ['String value "1.2.3" is not a valid numeric format', '1.2.3']; + yield ['Empty string cannot be cast to float', '']; + yield ['Cannot cast value of type "array" to float', []]; + yield ['is not a valid numeric format', '0x1A']; + yield ['is not a valid numeric format', '0b1010']; + } + + /** + * @dataProvider validStringProvider + * + * @param mixed $input can be anything + */ + #[DataProvider('validStringProvider')] + public function testToStringWithValidInput(string $expected, $input): void + { + self::assertSame($expected, SafeCast::toString($input)); + } + + /** @return iterable */ + public static function validStringProvider(): iterable + { + yield ['hello', 'hello']; + yield ['', '']; + yield ['42', 42]; + yield ['-123', -123]; + yield ['0', 0]; + yield ['3.14', 3.14]; + yield ['-2.5', -2.5]; + yield ['', null]; + } + + public function testToStringWithObjectHavingToStringMethod(): void + { + $object = new class() { + public function __toString(): string + { + return 'object-string'; + } + }; + + self::assertSame('object-string', SafeCast::toString($object)); + } + + /** + * @dataProvider invalidStringProvider + * + * @param mixed $input can be anything + */ + #[DataProvider('invalidStringProvider')] + public function testToStringThrowsExceptionForInvalidInput(string $expectedMessage, $input): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage($expectedMessage); + SafeCast::toString($input); + } + + /** @return iterable */ + public static function invalidStringProvider(): iterable + { + yield ['Cannot cast value of type "object" to string', new \stdClass()]; + yield ['Cannot cast value of type "array" to string', []]; + } +} From e2bf17eb72e923a1b756bde5d04d69971f6ec2c1 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Mon, 10 Nov 2025 12:11:03 +0100 Subject: [PATCH 2/3] actually use SafeCast --- .../V2/BclConvert/OverrideCycles.php | 3 ++- .../LightcyclerDataParsingTrait.php | 7 ++----- .../LightcyclerXmlParser.php | 3 ++- .../AbsoluteQuantificationSample.php | 7 ++++++- src/Microplate/CoordinateSystem.php | 9 +++++---- src/Microplate/Coordinates.php | 3 ++- src/Microplate/FullColumnSection.php | 3 ++- .../MicroplateSet/MicroplateSet.php | 3 ++- src/SafeCast.php | 20 +++---------------- src/StringUtil.php | 7 ++++--- .../BasicPipettingActionCommand.php | 3 ++- .../V2/DataSectionTest.php | 9 +++++++++ .../QpcrXmlParserTest.php | 2 +- tests/SafeCastTest.php | 6 ++++++ tests/StringUtilTest.php | 8 ++++++++ 15 files changed, 56 insertions(+), 37 deletions(-) diff --git a/src/IlluminaSampleSheet/V2/BclConvert/OverrideCycles.php b/src/IlluminaSampleSheet/V2/BclConvert/OverrideCycles.php index 03715cf..33565ac 100644 --- a/src/IlluminaSampleSheet/V2/BclConvert/OverrideCycles.php +++ b/src/IlluminaSampleSheet/V2/BclConvert/OverrideCycles.php @@ -4,6 +4,7 @@ use MLL\Utils\IlluminaSampleSheet\IlluminaSampleSheetException; use MLL\Utils\IlluminaSampleSheet\V2\HeaderSection; +use MLL\Utils\SafeCast; class OverrideCycles { @@ -55,7 +56,7 @@ public function makeOverrideCycle(string $cycleString): OverrideCycle return new OverrideCycle( array_map( - fn (array $match): CycleTypeWithCount => new CycleTypeWithCount(new CycleType($match[1]), (int) $match[2]), + fn (array $match): CycleTypeWithCount => new CycleTypeWithCount(new CycleType($match[1]), SafeCast::toInt($match[2])), $matches ) ); diff --git a/src/LightcyclerExportSheet/LightcyclerDataParsingTrait.php b/src/LightcyclerExportSheet/LightcyclerDataParsingTrait.php index d40dea0..014a8c0 100644 --- a/src/LightcyclerExportSheet/LightcyclerDataParsingTrait.php +++ b/src/LightcyclerExportSheet/LightcyclerDataParsingTrait.php @@ -3,6 +3,7 @@ namespace MLL\Utils\LightcyclerExportSheet; use Illuminate\Support\Collection; +use MLL\Utils\SafeCast; trait LightcyclerDataParsingTrait { @@ -14,11 +15,7 @@ protected function parseFloatValue(?string $value): ?float return null; } - if (! is_numeric($cleanString)) { - throw new \InvalidArgumentException("Invalid float value: '{$cleanString}'"); - } - - return (float) $cleanString; + return SafeCast::toFloat($cleanString); } /** @return array{float, float} */ diff --git a/src/LightcyclerExportSheet/LightcyclerXmlParser.php b/src/LightcyclerExportSheet/LightcyclerXmlParser.php index 3f3b0f0..08a75ac 100644 --- a/src/LightcyclerExportSheet/LightcyclerXmlParser.php +++ b/src/LightcyclerExportSheet/LightcyclerXmlParser.php @@ -5,6 +5,7 @@ use Illuminate\Support\Collection; use MLL\Utils\Microplate\Coordinates; use MLL\Utils\Microplate\CoordinateSystem12x8; +use MLL\Utils\SafeCast; use function Safe\simplexml_load_string; @@ -77,7 +78,7 @@ private function extractPropertiesFromXml(\SimpleXMLElement $xmlElement): array $properties = []; foreach ($xmlElement->prop as $propertyNode) { - $propertyName = (string) $propertyNode->attributes()->name; + $propertyName = SafeCast::toString($propertyNode->attributes()->name); $propertyValue = $propertyNode->__toString(); if (! isset($properties[$propertyName])) { diff --git a/src/LightcyclerSampleSheet/AbsoluteQuantificationSample.php b/src/LightcyclerSampleSheet/AbsoluteQuantificationSample.php index 9a236b8..eefb394 100644 --- a/src/LightcyclerSampleSheet/AbsoluteQuantificationSample.php +++ b/src/LightcyclerSampleSheet/AbsoluteQuantificationSample.php @@ -4,6 +4,7 @@ use MLL\Utils\Microplate\Coordinates; use MLL\Utils\Microplate\CoordinateSystem12x8; +use MLL\Utils\SafeCast; class AbsoluteQuantificationSample { @@ -42,7 +43,11 @@ public static function formatConcentration(?int $concentration): ?string return null; } - $exponent = (int) floor(log10(abs($concentration))); + if ($concentration === 0) { + return '0.00E0'; + } + + $exponent = SafeCast::toInt(floor(log10(abs($concentration)))); $mantissa = $concentration / (10 ** $exponent); return number_format($mantissa, 2) . 'E' . $exponent; diff --git a/src/Microplate/CoordinateSystem.php b/src/Microplate/CoordinateSystem.php index e63014b..729fe29 100644 --- a/src/Microplate/CoordinateSystem.php +++ b/src/Microplate/CoordinateSystem.php @@ -3,6 +3,7 @@ namespace MLL\Utils\Microplate; use Illuminate\Support\Arr; +use MLL\Utils\SafeCast; /** * Children should be called `CoordinateSystemXxY`, where X is the number of columns and Y is the number of rows. @@ -34,14 +35,14 @@ public function paddedColumns(): array /** 0-pad column to be as long as the longest column in the coordinate system. */ public function padColumn(int $column): string { - $maxColumnLength = strlen((string) $this->columnsCount()); + $maxColumnLength = strlen(SafeCast::toString($this->columnsCount())); - return str_pad((string) $column, $maxColumnLength, '0', STR_PAD_LEFT); + return str_pad(SafeCast::toString($column), $maxColumnLength, '0', STR_PAD_LEFT); } public function rowForRowFlowPosition(int $position): string { - $index = (int) floor(($position - 1) / $this->columnsCount()); + $index = SafeCast::toInt(floor(($position - 1) / $this->columnsCount())); return $this->rows()[$index]; } @@ -58,7 +59,7 @@ public function columnForRowFlowPosition(int $position): int public function columnForColumnFlowPosition(int $position): int { - $index = (int) floor(($position - 1) / $this->rowsCount()); + $index = SafeCast::toInt(floor(($position - 1) / $this->rowsCount())); return $this->columns()[$index]; } diff --git a/src/Microplate/Coordinates.php b/src/Microplate/Coordinates.php index a13c7b4..dd7cb36 100644 --- a/src/Microplate/Coordinates.php +++ b/src/Microplate/Coordinates.php @@ -5,6 +5,7 @@ use Illuminate\Support\Arr; use MLL\Utils\Microplate\Enums\FlowDirection; use MLL\Utils\Microplate\Exceptions\UnexpectedFlowDirection; +use MLL\Utils\SafeCast; use function Safe\preg_match; @@ -89,7 +90,7 @@ public static function fromString(string $coordinatesString, CoordinateSystem $c } /** @var array{1: string, 2: string} $matches */ - return new static($matches[1], (int) $matches[2], $coordinateSystem); + return new static($matches[1], SafeCast::toInt($matches[2]), $coordinateSystem); } /** diff --git a/src/Microplate/FullColumnSection.php b/src/Microplate/FullColumnSection.php index 6a87c26..e985aae 100644 --- a/src/Microplate/FullColumnSection.php +++ b/src/Microplate/FullColumnSection.php @@ -4,6 +4,7 @@ use MLL\Utils\Microplate\Exceptions\MicroplateIsFullException; use MLL\Utils\Microplate\Exceptions\SectionIsFullException; +use MLL\Utils\SafeCast; /** * A section that occupies all wells of a column if one sample exists in this column. @@ -90,6 +91,6 @@ private function sectionCanGrow(): bool private function reservedColumns(): int { - return (int) ceil($this->sectionItems->count() / $this->sectionedMicroplate->coordinateSystem->rowsCount()); + return SafeCast::toInt(ceil($this->sectionItems->count() / $this->sectionedMicroplate->coordinateSystem->rowsCount())); } } diff --git a/src/Microplate/MicroplateSet/MicroplateSet.php b/src/Microplate/MicroplateSet/MicroplateSet.php index f00b249..ce181ca 100644 --- a/src/Microplate/MicroplateSet/MicroplateSet.php +++ b/src/Microplate/MicroplateSet/MicroplateSet.php @@ -5,6 +5,7 @@ use MLL\Utils\Microplate\Coordinates; use MLL\Utils\Microplate\CoordinateSystem; use MLL\Utils\Microplate\Enums\FlowDirection; +use MLL\Utils\SafeCast; /** @template TCoordinateSystem of CoordinateSystem */ abstract class MicroplateSet @@ -39,7 +40,7 @@ public function locationFromPosition(int $setPosition, FlowDirection $direction) throw new \OutOfRangeException("Expected a position between 1-{$positionsCount}, got: {$setPosition}."); } - $plateIndex = (int) floor(($setPosition - 1) / $this->coordinateSystem->positionsCount()); + $plateIndex = SafeCast::toInt(floor(($setPosition - 1) / $this->coordinateSystem->positionsCount())); $positionOnSinglePlate = $setPosition - ($plateIndex * $this->coordinateSystem->positionsCount()); return new Location( diff --git a/src/SafeCast.php b/src/SafeCast.php index a1b507d..07b7328 100644 --- a/src/SafeCast.php +++ b/src/SafeCast.php @@ -2,8 +2,6 @@ namespace MLL\Utils; -use Safe\Exceptions\PcreException; - use function Safe\preg_match; /** @@ -151,11 +149,7 @@ public static function toString($value): string */ private static function isIntegerString(string $value): bool { - try { - return preg_match('/^[+-]?\d+$/', $value) === 1; - } catch (PcreException $ex) { - return false; - } + return preg_match('/^[+-]?\d+$/', $value) === 1; } /** @@ -165,20 +159,12 @@ private static function isIntegerString(string $value): bool */ private static function isNumericString(string $value): bool { - // Use is_numeric() but verify it's not in a weird format if (! is_numeric($value)) { return false; } - // is_numeric accepts some formats we might want to reject - // like hexadecimal (0x1F) or binary (0b1010) + // is_numeric accepts some formats we want to reject, like hexadecimal (0x1F) or binary (0b1010). // Check for these and reject them for stricter validation - try { - $hasHexOrBinary = preg_match('/^0[xXbB]/', $value) === 1; - - return ! $hasHexOrBinary; - } catch (PcreException $ex) { - return false; - } + return preg_match('/^0[xXbB]/', $value) !== 1; } } diff --git a/src/StringUtil.php b/src/StringUtil.php index 21a6092..bddc93e 100644 --- a/src/StringUtil.php +++ b/src/StringUtil.php @@ -171,11 +171,12 @@ private static function guessEncoding(string $text): string */ public static function leftPadNumber($number, int $length): string { - if (is_string($number) && ! is_numeric($number)) { - throw new \InvalidArgumentException("Expected numeric string, got: {$number}"); + // For strings, validate they're numeric by casting to float first + if (is_string($number)) { + $number = SafeCast::toFloat($number); } - return str_pad((string) $number, $length, '0', STR_PAD_LEFT); + return str_pad(SafeCast::toString($number), $length, '0', STR_PAD_LEFT); } /** Remove forbidden chars (<,>,:,",/,\,|,?,*) from file name. */ diff --git a/src/Tecan/BasicCommands/BasicPipettingActionCommand.php b/src/Tecan/BasicCommands/BasicPipettingActionCommand.php index 37fe25d..80defdc 100644 --- a/src/Tecan/BasicCommands/BasicPipettingActionCommand.php +++ b/src/Tecan/BasicCommands/BasicPipettingActionCommand.php @@ -2,6 +2,7 @@ namespace MLL\Utils\Tecan\BasicCommands; +use MLL\Utils\SafeCast; use MLL\Utils\Tecan\LiquidClass\LiquidClass; use MLL\Utils\Tecan\Location\Location; @@ -36,6 +37,6 @@ protected function getTipMask(): string public function setTipMask(int $tipMask): void { - $this->tipMask = (string) $tipMask; + $this->tipMask = SafeCast::toString($tipMask); } } diff --git a/tests/IlluminaSampleSheet/V2/DataSectionTest.php b/tests/IlluminaSampleSheet/V2/DataSectionTest.php index af44ba2..cffb3e4 100644 --- a/tests/IlluminaSampleSheet/V2/DataSectionTest.php +++ b/tests/IlluminaSampleSheet/V2/DataSectionTest.php @@ -55,4 +55,13 @@ public function testToStringWithProject(): void self::assertSame($expected, $dataSection->convertSectionToString()); } + + public function testThrowsExceptionForInvalidCycleFormat(): void + { + $dataSection = new DataSection(); + + $this->expectException(IlluminaSampleSheetException::class); + $this->expectExceptionMessage('Invalid Override Cycle Part'); + new OverrideCycles($dataSection, 'invalid', 'I8', null, null); + } } diff --git a/tests/LightcyclerExportSheet/QpcrXmlParserTest.php b/tests/LightcyclerExportSheet/QpcrXmlParserTest.php index 24ee8cc..42e45c7 100644 --- a/tests/LightcyclerExportSheet/QpcrXmlParserTest.php +++ b/tests/LightcyclerExportSheet/QpcrXmlParserTest.php @@ -219,7 +219,7 @@ public function testParseXmlHandlesInvalidFloatValues(): void XML; - $this->expectExceptionObject(new \InvalidArgumentException("Invalid float value: 'invalid'")); + $this->expectExceptionObject(new \InvalidArgumentException('String value "invalid" is not a valid numeric format')); $parser = new LightcyclerXmlParser(); $parser->parse($xmlWithInvalidFloat); diff --git a/tests/SafeCastTest.php b/tests/SafeCastTest.php index ff8716f..4787961 100644 --- a/tests/SafeCastTest.php +++ b/tests/SafeCastTest.php @@ -60,6 +60,8 @@ public static function invalidIntProvider(): iterable yield ['Float value "5.5" cannot be safely cast to int', 5.5]; yield ['cannot be safely cast to int (not a whole number or not finite)', INF]; yield ['Cannot cast value of type "array" to int', []]; + yield ['Cannot cast value of type "boolean" to int', true]; + yield ['Cannot cast value of type "boolean" to int', false]; } /** @@ -113,6 +115,8 @@ public static function invalidFloatProvider(): iterable yield ['Cannot cast value of type "array" to float', []]; yield ['is not a valid numeric format', '0x1A']; yield ['is not a valid numeric format', '0b1010']; + yield ['Cannot cast value of type "boolean" to float', true]; + yield ['Cannot cast value of type "boolean" to float', false]; } /** @@ -169,5 +173,7 @@ public static function invalidStringProvider(): iterable { yield ['Cannot cast value of type "object" to string', new \stdClass()]; yield ['Cannot cast value of type "array" to string', []]; + yield ['Cannot cast value of type "boolean" to string', true]; + yield ['Cannot cast value of type "boolean" to string', false]; } } diff --git a/tests/StringUtilTest.php b/tests/StringUtilTest.php index c0ac9bc..1234b30 100644 --- a/tests/StringUtilTest.php +++ b/tests/StringUtilTest.php @@ -154,9 +154,17 @@ public function testLeftPadNumber(): void ); self::expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('is not a valid numeric format'); StringUtil::leftPadNumber('foo', 3); } + public function testLeftPadNumberRejectsHexAndBinary(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('is not a valid numeric format'); + StringUtil::leftPadNumber('0x1A', 5); + } + public function testHasContent(): void { self::assertFalse(StringUtil::hasContent(null)); From e2bfcb8767f8c9da4bd9824758de4667b4c28152 Mon Sep 17 00:00:00 2001 From: spawnia <12158000+spawnia@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:54:18 +0000 Subject: [PATCH 3/3] Apply php-cs-fixer changes --- tests/SafeCastTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/SafeCastTest.php b/tests/SafeCastTest.php index 4787961..1dd3700 100644 --- a/tests/SafeCastTest.php +++ b/tests/SafeCastTest.php @@ -145,7 +145,7 @@ public static function validStringProvider(): iterable public function testToStringWithObjectHavingToStringMethod(): void { - $object = new class() { + $object = new class() implements \Stringable { public function __toString(): string { return 'object-string';