From 40eb314f332c8e22bc616b16c32a7bbde1e8a2fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raimondas=20Rimkevi=C4=8Dius=20=28aka=20MekDrop=29?= Date: Wed, 9 Jul 2025 00:20:20 +0300 Subject: [PATCH] Bumps min PHP to ^8.3 Resolves #53 --- .github/workflows/on-pull-request.yml | 10 +- composer.json | 65 +++-- src/ErrorsCollection.php | 392 ++++++++++++-------------- src/ErrorsTrait.php | 22 +- src/ParamsMode.php | 25 ++ tests/ErrorsCollectionTest.php | 174 +++++++----- tests/ErrorsTraitTest.php | 53 ++-- 7 files changed, 368 insertions(+), 373 deletions(-) create mode 100644 src/ParamsMode.php diff --git a/.github/workflows/on-pull-request.yml b/.github/workflows/on-pull-request.yml index 5069206..4375b3d 100644 --- a/.github/workflows/on-pull-request.yml +++ b/.github/workflows/on-pull-request.yml @@ -12,16 +12,8 @@ jobs: max-parallel: 2 matrix: php: - - 5.6 - - 7.0 - - 7.1 - - 7.2 - - 7.3 - - 7.4 - - 8.0 - - 8.1 - - 8.2 - 8.3 + - 8.4 os: - ubuntu-latest name: PHP ${{ matrix.php }}; ${{ matrix.os }} diff --git a/composer.json b/composer.json index 83b15b5..847fdb9 100644 --- a/composer.json +++ b/composer.json @@ -1,33 +1,38 @@ { - "name": "imponeer/object-errors", - "description": "Library that adds a possibility to collect errors for objects", - "keywords": [ - "errors", - "collection", - "object" - ], - "type": "library", - "require-dev": { - "phpunit/phpunit": "^5.2|^7.0|^8.0", - "mockery/mockery": "^1.0" - }, - "require": { - "ext-json": "*", - "php": ">=5.6" - }, - "license": "MIT", - "authors": [ - { - "name": "Raimondas Rimkevičius", - "email": "mekdrop@impresscms.org" + "name": "imponeer/object-errors", + "description": "Library that adds a possibility to collect errors for objects", + "keywords": [ + "errors", + "collection", + "object" + ], + "type": "library", + "require-dev": { + "phpunit/phpunit": "^5.2|^7.0|^8.0", + "mockery/mockery": "^1.0" + }, + "require": { + "ext-json": "*", + "php": ">=8.3" + }, + "license": "MIT", + "authors": [ + { + "name": "Raimondas Rimkevičius", + "email": "mekdrop@impresscms.org" + } + ], + "autoload": { + "psr-4": { + "Imponeer\\ObjectErrors\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Imponeer\\ObjectErrors\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "phpunit --testdox" } - ], - "autoload": { - "psr-4": { - "Imponeer\\ObjectErrors\\": "src/" - } - }, - "scripts": { - "test": "phpunit --testdox" - } } diff --git a/src/ErrorsCollection.php b/src/ErrorsCollection.php index 29fc29b..b51271e 100644 --- a/src/ErrorsCollection.php +++ b/src/ErrorsCollection.php @@ -4,226 +4,198 @@ use ArrayAccess; use Countable; +use JsonException; use JsonSerializable; -use Serializable; +use Stringable; /** * Collection of errors */ -class ErrorsCollection implements ArrayAccess, Countable, Serializable, JsonSerializable +class ErrorsCollection implements ArrayAccess, Countable, JsonSerializable, Stringable { - /** - * Mode that says that only one param for adding is used - */ - const MODE_1_PARAM = 0; - - /** - * Mode that says two params are used - */ - const MODE_2_PARAMS = 1; - - /** - * Mode that says that 2nd param is a used as prefix - */ - const MODE_2_AS_PREFIX = 2; - /** - * Mode how this errors collection works - * - * @var int - */ - public $mode = self::MODE_1_PARAM; - /** - * Errors data - * - * @var array - */ - private $errors = []; - - /** - * ErrorsCollection constructor. - * - * @param int $mode - */ - public function __construct($mode = self::MODE_1_PARAM) - { - $this->mode = $mode; - } - - /** - * Checks if offset exists - * - * @param mixed $offset - * @return bool - */ - public function offsetExists($offset) - { - return isset($this->errors[$offset]); - } - - /** - * Gets by pos - * - * @param mixed $offset - * - * @return mixed - */ - public function offsetGet($offset) - { - return $this->errors[$offset]; - } - - /** - * Sets by pos - * - * @param mixed $offset - * @param mixed $value - */ - public function offsetSet($offset, $value) - { - $this->errors[$offset] = $value; - } - - /** - * Tries to unset by offset but instead returns error - * - * @param mixed $offset - * - * @throws UnsetErrorException - */ - public function offsetUnset($offset) - { - throw new UnsetErrorException(); - } - - /** - * Is empty? - * - * @return bool - */ - public function isEmpty() - { - return empty($this->errors); - } - - /** - * Clear errors list - */ - public function clear() - { - $this->errors = []; - } - - /** - * Gets errors list as HTML - * - * @return string html listing the errors - */ - public function getHtml() - { - return nl2br( - $this->__toString() - ); - } - - /** - * Converts errors list to string - * - * @return string - */ - public function __toString() - { - if (empty($this->errors)) { - return ''; - } else { - return implode(PHP_EOL, $this->errors); - } - } - - /** - * Adds an - * - * @param array ...$err_data - */ - public function add(...$err_data) - { - switch ($this->mode) { - case self::MODE_1_PARAM: - $this->errors[] = $err_data[0]; - break; - case self::MODE_2_AS_PREFIX: - if (is_array($err_data[0])) { - if (!isset($err_data[1])) { - $err_data[1] = false; - } - foreach ($err_data[0] as $str) { - $this->add($str, $err_data[1]); - } - return; - } - if (isset($err_data[1]) && ($err_data[1] !== false)) { - $err_data[0] = "[" . $err_data[1] . "] " . $err_data[0]; - } - $this->errors[] = $err_data[0]; - break; - case self::MODE_2_PARAMS: - $this->errors[trim($err_data[0])] = trim($err_data[1]); - break; - } - } - - /** - * @inheritDoc - */ - public function count() - { - return count($this->errors); - } - - /** - * @inheritDoc - */ - public function serialize() - { - return serialize([ - $this->mode, - $this->errors - ]); - } - - /** - * @inheritDoc - */ - public function unserialize($serialized) - { - list($this->mode, $this->errors) = unserialize($serialized); - } - - /** - * @inheritDoc - */ - public function jsonSerialize() - { - return $this->errors; - } - - /** - * Export data to array - * - * @return array - */ - public function toArray() - { - return $this->errors; - } + /** + * Errors data + */ + private array $errors = []; + + /** + * ErrorsCollection constructor. + * + * @param ParamsMode $mode Mode how this errors collection works + */ + public function __construct( + public readonly ParamsMode $mode = ParamsMode::Mode1 + ) + { + } + + /** + * Checks if offset exists + * + * @param mixed $offset + * @return bool + */ + public function offsetExists(mixed $offset): bool + { + return isset($this->errors[$offset]); + } + + /** + * Gets by pos + * + * @param mixed $offset + * + * @return mixed + */ + public function offsetGet(mixed $offset): mixed + { + return $this->errors[$offset]; + } + + /** + * Sets by pos + * + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet(mixed $offset, mixed $value): void + { + $this->errors[$offset] = $value; + } + + /** + * Tries to unset by offset but instead returns error + * + * @param mixed $offset + * + * @throws UnsetErrorException + */ + public function offsetUnset(mixed $offset): never + { + throw new UnsetErrorException(); + } + + /** + * Is empty? + * + * @return bool + */ + public function isEmpty(): bool + { + return empty($this->errors); + } + + /** + * Clear errors list + */ + public function clear(): void + { + $this->errors = []; + } + + /** + * Gets errors list as HTML + * + * @return string html listing the errors + */ + public function getHtml(): string + { + return nl2br( + (string)$this + ); + } + + /** + * Converts errors list to string + * + * @return string + */ + public function __toString(): string + { + if (empty($this->errors)) { + return ''; + } + + return implode(PHP_EOL, $this->errors); + } + + /** + * Adds an + */ + public function add(mixed ...$err_data): void + { + switch ($this->mode) { + case ParamsMode::Mode1: + $this->errors[] = (string) $err_data[0]; + break; + case ParamsMode::Mode2AsPrefix: + if (is_array($err_data[0])) { + if (!isset($err_data[1])) { + $err_data[1] = false; + } + foreach ($err_data[0] as $str) { + $this->add($str, $err_data[1]); + } + return; + } + if (isset($err_data[1]) && ($err_data[1] !== false)) { + $err_data[0] = sprintf("[%s] %s", $err_data[1], $err_data[0]); + } + $this->errors[] = $err_data[0]; + break; + case ParamsMode::Mode2: + $this->errors[trim((string) $err_data[0])] = trim((string) $err_data[1]); + break; + } + } + + /** + * @inheritDoc + */ + public function count(): int + { + return count($this->errors); + } + + public function __serialize(): array + { + return [ + $this->mode, + $this->errors + ]; + } + + public function __unserialize(array $data): void + { + [$this->mode, $this->errors] = $data; + } + + /** + * @inheritDoc + */ + public function jsonSerialize(): array + { + return $this->errors; + } + + /** + * Export data to array + * + * @return array + */ + public function toArray(): array + { + return $this->errors; + } /** * Export data to json * - * @return string + * @throws JsonException */ - public function toJson() - { - return json_encode($this); - } + public function toJson(): string + { + return json_encode($this, JSON_THROW_ON_ERROR); + } } \ No newline at end of file diff --git a/src/ErrorsTrait.php b/src/ErrorsTrait.php index d5aa497..0ff9620 100644 --- a/src/ErrorsTrait.php +++ b/src/ErrorsTrait.php @@ -11,10 +11,8 @@ trait ErrorsTrait /** * Errors collection - * - * @var ErrorsCollection */ - protected $errors; + protected ErrorsCollection $errors; /** * ErrorsTrait constructor. @@ -27,13 +25,13 @@ public function __construct() /** * return the errors for this object as an array * - * @param bool $ashtml Format using HTML? + * @param bool $asHTML Format using HTML? * * @return array|string an array of errors */ - public function getErrors($ashtml = true) + public function getErrors(bool $asHTML = true): array|string { - return $ashtml ? $this->getHtmlErrors() : $this->errors->toArray(); + return $asHTML ? $this->getHtmlErrors() : $this->errors->toArray(); } /** @@ -41,27 +39,23 @@ public function getErrors($ashtml = true) * * @param string $err_str error to add */ - public function setErrors($err_str) + public function setErrors(string $err_str): void { call_user_func_array([$this->errors, 'add'], func_get_args()); } /** * Returns the errors for this object as html - * - * @return string */ - public function getHtmlErrors() + public function getHtmlErrors(): string { return $this->errors->getHtml(); } /** - * Has some errors - * - * @return bool + * Has some errors? */ - public function hasError() + public function hasError(): bool { return !$this->errors->isEmpty(); } diff --git a/src/ParamsMode.php b/src/ParamsMode.php new file mode 100644 index 0000000..13b10a6 --- /dev/null +++ b/src/ParamsMode.php @@ -0,0 +1,25 @@ +assertSame( - $mode, - $instance->mode, - 'Mode ' . $mode . ' is different after creating instance' - ); - } +class ErrorsCollectionTest extends TestCase +{ + + public function testDefaultConstructorParams(): void + { $instance = new ErrorsCollection(); $this->assertSame( - ErrorsCollection::MODE_1_PARAM, + ParamsMode::Mode1, $instance->mode, 'When creating ErrorsCollection instance default mode should be MODE_1_PARAM, but isn\'t' ); } - public function testImplements() { - $instance = new ErrorsCollection(); - $this->assertInstanceOf(\ArrayAccess::class, $instance, 'ErrorsCollection must implement ArrayAccess'); - $this->assertInstanceOf(\JsonSerializable::class, $instance, 'ErrorsCollection must implement JsonSerializable'); - $this->assertInstanceOf(\Countable::class, $instance, 'ErrorsCollection must implement Countable'); - $this->assertInstanceOf(\Serializable::class, $instance, 'ErrorsCollection must implement Serializable'); + /** + * @dataProvider provideConstructorParams + */ + public function testConstructorParams(ParamsMode $mode): void + { + $instance = new ErrorsCollection($mode); + $this->assertSame( + $mode, + $instance->mode, + 'Mode ' . $mode->name . ' is different after creating instance' + ); } - public function testOffsetExists() { - $instance = new ErrorsCollection(ErrorsCollection::MODE_2_PARAMS); + public function testOffsetExists(): void + { + $instance = new ErrorsCollection(ParamsMode::Mode2); $key = crc32(time()); $this->assertArrayNotHasKey( $key, @@ -52,23 +52,25 @@ public function testOffsetExists() { ); } - public function testOffsetGet() { + public function testOffsetGet(): void + { $offset = crc32(time()); $data = sha1(time()); - $instance = new ErrorsCollection(ErrorsCollection::MODE_2_PARAMS); + $instance = new ErrorsCollection(ParamsMode::Mode2); $instance->add($offset, $data); $this->assertSame($instance[$offset], $data, 'Data is not same #1'); $this->assertSame($instance->offsetGet($offset), $data, 'Data is not same #2'); - $this->assertNotSame($instance[$offset], null, 'Data is not same #3'); - $this->assertNotSame($instance->offsetGet($offset), null, 'Data is not same #4'); + $this->assertNotNull($instance[$offset], 'Data is not same #3'); + $this->assertNotNull($instance->offsetGet($offset), 'Data is not same #4'); } - public function testOffsetSet() { + public function testOffsetSet(): void + { $offset = crc32(time()); $data = sha1(time()); - $instance = new ErrorsCollection(ErrorsCollection::MODE_2_PARAMS); + $instance = new ErrorsCollection(ParamsMode::Mode2); $instance->add($offset, $data); $ndata = md5(time()); @@ -90,26 +92,18 @@ public function testOffsetSet() { $this->assertSame($instance[$offset], $ndata, 'Changed data is not same as readed one #4'); } - public function testOffsetUnset() { + public function testOffsetUnset(): void + { $offset = crc32(time()); - $instance = new ErrorsCollection(ErrorsCollection::MODE_2_PARAMS); + $instance = new ErrorsCollection(ParamsMode::Mode2); $this->expectException(UnsetErrorException::class); $instance->offsetUnset($offset); - unset($instance[$offset]); - - $this->expectException(null); - $instance->add($offset, crc32(time())); - - $this->expectException(UnsetErrorException::class); - $instance->offsetUnset($offset); - unset($instance[$offset]); - - $this->expectException(null); } - public function testIsEmpty() { + public function testIsEmpty(): void + { $instance = new ErrorsCollection(); $this->assertTrue($instance->isEmpty(), 'Is not empty after creation'); @@ -117,34 +111,33 @@ public function testIsEmpty() { $this->assertNotTrue($instance->isEmpty(), 'Is still empty after one element was added'); } - public function testClear() { + public function testClear(): void + { $instance = new ErrorsCollection(); $instance->add(crc32(time())); $instance->clear(); $this->assertEmpty($instance, 'Clear() must clear'); } - public function testStringConversion() { + public function testStringConversion(): void + { $instance = new ErrorsCollection(); - $this->assertEmpty((string) $instance, 'Converted to string empty ErrorsCollection must be empty'); + $this->assertEmpty((string)$instance, 'Converted to string empty ErrorsCollection must be empty'); $this->assertEmpty($instance->getHtml(), 'Converted to HTML empty ErrorsCollection must be empty'); $instance->add(crc32(time())); - $this->assertNotEmpty((string) $instance, 'Converted to string not empty ErrorsCollection must be not empty'); + $this->assertNotEmpty((string)$instance, 'Converted to string not empty ErrorsCollection must be not empty'); $this->assertNotEmpty($instance->getHtml(), 'Converted to HTML not empty ErrorsCollection must be not empty'); - if (method_exists($this, 'assertIsString')) { - $this->assertIsString($instance->getHtml(), 'getHTML must generate strings'); - } else { - $this->assertInternalType('string', $instance->getHtml(), 'getHTML must generate strings'); - } + $this->assertIsString($instance->getHtml(), 'getHTML must generate strings'); } - public function testCount() { + public function testCount(): void + { $instance = new ErrorsCollection(); - $this->assertSame(0, $instance->count(), 'Count is not 0 when collection was just created'); + $this->assertCount(0, $instance, 'Count is not 0 when collection was just created'); $instance->add(crc32(time())); $this->assertSame(1, $instance->count(), 'Count must be 1 after one element was added'); @@ -152,25 +145,52 @@ public function testCount() { $this->assertCount(1, $instance, 'Count function doesn\'t work'); } - public function testAdd() { - $instance = new ErrorsCollection(ErrorsCollection::MODE_1_PARAM); - $instance->add(md5(time())); - $this->assertArrayHasKey(0, $instance, 'With MODE_1_PARAM after adding element first element must be with index 0'); + public function provideTestAddData(): Generator + { + yield 'mode1' => [ + 'mode' => ParamsMode::Mode1, + 'addParams' => [ + md5(time()) + ], + 'expectedKey' => 0 + ]; + + yield 'mode2asprefix' => [ + 'mode' => ParamsMode::Mode2AsPrefix, + 'addParams' => [ + md5(time()) + ], + 'expectedKey' => 0 + ]; - $instance = new ErrorsCollection(ErrorsCollection::MODE_2_AS_PREFIX); - $instance->add(md5(time())); - $this->assertArrayHasKey(0, $instance, 'With MODE_2_AS_PREFIX after adding element first element must be with index 0'); - - $instance = new ErrorsCollection(ErrorsCollection::MODE_2_PARAMS); $key = crc32(time()); - $instance->add($key, md5(time())); - $this->assertArrayHasKey($key, $instance, 'With MODE_2_PARAMS after adding element first element must be with index same as added key'); + yield 'mode2' => [ + 'mode' => ParamsMode::Mode2, + 'addParams' => [ + $key, + md5(time()) + ], + 'expectedKey' => $key + ]; } - public function testSerialization() { - $instance = new ErrorsCollection(); + /** + * @dataProvider provideTestAddData + */ + public function testAdd(ParamsMode $mode, array $addParams, int|string $expectedKey): void + { + $instance = new ErrorsCollection($mode); + call_user_func_array([$instance, 'add'], $addParams); + $this->assertArrayHasKey($expectedKey, $instance); + } + + /** + * @throws JsonException + */ + public function testSerialization(): void + { + $instance = new ErrorsCollection(ParamsMode::Mode2); - $instance->mode = ErrorsCollection::MODE_2_PARAMS; $instance->add(md5(time()), sha1(time())); $instance->add(crc32(time()), soundex(time())); @@ -180,12 +200,16 @@ public function testSerialization() { $this->assertSame($instance->mode, $unserialized->mode, 'Serialization-unserialization fails #1'); $this->assertSame($instance->toArray(), $unserialized->toArray(), 'Serialization-unserialization fails #2'); - if (method_exists($this, 'assertIsString')) { - $this->assertIsArray($instance->toArray(), 'toArray doesn\'t makes an array'); - $this->assertIsString($instance->toJson(), 'toJSON doesn\'t makes a string'); - } else { - $this->assertInternalType('array', $instance->toArray(), 'toArray doesn\'t makes an array'); - $this->assertInternalType('string', $instance->toJson(), 'toJSON doesn\'t makes a string'); + $this->assertIsArray($instance->toArray(), 'toArray doesn\'t makes an array'); + $this->assertIsString($instance->toJson(), 'toJSON doesn\'t makes a string'); + } + + public function provideConstructorParams(): Generator + { + foreach (ParamsMode::cases() as $mode) { + yield $mode->name => [ + 'mode' => $mode, + ]; } } diff --git a/tests/ErrorsTraitTest.php b/tests/ErrorsTraitTest.php index 54682ef..ba3c69b 100644 --- a/tests/ErrorsTraitTest.php +++ b/tests/ErrorsTraitTest.php @@ -1,5 +1,6 @@ makePartial(); $mock->__construct(); return $mock; } - public function testGetErrors() + public function testGetErrors(): void { $mock = $this->createTraitMock(); @@ -40,14 +40,12 @@ public function testGetErrors() } else { - $this->assertInternalType( - 'array', + $this->assertIsArray( $mock->getErrors(false), 'getErrors without param must return array' ); - $this->assertInternalType( - 'string', + $this->assertIsString( $mock->getErrors(true), 'getErrors without param must return string' ); @@ -55,40 +53,25 @@ public function testGetErrors() } } - public function testGetHtmlErrors() + public function testGetHtmlErrors(): void { $mock = $this->createTraitMock(); - if (method_exists($this, 'assertIsString')) { - $this->assertIsString( - $mock->getHtmlErrors(), - 'getErrors without param must return array' - ); - } else { - $this->assertInternalType( - 'string', - $mock->getHtmlErrors(), - 'getErrors without param must return array' - ); - } + $this->assertIsString( + $mock->getHtmlErrors(), + 'getErrors without param must return array' + ); } - public function testHasAndSetError() + public function testHasAndSetError(): void { $mock = $this->createTraitMock(); - if (method_exists($this, 'assertIsBool')) { - $this->assertIsBool( - $mock->hasError(), - 'hasError method should return bool' - ); - } else { - $this->assertInternalType( - 'bool', - $mock->hasError(), - 'hasError method should return bool' - ); - } + $this->assertIsBool( + $mock->hasError(), + 'hasError method should return bool' + ); + $this->assertFalse( $mock->hasError(), 'When there are no errors hasError should return false'