├── .gitignore ├── src ├── Equatable.php ├── functions.php └── Result.php ├── phpunit.xml.dist ├── .github └── workflows │ └── continuous-integration.yml ├── composer.json ├── LICENSE ├── CHANGELOG.md ├── tests ├── ResultTest.php └── FunctionsTest.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /vendor 3 | /composer.lock 4 | /.phpunit.cache 5 | -------------------------------------------------------------------------------- /src/Equatable.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | runs-on: "ubuntu-latest" 8 | 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | php-version: 13 | - 8.4 14 | dependencies: 15 | - "highest" 16 | 17 | steps: 18 | - uses: "actions/checkout@v5" 19 | 20 | - name: "Setup PHP" 21 | uses: "shivammathur/setup-php@v2" 22 | with: 23 | php-version: "${{ matrix.php-version }}" 24 | 25 | - name: "Install dependencies using Composer" 26 | uses: "ramsey/composer-install@v3" 27 | with: 28 | dependency-versions: "${{ matrix.dependencies }}" 29 | 30 | - name: "Run PHPUnit" 31 | run: "vendor/bin/phpunit" 32 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jungi/common", 3 | "description": "A minimal library that defines primitive building blocks of PHP code.", 4 | "keywords": ["equatable", "result", "expected"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Piotr Kugla", 10 | "email": "piku235@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=8.4" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^12.3" 18 | }, 19 | "autoload": { 20 | "files": [ 21 | "src/functions.php" 22 | ], 23 | "psr-4": { 24 | "Jungi\\Common\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Jungi\\Common\\Tests\\": "tests/" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Piotr Kugla 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [2.0.0] - 2025-09-27 11 | 12 | ### Changed 13 | - Simplified `Result` and adjusted to suit more PHP. The main inspirations: c++23 `std::expected` and Rust's `Result` 14 | - Adjusted to PHP **8.4** 15 | - Updated PHPUnit to **v12** 16 | 17 | ### Removed 18 | - Class `Option` 19 | 20 | ## [1.2.0] - 2022-07-09 21 | 22 | ### Added 23 | - Function `array_equals()` that checks if both arrays have the same keys and their values are equal 24 | 25 | ## [1.1.1] - 2022-05-27 26 | 27 | ### Added 28 | - Template type declaration at `Equatable::equals()`. PHPStorm 2022.1 now handles generics types in @method tags, and it's also helpful when using an analysis tool. 29 | - Missing @param tag at `Option::some()` 30 | - Missing @param tags in local functions 31 | - Missing @template tags in static methods of `Result` 32 | - Missing @template tags in static methods of `Option` 33 | 34 | ## [1.1.0] - 2021-10-30 35 | 36 | ### Added 37 | - Function `iterable_search()` that returns the first key where the given value is equal. 38 | 39 | [unreleased]: https://github.com/jungi-php/common/compare/v2.0.0...HEAD 40 | [2.0.0]: https://github.com/jungi-php/common/compare/v1.2.0...v2.0.0 41 | [1.2.0]: https://github.com/jungi-php/common/compare/v1.1.1...v1.2.0 42 | [1.1.1]: https://github.com/jungi-php/common/compare/v1.1.0...v1.1.1 43 | [1.1.0]: https://github.com/jungi-php/common/compare/v1.0.0...v1.1.0 44 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | equals($b); 16 | } catch (\TypeError) { 17 | return false; 18 | } 19 | } 20 | 21 | return $a === $b; 22 | } 23 | 24 | /** 25 | * Returns true if a value is present in an iterable, 26 | * otherwise false. 27 | */ 28 | function in_iterable(mixed $value, iterable $iterable): bool 29 | { 30 | foreach ($iterable as $iteratedValue) { 31 | if (equals($value, $iteratedValue)) { 32 | return true; 33 | } 34 | } 35 | 36 | return false; 37 | } 38 | 39 | /** 40 | * Returns an iterable without duplicates. 41 | */ 42 | function iterable_unique(iterable $iterable): iterable 43 | { 44 | $seen = []; 45 | foreach ($iterable as $key => $value) { 46 | if (!in_iterable($value, $seen)) { 47 | $seen[] = $value; 48 | 49 | yield $key => $value; 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * Returns the first key where the given value is equal. 56 | * If the value is not found, false is returned. 57 | */ 58 | function iterable_search(mixed $value, iterable $iterable): mixed 59 | { 60 | foreach ($iterable as $key => $iteratedValue) { 61 | if (equals($value, $iteratedValue)) { 62 | return $key; 63 | } 64 | } 65 | 66 | return false; 67 | } 68 | 69 | /** 70 | * Returns true if both arrays have the same keys 71 | * and their values are equal. 72 | */ 73 | function array_equals(array $a, array $b): bool 74 | { 75 | if (count($a) !== count($b)) { 76 | return false; 77 | } 78 | 79 | foreach ($a as $key => $value) { 80 | if (!array_key_exists($key, $b) || !equals($value, $b[$key])) { 81 | return false; 82 | } 83 | } 84 | 85 | return true; 86 | } 87 | -------------------------------------------------------------------------------- /tests/ResultTest.php: -------------------------------------------------------------------------------- 1 | $r */ 16 | $r = Result::ok("foo"); 17 | 18 | $this->assertTrue($r->isOk()); 19 | $this->assertEquals("foo", $r->value); 20 | $this->assertEquals("foo", $r()); 21 | } 22 | 23 | public function testThatThrowsOnValue(): void 24 | { 25 | /** @var Result $r */ 26 | $r = Result::ok("foo"); 27 | 28 | $this->expectException(\LogicException::class); 29 | 30 | $r->error; 31 | } 32 | 33 | public function testThatErrorIsAvailable(): void 34 | { 35 | /** @var Result $r */ 36 | $r = Result::error(self::ERR_TEST); 37 | 38 | $this->assertFalse($r->isOk()); 39 | $this->assertEquals(self::ERR_TEST, $r->error); 40 | } 41 | 42 | public function testThatThrowsOnError(): void 43 | { 44 | /** @var Result $r */ 45 | $r = Result::error(self::ERR_TEST); 46 | 47 | $this->expectException(\LogicException::class); 48 | 49 | $r->value; 50 | } 51 | 52 | public function testThatResultEqualsAndDoesNotEqual(): void 53 | { 54 | $this->assertTrue(Result::ok("foo")->equals(Result::ok("foo"))); 55 | $this->assertTrue(Result::ok(new Foo("foo"))->equals(Result::ok(new Foo("foo")))); 56 | $this->assertTrue(Result::error(self::ERR_TEST)->equals(Result::error(self::ERR_TEST))); 57 | $this->assertTrue(Result::error(new Foo("foo"))->equals(Result::error(new Foo("foo")))); 58 | $this->assertFalse(Result::ok("foo")->equals(Result::error("foo"))); 59 | $this->assertFalse(Result::error("foo")->equals(Result::ok("foo"))); 60 | $this->assertFalse(Result::error("foo")->equals(Result::ok("foo"))); 61 | } 62 | 63 | public function testThatDefaultValueIsReturned(): void 64 | { 65 | /** @var Result $r */ 66 | $r = Result::error(self::ERR_TEST); 67 | 68 | $this->assertNull($r->valueOr(null)); 69 | } 70 | 71 | public function testThatDefaultValueIsNotReturned(): void 72 | { 73 | /** @var Result $r */ 74 | $r = Result::ok("foo"); 75 | 76 | $this->assertEquals("foo", $r->valueOr("default")); 77 | } 78 | 79 | public function testThatValueIsMapped(): void 80 | { 81 | /** @var Result $r */ 82 | $r = Result::ok("foo")->map(fn ($value) => 1.23); 83 | 84 | $this->assertTrue($r->isOk()); 85 | $this->assertEquals(1.23, $r->value); 86 | } 87 | 88 | public function testThatValueIsNotMapped(): void 89 | { 90 | /** @var Result $r */ 91 | $r = Result::error(self::ERR_TEST)->map(fn($value) => 1.23); 92 | 93 | $this->assertFalse($r->isOk()); 94 | $this->assertEquals(self::ERR_TEST, $r->error); 95 | } 96 | 97 | public function testThatErrorIsMapped(): void 98 | { 99 | /** @var Result $r */ 100 | $r = Result::error(self::ERR_TEST)->mapError(fn ($value) => "err"); 101 | 102 | $this->assertFalse($r->isOk()); 103 | $this->assertEquals("err", $r->error); 104 | } 105 | 106 | public function testThatErrorIsNotMapped(): void 107 | { 108 | /** @var Result $r */ 109 | $r = Result::ok("foo")->mapError(fn ($value) => "err"); 110 | 111 | $this->assertTrue($r->isOk()); 112 | $this->assertEquals("foo", $r->value); 113 | } 114 | } 115 | 116 | final class Foo implements Equatable 117 | { 118 | public function __construct(public readonly string $value) {} 119 | 120 | public function equals(self $other): bool 121 | { 122 | return $this->value === $other->value; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Result.php: -------------------------------------------------------------------------------- 1 | > 13 | * 14 | * @final 15 | */ 16 | abstract class Result implements Equatable 17 | { 18 | /** 19 | * @param T|null $value Null in case T of void 20 | * 21 | * @return Result 22 | */ 23 | public static function ok($value = null): self 24 | { 25 | return new Ok($value); 26 | } 27 | 28 | /** 29 | * @param E $value 30 | * 31 | * @return Result 32 | */ 33 | public static function error($value): self 34 | { 35 | return new Error($value); 36 | } 37 | 38 | /** 39 | * @var T 40 | * 41 | * @throws \LogicException If accessed when the result has error 42 | */ 43 | protected(set) mixed $value; 44 | 45 | /** 46 | * @var E 47 | * 48 | * @throws \LogicException If accessed when the result has value 49 | */ 50 | protected(set) mixed $error; 51 | 52 | /** 53 | * Dereferences the contained value. 54 | * 55 | * This is a shorthand for accessing the result's value directly `$this->value`. 56 | * 57 | * @example $r()->foo 58 | * 59 | * @return T The contained value 60 | * 61 | * @throws \LogicException If the result has error 62 | */ 63 | public function __invoke() 64 | { 65 | return $this->value; 66 | } 67 | 68 | abstract public function isOk(): bool; 69 | 70 | /** 71 | * Returns the contained value, or the given default if the result is an error. 72 | * 73 | * @template U 74 | * 75 | * @param U $defaultValue 76 | * 77 | * @return T|U 78 | */ 79 | abstract public function valueOr($defaultValue); 80 | 81 | /** 82 | * Returns a new result with the value mapped using the given callback. 83 | * If the result has error, it is returned unchanged. 84 | * 85 | * 86 | * function mul(int $value): int { return 3 * $value; } 87 | * 88 | * assert(6 === Result::ok(2)->map('mul')); 89 | * assert(2 === Result::error(2)->map('mul')); 90 | * 91 | * 92 | * @template U 93 | * 94 | * @param callable(T): U $fn 95 | * 96 | * @return Result 97 | */ 98 | abstract public function map(callable $fn): self; 99 | 100 | /** 101 | * Returns a new result with the error mapped using the given callback. 102 | * If the result has value, it is returned unchanged. 103 | * 104 | * 105 | * function mul(int $value): int { return 3 * $value; } 106 | * 107 | * assert(2 === Result::ok(2)->mapError('mul')); 108 | * assert(6 === Result::error(2)->mapError('mul')); 109 | * 110 | * 111 | * @template U 112 | * 113 | * @param callable(E): U $fn 114 | * 115 | * @return Result 116 | */ 117 | abstract public function mapError(callable $fn): self; 118 | } 119 | 120 | /** 121 | * @internal Part of the implementation details, do not use outside 122 | */ 123 | final class Ok extends Result 124 | { 125 | protected(set) mixed $value { 126 | get { 127 | return $this->value; 128 | } 129 | } 130 | 131 | protected(set) mixed $error { 132 | get => throw new \LogicException("Called on expected value."); 133 | } 134 | 135 | public function __construct($value) 136 | { 137 | $this->value = $value; 138 | } 139 | 140 | public function equals(Result $other): bool 141 | { 142 | return $other instanceof self && equals($this->value, $other->value); 143 | } 144 | 145 | public function isOk(): bool 146 | { 147 | return true; 148 | } 149 | 150 | public function valueOr($defaultValue) 151 | { 152 | return $this->value; 153 | } 154 | 155 | public function map(callable $fn): self 156 | { 157 | return new self($fn($this->value)); 158 | } 159 | 160 | public function mapError(callable $fn): self 161 | { 162 | return $this; 163 | } 164 | } 165 | 166 | /** 167 | * @internal Part of the implementation details, do not use outside 168 | */ 169 | final class Error extends Result 170 | { 171 | protected(set) mixed $value { 172 | get => throw new \LogicException("Called on error."); 173 | } 174 | 175 | protected(set) mixed $error { 176 | get { 177 | return $this->error; 178 | } 179 | } 180 | 181 | public function __construct($error) 182 | { 183 | $this->error = $error; 184 | } 185 | 186 | public function equals(Result $other): bool 187 | { 188 | return $other instanceof self && equals($this->error, $other->error); 189 | } 190 | 191 | public function isOk(): bool 192 | { 193 | return false; 194 | } 195 | 196 | public function valueOr($defaultValue) 197 | { 198 | return $defaultValue; 199 | } 200 | 201 | public function map(callable $fn): self 202 | { 203 | return $this; 204 | } 205 | 206 | public function mapError(callable $fn): self 207 | { 208 | return new self($fn($this->error)); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌱 jungi/common 2 | 3 | [![CI](https://github.com/jungi-php/common/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/jungi-php/common/actions/workflows/continuous-integration.yml) 4 | ![PHP](https://img.shields.io/packagist/php-v/jungi/common) 5 | 6 | A minimal library that defines primitive building blocks of PHP code. It defines basic types and useful functions. 7 | 8 | **Primitive types:** 9 | 10 | * [`Equatable`](https://piku235.gitbook.io/jungi-common/equatable) 11 | * [`Result`](https://piku235.gitbook.io/jungi-common/result) 12 | 13 | ## Installation 14 | 15 | ```text 16 | composer require jungi/common 17 | ``` 18 | 19 | ## Documentation 20 | 21 | [GitBook](https://piku235.gitbook.io/jungi-common) 22 | 23 | ## Quick insight 24 | 25 | ### Equatable 26 | 27 | ```php 28 | /** @implements Equatable */ 29 | class Phone implements Equatable 30 | { 31 | public function __construct(private string $value) {} 32 | 33 | public function equals(self $other): bool 34 | { 35 | return $this->value === $other->value; 36 | } 37 | } 38 | 39 | assert(true === (new Phone('(321) 456-1234'))->equals(new Phone('(321) 456-1234'))); 40 | assert(false === (new Phone('(321) 456-1234'))->equals(new Phone('(454) 456-1234'))); 41 | ``` 42 | 43 | ### Result 44 | 45 | ```php 46 | final class Student 47 | { 48 | public function __construct( 49 | public readonly StudentId $id, 50 | private(set) bool $active, 51 | private(set) string $name, 52 | ) {} 53 | } 54 | 55 | enum ClassEnrollmentError: string { 56 | case InactiveStudent = 'inactive_student'; 57 | case StudentAlreadyEnrolled = 'student_already_enrolled'; 58 | case NoSeatsAvailable = 'no_seats_available'; 59 | } 60 | 61 | final class Class_ 62 | { 63 | public function __construct( 64 | public readonly ClassId $id, 65 | private(set) int $numberOfSeats, 66 | /** @var StudentId[] */ 67 | private(set) array $students, 68 | ) {} 69 | 70 | /** @return Result */ 71 | public function enroll(Student $student): Result 72 | { 73 | if (!$student->active) { 74 | return Result::error(ClassEnrollmentError::InactiveStudent); 75 | } 76 | if (in_iterable($student->id(), $this->students)) { 77 | return Result::error(ClassEnrollmentError::StudentAlreadyEnrolled); 78 | } 79 | if (count($this->students) >= $this->numberOfSeats) { 80 | return Result::error(ClassEnrollmentError::NoSeatsAvailable); 81 | } 82 | 83 | $this->students[] = $student->id(); 84 | 85 | return Result::ok(); 86 | } 87 | } 88 | 89 | class ClassController 90 | { 91 | // PUT /classes/{classId}/students/{studentId} 92 | public function enrollToClass(string $classId, string $studentId) 93 | { 94 | // ... fetch the class and the student 95 | $r = $class->enroll($student); 96 | 97 | if ($r->isOk()) { 98 | return $this->created(); 99 | } 100 | 101 | return match ($r->error) { 102 | ClassEnrollmentError::StudentAlreadyEnrolled => $this->noContent(), 103 | default => $this->conflict($error), 104 | }; 105 | } 106 | } 107 | ``` 108 | 109 | ### Functions 110 | 111 | ```php 112 | use function Jungi\Common\equals; 113 | use function Jungi\Common\in_iterable; 114 | use function Jungi\Common\iterable_unique; 115 | use function Jungi\Common\array_equals; 116 | 117 | /** @implements Equatable */ 118 | class ContactInformation implements Equatable 119 | { 120 | public function __construct( 121 | private Phone $phone, 122 | private ?Phone $mobile = null 123 | ) {} 124 | 125 | public function equals(self $other): bool 126 | { 127 | return $this->phone->equals($other->phone) 128 | && equals($this->mobile, $other->mobile); 129 | } 130 | } 131 | 132 | // equals() 133 | 134 | $a = new ContactInformation(new Phone('(321) 456-1234'), new Phone('(886) 456-6543')); 135 | $b = new ContactInformation(new Phone('(321) 456-1234'), new Phone('(886) 456-6543')); 136 | assert(true === equals($a, $b); 137 | 138 | $a = new ContactInformation(new Phone('(321) 456-1234')); 139 | $b = new ContactInformation(new Phone('(321) 456-1234'), new Phone('(886) 456-6543')); 140 | assert(false === equals($a, $b); 141 | 142 | // array_equals() 143 | 144 | $a = [new Phone('(321) 456-1234'), new Phone('(465) 799-4566')]; 145 | $b = [new Phone('(321) 456-1234'), new Phone('(465) 799-4566')]; 146 | assert(true === array_equals($a, $b)); 147 | 148 | $a = [new Phone('(321) 456-1234'), new Phone('(465) 799-4566')]; 149 | $b = [new Phone('(321) 456-1234')]; 150 | assert(false === array_equals($a, $b)); 151 | 152 | // in_iterable() 153 | 154 | $iterable = [new Phone('(656) 456-7765'), new Phone('(321) 456-1234')]; 155 | assert(true === in_iterable(new Phone('(321) 456-1234'), $iterable)); 156 | assert(false === in_iterable(new Phone('(232) 456-1234'), $iterable)); 157 | 158 | // iterable_unique() 159 | 160 | $unique = iterable_unique([ 161 | new Phone('(321) 456-1234'), 162 | new Phone('(465) 799-4566'), 163 | new Phone('(321) 456-1234'), 164 | ]); 165 | $expected = [ 166 | new Phone('(321) 456-1234'), 167 | new Phone('(465) 799-4566'), 168 | ]; 169 | assert(true === array_equals($expected, $unique)); 170 | ``` 171 | -------------------------------------------------------------------------------- /tests/FunctionsTest.php: -------------------------------------------------------------------------------- 1 | 3], [1, 2, 2, 3, 2]]; 67 | yield [['foo'], ['foo', 'foo']]; 68 | yield [['foo', ''], ['foo', '']]; 69 | yield [[true, false], [true, false, true]]; 70 | yield [[1.23, 2.34], [1.23, 2.34, 1.23]]; 71 | yield [ 72 | [new SameEquatable(123), new SameEquatable(345), 3 => new SameEquatable(321)], 73 | [new SameEquatable(123), new SameEquatable(345), new SameEquatable(123), new SameEquatable(321)] 74 | ]; 75 | } 76 | 77 | public static function provideIterablesWithExistingKeys(): iterable 78 | { 79 | yield ['bar', 2, [ 80 | 'foo' => 1, 81 | 'bar' => 2, 82 | 'zoo' => 2 83 | ]]; 84 | yield ['bar', new SameEquatable(2), [ 85 | 'foo' => 1, 86 | 'bar' => new SameEquatable(2), 87 | 'zoo' => new SameEquatable(2) 88 | ]]; 89 | yield ['zoo', new VaryEquatable(2), [ 90 | 'foo' => 1, 91 | 'bar' => new SameEquatable(2), 92 | 'zoo' => 2 93 | ]]; 94 | } 95 | 96 | public static function provideIterablesWithNonExistingKeys(): iterable 97 | { 98 | yield [0, []]; 99 | yield [0, [ 100 | 'foo' => 1, 101 | 'bar' => 2, 102 | 'zoo' => 2 103 | ]]; 104 | yield [new SameEquatable(1), [ 105 | 'foo' => 1, 106 | 'bar' => new SameEquatable(2), 107 | 'zoo' => new SameEquatable(2) 108 | ]]; 109 | } 110 | 111 | public static function provideEqualArrays(): iterable 112 | { 113 | yield [[], []]; 114 | yield [[null], [null]]; 115 | yield [[1 => 'foo', 0 => 'bar'], ['bar', 'foo']]; 116 | yield [[1.23, 2.34], [1.23, 2.34]]; 117 | yield [[new SameEquatable(123), new SameEquatable(234)], [new SameEquatable(123), new SameEquatable(234)]]; 118 | yield [[new VaryEquatable(123)], [123]]; 119 | } 120 | 121 | public static function provideNotEqualArrays(): iterable 122 | { 123 | yield [[], [null]]; 124 | yield [[false], [null]]; 125 | yield [['foo'], []]; 126 | yield [['foo'], ['foo', 'bar']]; 127 | yield [['foo', 'bar'], ['foo']]; 128 | yield [['foo', 'bar'], ['bar', 'foo']]; 129 | yield [[new SameEquatable(123)], [new SameEquatable(234)]]; 130 | yield [[123], [new VaryEquatable(123)]]; 131 | } 132 | 133 | #[DataProvider('provideEqualVariables')] 134 | public function testThatTwoVariablesEqual($a, $b): void 135 | { 136 | $this->assertTrue(equals($a, $b)); 137 | } 138 | 139 | #[DataProvider('provideNotEqualVariables')] 140 | public function testThatTwoVariablesNotEqual($a, $b): void 141 | { 142 | $this->assertFalse(equals($a, $b)); 143 | } 144 | 145 | #[DataProvider('providePresentValuesInIterables')] 146 | public function testThatValueIsInIterable($value, $iterable): void 147 | { 148 | $this->assertTrue(in_iterable($value, $iterable)); 149 | } 150 | 151 | #[DataProvider('provideNotPresentValuesInIterables')] 152 | public function testThatValueIsNotInIterable($value, $iterable): void 153 | { 154 | $this->assertFalse(in_iterable($value, $iterable)); 155 | } 156 | 157 | #[DataProvider('provideIterablesWithUniqueAndDuplicatedValues')] 158 | public function testThatDuplicatedIterableValuesAreRemoved(array $expected, iterable $iterable): void 159 | { 160 | $uniqueIterable = $this->iterableToArray(iterable_unique($iterable)); 161 | 162 | $this->assertCount(count($expected), $uniqueIterable); 163 | $this->assertEmpty(array_udiff_assoc($expected, $uniqueIterable, function ($a, $b) { 164 | return equals($a, $b) ? 0 : ($a > $b ? 1 : -1); 165 | })); 166 | } 167 | 168 | #[DataProvider('provideIterablesWithExistingKeys')] 169 | public function testThatKeyIsReturnedFromIterable($expectedKey, $value, iterable $iterable): void 170 | { 171 | $this->assertSame($expectedKey, iterable_search($value, $iterable)); 172 | } 173 | 174 | #[DataProvider('provideIterablesWithNonExistingKeys')] 175 | public function testThatKeyIsNotReturnedFromIterable($value, iterable $iterable): void 176 | { 177 | $this->assertFalse(iterable_search($value, $iterable)); 178 | } 179 | 180 | #[DataProvider('provideEqualArrays')] 181 | public function testThatArraysEqual(array $a, array $b): void 182 | { 183 | $this->assertTrue(array_equals($a, $b)); 184 | } 185 | 186 | #[DataProvider('provideNotEqualArrays')] 187 | public function testThatArraysNotEqual(array $a, array $b): void 188 | { 189 | $this->assertFalse(array_equals($a, $b)); 190 | } 191 | 192 | private function iterableToArray(iterable $iterable): array 193 | { 194 | $arr = []; 195 | foreach ($iterable as $key => $value) { 196 | $arr[$key] = $value; 197 | } 198 | 199 | return $arr; 200 | } 201 | } 202 | 203 | /** @implements Equatable */ 204 | final class SameEquatable implements Equatable 205 | { 206 | private $value; 207 | 208 | public function __construct($value) 209 | { 210 | $this->value = $value; 211 | } 212 | 213 | public function equals(self $other): bool 214 | { 215 | return $this->value == $other->value; 216 | } 217 | } 218 | 219 | /** @implements Equatable */ 220 | final class VaryEquatable implements Equatable 221 | { 222 | private int $value; 223 | 224 | public function __construct(int $value) 225 | { 226 | $this->value = $value; 227 | } 228 | 229 | public function equals(int $other): bool 230 | { 231 | return $this->value == $other; 232 | } 233 | } 234 | --------------------------------------------------------------------------------