diff --git a/AGENTS.md b/AGENTS.md
index 2427e44..56f6e12 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -7,6 +7,7 @@ This file outlines the requirements and best practices for adding new assertion
- **Version Compliance**: Always use the most current version of AGENTS.md and do not rely on cached or outdated copies. Refresh and re-read AGENTS.md before every change to ensure compliance with the latest requirements.
- **Branching and Commits**: It is forbidden to commit directly to the `main` branch. All changes must be added via pull request from a feature branch. If the current branch is `main`, MUST checkout to a new branch before changing any files. Do not push changes automatically—only push after explicit user request.
- **Pull Requests**: When creating a pull request, update the PR description with a detailed summary of changes, including new methods added, files modified, and any breaking changes. Ensure the description follows the format: Summary, Changes, Testing, Validation.
+- **Dependency Management**: Remove unused dependencies from composer.json. If a tool (e.g., Psalm) is no longer used, remove its require-dev entry and update scripts accordingly.
- **Method Signature**: All new methods must be public, accept an optional `$message` parameter (string, default empty), and return `self` to enable fluent chaining.
- **Type Safety**: Specify strict types for parameters where applicable (e.g., `int|float` for numeric comparisons). Avoid `mixed` unless necessary.
- **PHPUnit Integration**: Use appropriate PHPUnit assertion methods (e.g., `Assert::assertLessThan`) without named parameters for compatibility.
@@ -49,7 +50,7 @@ This file outlines the requirements and best practices for adding new assertion
## Validation Steps
- **Run Tests**: Execute `./vendor/bin/phpunit tests/FluentAssertions/Asserts/MethodName/MethodNameTest.php` to verify implementation.
-- **Lint and Typecheck**: Run linting and type checking commands (e.g., via composer scripts or direct tools) to ensure code quality.
+- **Lint and Typecheck**: Run PHPStan static analysis via `composer run analyze` to ensure code quality.
- **Integration**: Ensure the method works in the overall fluent chain without breaking existing functionality.
## Example Workflow for Adding `isGreaterThan`
diff --git a/README.md b/README.md
index f579213..3936d13 100644
--- a/README.md
+++ b/README.md
@@ -65,8 +65,16 @@ fact([])->isEmptyArray(); // Passes
fact([1, 2])->isEmptyArray(); // Fails
fact([1, 2])->isNotEmptyArray(); // Passes
-fact([])->isNotEmptyArray(); // Fails
+fact([])->isNotEmptyArray(); // Fails
+fact([2, 4, 6])->every(fn($v) => $v % 2 === 0); // Passes
+fact([1, 2, 3])->every(fn($v) => $v > 5); // Fails
+
+fact([1, 2, 3])->some(fn($v) => $v > 2); // Passes
+fact([1, 2, 3])->some(fn($v) => $v > 10); // Fails
+
+fact([1, 2, 3])->none(fn($v) => $v > 10); // Passes
+fact([1, 2, 3])->none(fn($v) => $v > 2); // Fails
```
### Boolean assertions
@@ -84,8 +92,7 @@ fact(true)->notFalse(); // Passes
fact(false)->notFalse(); // Fails
```
-
-### Comparison and Equality assertions
+### Comparison and equality assertions
```php
fact(42)->is(42); // Passes
fact(42)->is('42'); // Fails due to type difference
@@ -97,7 +104,6 @@ fact(42)->not(43); // Passes
fact(42)->not(42); // Fails
```
-
### Null assertions
```php
fact(null)->null(); // Passes
@@ -114,7 +120,7 @@ fact(10)->isLowerThan(5); // Fails
fact(10)->isGreaterThan(5); // Passes
fact(5)->isGreaterThan(10); // Fails
-
+
fact(5)->isPositive(); // Passes
fact(-3)->isPositive(); // Fails
@@ -129,12 +135,6 @@ fact(5)->isBetween(1, 10); // Passes
fact(15)->isBetween(1, 10); // Fails
```
-### Special assertions
-```php
-fact('01ARZ3NDEKTSV4RRFFQ69G5FAV')->ulid(); // Passes (if valid ULID)
-fact('invalid-ulid')->ulid(); // Fails
-```
-
### String assertions
```php
fact('abc123')->matchesRegularExpression('/^[a-z]+\d+$/'); // Passes
@@ -175,6 +175,9 @@ fact('invalid json')->isJson(); // Fails
fact('user@example.com')->isValidEmail(); // Passes
fact('invalid-email')->isValidEmail(); // Fails
+
+fact('01ARZ3NDEKTSV4RRFFQ69G5FAV')->ulid(); // Passes (if valid ULID)
+fact('invalid-ulid')->ulid(); // Fails
```
### Type Checking assertions
@@ -205,6 +208,18 @@ fact(1)->isBool(); // Fails
fact([1, 2])->isArray(); // Passes
fact('not array')->isArray(); // Fails
+
+fact(fopen('php://memory', 'r'))->isResource(); // Passes
+fact('string')->isResource(); // Fails
+
+fact('strlen')->isCallable(); // Passes
+fact(123)->isCallable(); // Fails
+
+fact(3.14)->isFloat(); // Passes
+fact(42)->isFloat(); // Fails
+
+fact(true)->isBool(); // Passes
+fact(1)->isBool(); // Fails
```
## Pull requests are always welcome
diff --git a/composer.json b/composer.json
index 27380c5..257697e 100644
--- a/composer.json
+++ b/composer.json
@@ -34,15 +34,12 @@
},
"minimum-stability": "stable",
"scripts": {
- "analyze": [
- "@phpstan",
- "@psalm"
- ],
- "phpstan": "./vendor/bin/phpstan analyse",
- "psalm": "./vendor/bin/psalm.phar --config=psalm.xml"
+ "analyze": [
+ "@phpstan"
+ ],
+ "phpstan": "./vendor/bin/phpstan analyse"
},
- "require-dev": {
- "psalm/phar": "^6.14",
- "phpstan/phpstan": "^2.1"
- }
+ "require-dev": {
+ "phpstan/phpstan": "^2.1"
+ }
}
diff --git a/phpunit.xml b/phpunit.xml
index 9db7f9a..17f7cb5 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -4,6 +4,21 @@
tests
+
+ tests/FluentAssertions/Asserts
+
+
+ tests/FluentAssertions/Asserts
+
+
+ tests/FluentAssertions/Asserts
+
+
+ tests/FluentAssertions/Asserts
+
+
+ tests/FluentAssertions/Asserts
+
diff --git a/psalm.xml b/psalm.xml
deleted file mode 100644
index 2edfffe..0000000
--- a/psalm.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/FluentAssertions.php b/src/FluentAssertions.php
index 3ca73ea..23bfa3b 100644
--- a/src/FluentAssertions.php
+++ b/src/FluentAssertions.php
@@ -9,7 +9,6 @@
use K2gl\PHPUnitFluentAssertions\Traits\ComparisonAndEqualityAssertions;
use K2gl\PHPUnitFluentAssertions\Traits\NullAssertions;
use K2gl\PHPUnitFluentAssertions\Traits\NumericAssertions;
-use K2gl\PHPUnitFluentAssertions\Traits\SpecialAssertions;
use K2gl\PHPUnitFluentAssertions\Traits\StringAssertions;
use K2gl\PHPUnitFluentAssertions\Traits\TypeCheckingAssertions;
@@ -22,7 +21,6 @@ class FluentAssertions
use StringAssertions;
use ArrayAssertions;
use TypeCheckingAssertions;
- use SpecialAssertions;
public function __construct(
public readonly mixed $variable = null
diff --git a/src/Traits/ArrayAssertions.php b/src/Traits/ArrayAssertions.php
index 973cf2c..2407a1d 100644
--- a/src/Traits/ArrayAssertions.php
+++ b/src/Traits/ArrayAssertions.php
@@ -240,4 +240,118 @@ public function isNotEmptyArray(string $message = ''): self
return $this;
}
+
+ /**
+ * Asserts that every element in the array satisfies the given callback.
+ *
+ * This method checks if all elements pass the condition defined by the callback.
+ * The callback receives the value and optionally the key.
+ *
+ * Example usage:
+ * fact([2, 4, 6])->every(fn($v) => $v % 2 === 0); // Passes
+ * fact([1, 2, 3])->every(fn($v) => $v > 5); // Fails
+ *
+ * @param callable $callback The function to test each element (receives value and key).
+ * @param string $message Optional custom error message.
+ *
+ * @return self Enables fluent chaining of assertion methods.
+ */
+ public function every(callable $callback, string $message = ''): self
+ {
+ $array = $this->variable;
+
+ if (!is_array($array)) {
+ Assert::assertTrue(false, $message ?: 'Variable is not an array.');
+ }
+
+ if (empty($array)) {
+ Assert::assertTrue(false, $message ?: 'Array is empty, cannot evaluate condition on elements.');
+ }
+
+ foreach ($array as $key => $value) {
+ if (!$callback($value, $key)) {
+ Assert::assertTrue(false, $message ?: 'Not all elements satisfy the condition.');
+ }
+ }
+
+ Assert::assertTrue(true, $message);
+
+ return $this;
+ }
+
+ /**
+ * Asserts that at least one element in the array satisfies the given callback.
+ *
+ * This method checks if any element passes the condition defined by the callback.
+ * The callback receives the value and optionally the key.
+ *
+ * Example usage:
+ * fact([1, 2, 3])->some(fn($v) => $v > 2); // Passes
+ * fact([1, 2, 3])->some(fn($v) => $v > 10); // Fails
+ *
+ * @param callable $callback The function to test each element (receives value and key).
+ * @param string $message Optional custom error message.
+ *
+ * @return self Enables fluent chaining of assertion methods.
+ */
+ public function some(callable $callback, string $message = ''): self
+ {
+ $array = $this->variable;
+
+ if (!is_array($array)) {
+ Assert::assertTrue(false, $message ?: 'Variable is not an array.');
+ }
+
+ if (empty($array)) {
+ Assert::assertTrue(false, $message ?: 'Array is empty, cannot evaluate condition on elements.');
+ }
+
+ foreach ($array as $key => $value) {
+ if ($callback($value, $key)) {
+ Assert::assertTrue(true, $message);
+
+ return $this;
+ }
+ }
+
+ Assert::assertTrue(false, $message ?: 'No elements satisfy the condition.');
+ }
+
+ /**
+ * Asserts that no elements in the array satisfy the given callback.
+ *
+ * This method checks if none of the elements pass the condition defined by the callback.
+ * The callback receives the value and optionally the key.
+ *
+ * Example usage:
+ * fact([1, 2, 3])->none(fn($v) => $v > 10); // Passes
+ * fact([1, 2, 3])->none(fn($v) => $v > 2); // Fails
+ *
+ * @param callable $callback The function to test each element (receives value and key).
+ * @param string $message Optional custom error message.
+ *
+ * @return self Enables fluent chaining of assertion methods.
+ */
+ public function none(callable $callback, string $message = ''): self
+ {
+ $array = $this->variable;
+
+ if (!is_array($array)) {
+ Assert::assertTrue(false, $message ?: 'Variable is not an array.');
+ }
+
+ if (empty($array)) {
+ Assert::assertTrue(false, $message ?: 'Array is empty, cannot evaluate condition on elements.');
+ }
+
+ foreach ($array as $key => $value) {
+ if ($callback($value, $key)) {
+ Assert::assertTrue(false, $message ?: 'At least one element satisfies the condition.');
+ }
+ }
+
+ Assert::assertTrue(true, $message);
+
+ return $this;
+ }
}
\ No newline at end of file
diff --git a/src/Traits/SpecialAssertions.php b/src/Traits/SpecialAssertions.php
deleted file mode 100644
index dc33542..0000000
--- a/src/Traits/SpecialAssertions.php
+++ /dev/null
@@ -1,32 +0,0 @@
-ulid(); // Passes (if valid ULID)
- * fact('invalid-ulid')->ulid(); // Fails
- *
- * @return self Enables fluent chaining of assertion methods.
- */
- public function ulid(): self
- {
- return $this->matchesRegularExpression(RegularExpressionPattern::ULID);
- }
-}
\ No newline at end of file
diff --git a/src/Traits/StringAssertions.php b/src/Traits/StringAssertions.php
index 7e41f73..00e078c 100644
--- a/src/Traits/StringAssertions.php
+++ b/src/Traits/StringAssertions.php
@@ -5,6 +5,7 @@
namespace K2gl\PHPUnitFluentAssertions\Traits;
use K2gl\PHPUnitFluentAssertions\FluentAssertions;
+use K2gl\PHPUnitFluentAssertions\Reference\RegularExpressionPattern;
use PHPUnit\Framework\Assert;
/**
@@ -333,5 +334,25 @@ public function isValidEmail(string $message = ''): self
return $this;
}
+
+
+ /**
+ * Asserts that a variable is a valid ULID.
+ *
+ * This method checks if the actual value matches the ULID (Universally Unique Lexicographically Sortable Identifier) format.
+ *
+ * @see https://github.com/ulid/spec
+ *
+ * Example usage:
+ * fact('01ARZ3NDEKTSV4RRFFQ69G5FAV')->ulid(); // Passes (if valid ULID)
+ * fact('invalid-ulid')->ulid(); // Fails
+ *
+ * @return self Enables fluent chaining of assertion methods.
+ */
+ public function ulid(): self
+ {
+ return $this->matchesRegularExpression(RegularExpressionPattern::ULID);
+ }
+
// endregion Length Methods
}
\ No newline at end of file
diff --git a/src/Traits/TypeCheckingAssertions.php b/src/Traits/TypeCheckingAssertions.php
index 1a01ae5..e07c866 100644
--- a/src/Traits/TypeCheckingAssertions.php
+++ b/src/Traits/TypeCheckingAssertions.php
@@ -195,4 +195,44 @@ public function isArray(string $message = ''): self
return $this;
}
+
+ /**
+ * Asserts that a variable is of type resource.
+ *
+ * This method checks if the actual value is a resource.
+ *
+ * Example usage:
+ * fact(fopen('file.txt', 'r'))->isResource(); // Passes
+ * fact('string')->isResource(); // Fails
+ *
+ * @param string $message Optional custom error message.
+ *
+ * @return self Enables fluent chaining of assertion methods.
+ */
+ public function isResource(string $message = ''): self
+ {
+ Assert::assertIsResource($this->variable, $message);
+
+ return $this;
+ }
+
+ /**
+ * Asserts that a variable is callable.
+ *
+ * This method checks if the actual value is callable (function, method, closure).
+ *
+ * Example usage:
+ * fact('strlen')->isCallable(); // Passes
+ * fact(123)->isCallable(); // Fails
+ *
+ * @param string $message Optional custom error message.
+ *
+ * @return self Enables fluent chaining of assertion methods.
+ */
+ public function isCallable(string $message = ''): self
+ {
+ Assert::assertIsCallable($this->variable, $message);
+
+ return $this;
+ }
}
\ No newline at end of file
diff --git a/tests/FluentAssertions/Asserts/Every/EveryTest.php b/tests/FluentAssertions/Asserts/Every/EveryTest.php
new file mode 100644
index 0000000..ac42781
--- /dev/null
+++ b/tests/FluentAssertions/Asserts/Every/EveryTest.php
@@ -0,0 +1,51 @@
+every($callback);
+
+ // assert
+ $this->correctAssertionExecuted();
+ }
+
+ #[DataProvider('notEveryDataProvider')]
+ public function testNotEvery(mixed $variable, callable $callback): void
+ {
+ // assert
+ $this->incorrectAssertionExpected();
+
+ // act
+ fact($variable)->every($callback);
+ }
+
+ public static function everyDataProvider(): array
+ {
+ return [
+ [[2, 4, 6], fn($v) => $v % 2 === 0],
+ [['a' => 1, 'b' => 3], fn($v, $k) => $v > 0],
+ ];
+ }
+
+ public static function notEveryDataProvider(): array
+ {
+ return [
+ [[1, 2, 3], fn($v) => $v > 5],
+ [[], fn($v) => true], // empty array
+ ['not array', fn($v) => true], // not array
+ ];
+ }
+}
\ No newline at end of file
diff --git a/tests/FluentAssertions/Asserts/IsCallable/IsCallableTest.php b/tests/FluentAssertions/Asserts/IsCallable/IsCallableTest.php
new file mode 100644
index 0000000..a820513
--- /dev/null
+++ b/tests/FluentAssertions/Asserts/IsCallable/IsCallableTest.php
@@ -0,0 +1,53 @@
+isCallable();
+
+ // assert
+ $this->correctAssertionExecuted();
+ }
+
+ #[DataProvider('notIsCallableDataProvider')]
+ public function testNotIsCallable(mixed $variable): void
+ {
+ // assert
+ $this->incorrectAssertionExpected();
+
+ // act
+ fact($variable)->isCallable();
+ }
+
+ public static function isCallableDataProvider(): array
+ {
+ return [
+ ['strlen'],
+ [fn() => true],
+ ];
+ }
+
+ public static function notIsCallableDataProvider(): array
+ {
+ return [
+ ['string'],
+ [42],
+ [true],
+ [null],
+ [[]],
+ ];
+ }
+}
\ No newline at end of file
diff --git a/tests/FluentAssertions/Asserts/IsResource/IsResourceTest.php b/tests/FluentAssertions/Asserts/IsResource/IsResourceTest.php
new file mode 100644
index 0000000..4724c75
--- /dev/null
+++ b/tests/FluentAssertions/Asserts/IsResource/IsResourceTest.php
@@ -0,0 +1,48 @@
+isResource();
+
+ // assert
+ $this->correctAssertionExecuted();
+
+ fclose($resource);
+ }
+
+ #[DataProvider('notIsResourceDataProvider')]
+ public function testNotIsResource(mixed $variable): void
+ {
+ // assert
+ $this->incorrectAssertionExpected();
+
+ // act
+ fact($variable)->isResource();
+ }
+
+ public static function notIsResourceDataProvider(): array
+ {
+ return [
+ ['string'],
+ [42],
+ [true],
+ [null],
+ [[]],
+ ];
+ }
+}
\ No newline at end of file
diff --git a/tests/FluentAssertions/Asserts/None/NoneTest.php b/tests/FluentAssertions/Asserts/None/NoneTest.php
new file mode 100644
index 0000000..2f73e4b
--- /dev/null
+++ b/tests/FluentAssertions/Asserts/None/NoneTest.php
@@ -0,0 +1,51 @@
+none($callback);
+
+ // assert
+ $this->correctAssertionExecuted();
+ }
+
+ #[DataProvider('notNoneDataProvider')]
+ public function testNotNone(mixed $variable, callable $callback): void
+ {
+ // assert
+ $this->incorrectAssertionExpected();
+
+ // act
+ fact($variable)->none($callback);
+ }
+
+ public static function noneDataProvider(): array
+ {
+ return [
+ [[1, 2, 3], fn($v) => $v > 10],
+ [['a' => 1, 'b' => 2], fn($v, $k) => $k === 'c'],
+ ];
+ }
+
+ public static function notNoneDataProvider(): array
+ {
+ return [
+ [[1, 2, 3], fn($v) => $v > 2],
+ [[], fn($v) => true], // empty array
+ ['not array', fn($v) => true], // not array
+ ];
+ }
+}
\ No newline at end of file
diff --git a/tests/FluentAssertions/Asserts/Some/SomeTest.php b/tests/FluentAssertions/Asserts/Some/SomeTest.php
new file mode 100644
index 0000000..fec4385
--- /dev/null
+++ b/tests/FluentAssertions/Asserts/Some/SomeTest.php
@@ -0,0 +1,51 @@
+some($callback);
+
+ // assert
+ $this->correctAssertionExecuted();
+ }
+
+ #[DataProvider('notSomeDataProvider')]
+ public function testNotSome(mixed $variable, callable $callback): void
+ {
+ // assert
+ $this->incorrectAssertionExpected();
+
+ // act
+ fact($variable)->some($callback);
+ }
+
+ public static function someDataProvider(): array
+ {
+ return [
+ [[1, 2, 3], fn($v) => $v > 2],
+ [['a' => 1, 'b' => 2], fn($v, $k) => $k === 'b'],
+ ];
+ }
+
+ public static function notSomeDataProvider(): array
+ {
+ return [
+ [[1, 2, 3], fn($v) => $v > 10],
+ [[], fn($v) => true], // empty array
+ ['not array', fn($v) => true], // not array
+ ];
+ }
+}
\ No newline at end of file