From b07d03bfe551ef6f4cb00405246c3e7970521fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 17:24:38 +0200 Subject: [PATCH 01/16] feat: enhance rule engine documentation with detailed PHPDoc comments and descriptions --- src/RuleEngineService.php | 33 +++++++++++++++++ src/Rules/Rule.php | 66 ++++++++++++++++++++++++++++------ src/Rules/RuleInterface.php | 30 ++++++++++++---- src/Rules/RuleSet.php | 41 ++++++++++++++++++--- src/Rules/RuleSetInterface.php | 28 +++++++++++---- 5 files changed, 169 insertions(+), 29 deletions(-) diff --git a/src/RuleEngineService.php b/src/RuleEngineService.php index 62d99f8..0f19d4e 100644 --- a/src/RuleEngineService.php +++ b/src/RuleEngineService.php @@ -8,20 +8,46 @@ use Maniaba\RuleEngine\Builders\BuilderInterface; use Maniaba\RuleEngine\Rules\RuleSet; +/** + * Main service class for the Rule Engine. + * + * This class serves as the primary entry point for the rule engine system. + * It manages a builder that constructs rule sets from configuration data. + */ final class RuleEngineService { + /** + * The builder used to construct rule sets. + */ private BuilderInterface $builder; + /** + * Initializes a new instance of the RuleEngineService. + * + * By default, it uses an ArrayBuilder for constructing rule sets. + */ public function __construct() { $this->builder = new ArrayBuilder(); } + /** + * Gets the current builder instance. + * + * @return BuilderInterface The current builder used by this service. + */ public function getBuilder(): BuilderInterface { return $this->builder; } + /** + * Sets a custom builder for this service. + * + * @param BuilderInterface $builder The builder to use for constructing rule sets. + * + * @return self Returns the service instance for method chaining. + */ public function setBuilder(BuilderInterface $builder): self { $this->builder = $builder; @@ -29,6 +55,13 @@ public function setBuilder(BuilderInterface $builder): self return $this; } + /** + * Builds a rule set from the provided rules configuration. + * + * @param mixed $rules The rules configuration data to build from. + * + * @return RuleSet The constructed rule set containing the rules defined in the configuration. + */ public function build(mixed $rules): RuleSet { return $this->builder->build($rules); diff --git a/src/Rules/Rule.php b/src/Rules/Rule.php index edfcb65..8cba53f 100644 --- a/src/Rules/Rule.php +++ b/src/Rules/Rule.php @@ -10,30 +10,51 @@ use Maniaba\RuleEngine\Context\ContextInterface; /** - * Implementacija pravila. + * Implementation of a rule in the rule engine. * - * @see RuleTest + * A rule consists of a condition and two sets of actions: + * - actions to execute when the condition is satisfied + * - elseActions to execute when the condition is not satisfied * - * @property list $actions - * @property list $elseActions + * @see RuleTest */ final class Rule implements RuleInterface { - private array $executionErrors = []; + /** + * Collection of errors that occurred during action execution. + * + * @var list + */ + private array $executionErrors = []; + + /** + * Cached result of the condition evaluation. + */ private ?bool $evaluationResult = null; + /** + * Creates a new Rule instance. + * + * @param ConditionInterface $condition The condition to evaluate + * @param list $actions Actions to execute when condition is satisfied + * @param list $elseActions Actions to execute when condition is not satisfied + * @param int $priority The rule priority (higher number = higher priority) + */ public function __construct( private readonly ConditionInterface $condition, - /** @var list $actions */ private readonly array $actions = [], - /** @var list $elseActions */ private readonly array $elseActions = [], private int $priority = 0, ) { } /** - * Izvršava akcije na temelju rezultata evaluacije. + * Executes actions based on the evaluation result. + * + * If the condition is satisfied, executes the primary actions. + * Otherwise, executes the else actions. + * + * @param ContextInterface $context The context data to use for execution */ public function execute(ContextInterface $context): void { @@ -55,7 +76,9 @@ public function execute(ContextInterface $context): void } /** - * @return list|string|null + * Gets the failure message from the condition when evaluation fails. + * + * @return list|string|null The failure message(s) or null if no failure occurred */ public function getFailureMessage(): array|string|null { @@ -63,30 +86,51 @@ public function getFailureMessage(): array|string|null } /** - * Evaluira pravilo na temelju konteksta. + * Evaluates the rule based on the provided context. + * + * @param ContextInterface $context The context data to evaluate against + * + * @return bool True if the rule's condition is satisfied, false otherwise */ public function evaluate(ContextInterface $context): bool { return $this->evaluationResult ??= $this->condition->isSatisfied($context); } + /** + * Gets the priority of the rule. + * + * @return int The rule priority (higher number = higher priority) + */ public function getPriority(): int { return $this->priority; } + /** + * Sets the priority of the rule. + * + * @param int $priority The new priority value + */ public function setPriority(int $priority): void { $this->priority = $priority; } + /** + * Gets any errors that occurred during rule execution. + * + * @return list|null The execution errors or null if no errors occurred + */ public function getExecutionErrors(): ?array { return [] !== $this->executionErrors ? $this->executionErrors : null; } /** - * Prikuplja greške iz akcije. + * Collects error messages from an action that failed to execute. + * + * @param ActionInterface $action The action that failed */ private function collectActionErrors(ActionInterface $action): void { diff --git a/src/Rules/RuleInterface.php b/src/Rules/RuleInterface.php index cf4cc65..43e5676 100644 --- a/src/Rules/RuleInterface.php +++ b/src/Rules/RuleInterface.php @@ -6,32 +6,48 @@ use Maniaba\RuleEngine\Context\ContextInterface; +/** + * Interface for rule objects in the rule engine. + * + * Rules are the core components that define conditions to evaluate and actions to execute + * based on the evaluation results. + */ interface RuleInterface { /** - * Evaluira pravilo na temelju konteksta. + * Evaluates the rule based on the provided context. * - * @param ContextInterface $context kontekst podataka + * @param ContextInterface $context The context data to evaluate against * - * @return bool true ako je pravilo zadovoljeno, false inače + * @return bool True if the rule is satisfied, false otherwise */ public function evaluate(ContextInterface $context): bool; /** - * Izvršava akcije na temelju rezultata evaluacije. + * Executes actions based on the evaluation result. * - * @param ContextInterface $context kontekst podataka + * @param ContextInterface $context The context data to use for execution */ public function execute(ContextInterface $context): void; /** - * Dohvata prioritet pravila. + * Gets the priority of the rule. * - * @return int prioritet pravila (veći broj = veći prioritet) + * @return int The rule priority (higher number = higher priority) */ public function getPriority(): int; + /** + * Gets the failure message when the rule evaluation fails. + * + * @return array|string|null The failure message(s) or null if no failure occurred + */ public function getFailureMessage(): array|string|null; + /** + * Gets any errors that occurred during rule execution. + * + * @return array|string|null The execution error(s) or null if no errors occurred + */ public function getExecutionErrors(): array|string|null; } diff --git a/src/Rules/RuleSet.php b/src/Rules/RuleSet.php index 606dc68..d2f0cf9 100644 --- a/src/Rules/RuleSet.php +++ b/src/Rules/RuleSet.php @@ -7,19 +7,32 @@ use Maniaba\RuleEngine\Context\ContextInterface; use Maniaba\RuleEngine\Evaluators\Results\EvaluationResult; +/** + * A collection of rules that can be evaluated and executed as a group. + * + * RuleSet manages a list of rules and tracks which ones have failed during + * evaluation or execution. + */ final class RuleSet implements RuleSetInterface { /** - * Lista pravila u RuleSet-u. + * List of rules in the RuleSet. * * @var list */ private array $rules = []; + /** + * List of rules that failed during evaluation or execution. + * + * @var list + */ private array $failedRules = []; /** - * Dodaje pravilo u RuleSet. + * Adds a rule to the RuleSet. + * + * @param RuleInterface $rule The rule to add to the set */ public function addRule(RuleInterface $rule): void { @@ -27,9 +40,13 @@ public function addRule(RuleInterface $rule): void } /** - * Evaluira sva pravila u RuleSet-u. + * Evaluates all rules in the RuleSet against the provided context. + * + * Rules that fail evaluation are added to the failedRules list. * - * @return list lista rezultata evaluacije pravila + * @param ContextInterface $context The context to evaluate rules against + * + * @return list List of evaluation results for each rule */ public function evaluate(ContextInterface $context): array { @@ -48,7 +65,11 @@ public function evaluate(ContextInterface $context): array } /** - * Izvršava sva pravila u RuleSet-u. + * Executes all rules in the RuleSet against the provided context. + * + * Rules that encounter execution errors are added to the failedRules list. + * + * @param ContextInterface $context The context to execute rules against */ public function execute(ContextInterface $context): void { @@ -61,11 +82,21 @@ public function execute(ContextInterface $context): void } } + /** + * Gets all rules in this RuleSet. + * + * @return list List of all rules + */ public function getRules(): array { return $this->rules; } + /** + * Gets all rules that failed during evaluation or execution. + * + * @return list List of failed rules + */ public function getFailedRules(): array { return $this->failedRules; diff --git a/src/Rules/RuleSetInterface.php b/src/Rules/RuleSetInterface.php index 4b9c428..6b7306e 100644 --- a/src/Rules/RuleSetInterface.php +++ b/src/Rules/RuleSetInterface.php @@ -6,31 +6,47 @@ use Maniaba\RuleEngine\Context\ContextInterface; +/** + * Interface for rule set objects in the rule engine. + * + * A rule set is a collection of rules that can be evaluated and executed as a group. + */ interface RuleSetInterface { /** - * Dodaje pravilo u RuleSet. + * Adds a rule to the rule set. + * + * @param RuleInterface $rule The rule to add to the set */ public function addRule(RuleInterface $rule): void; /** - * Evaluira sva pravila u RuleSet-u. + * Evaluates all rules in the rule set against the provided context. + * + * @param ContextInterface $context The context to evaluate rules against * - * @return array lista rezultata evaluacije pravila + * @return array List of evaluation results for each rule */ public function evaluate(ContextInterface $context): array; /** - * Izvršava sva pravila u RuleSet-u. + * Executes all rules in the rule set against the provided context. + * + * @param ContextInterface $context The context to execute rules against */ public function execute(ContextInterface $context): void; /** - * Dohvaća pravila u RuleSet-u. + * Gets all rules in this rule set. * - * @return list + * @return list List of all rules */ public function getRules(): array; + /** + * Gets all rules that failed during evaluation or execution. + * + * @return list List of failed rules + */ public function getFailedRules(): array; } From 78a87af76678034542312368865abdaeafebbf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 17:35:07 +0200 Subject: [PATCH 02/16] feat: enhance documentation with detailed PHPDoc comments for action and condition factories --- composer-unused.php | 7 +-- phpstan.neon.dist | 7 --- phpunit.xml.dist | 1 - psalm_autoload.php | 20 --------- src/Builders/ArrayBuilder.php | 52 +++++++++++++++++++-- src/Factories/ActionFactory.php | 72 +++++++++++++++++++++++++++--- src/Factories/ConditionFactory.php | 18 ++++++-- tests/Builders/JsonBuilderTest.php | 4 +- 8 files changed, 134 insertions(+), 47 deletions(-) diff --git a/composer-unused.php b/composer-unused.php index 6ba235a..692655c 100644 --- a/composer-unused.php +++ b/composer-unused.php @@ -7,9 +7,6 @@ use ComposerUnused\ComposerUnused\Configuration\PatternFilter; use Webmozart\Glob\Glob; -return static fn (Configuration $config): Configuration => $config +return static fn (Configuration $config): Configuration => $config; // ->addNamedFilter(NamedFilter::fromString('symfony/config')) - // ->addPatternFilter(PatternFilter::fromString('/symfony-.*/')) - ->setAdditionalFilesFor('codeigniter4/framework', [ - ...Glob::glob(__DIR__ . '/vendor/codeigniter4/framework/system/Helpers/*.php'), - ]); + // ->addPatternFilter(PatternFilter::fromString('/symfony-.*/')); diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 3707a60..fcc4db8 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -5,17 +5,10 @@ parameters: - src/ - tests/ bootstrapFiles: - - vendor/codeigniter4/framework/system/Test/bootstrap.php excludePaths: universalObjectCratesClasses: - - CodeIgniter\Entity - - CodeIgniter\Entity\Entity - - Faker\Generator scanDirectories: - - vendor/codeigniter4/framework/system/Helpers dynamicConstantNames: - - APP_NAMESPACE - - CI_DEBUG - ENVIRONMENT strictRules: allRules: false diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6ae4e0d..59835b3 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,6 @@ conditions)) { @@ -58,6 +91,13 @@ public function conditions(): ConditionFactory return $this->conditions; } + /** + * Gets the action factory used by this builder. + * + * Creates a new factory instance if one doesn't exist yet. + * + * @return ActionFactory The action factory + */ public function actions(): ActionFactory { if (! isset($this->actions)) { @@ -68,9 +108,13 @@ public function actions(): ActionFactory } /** - * Kreira Rule na osnovu konfiguracije. + * Creates a Rule from the provided configuration. + * + * @param array $config The rule configuration array + * + * @return Rule The created rule * - * @param array $config konfiguracija za pravilo + * @throws BuilderException If the configuration is invalid */ private function buildRule(array $config): Rule { diff --git a/src/Factories/ActionFactory.php b/src/Factories/ActionFactory.php index fb4fe8c..f479b87 100644 --- a/src/Factories/ActionFactory.php +++ b/src/Factories/ActionFactory.php @@ -12,11 +12,20 @@ use ReflectionClass; use ReflectionException; use ReflectionFunction; +use ReflectionParameter; use ReflectionUnionType; use Tests\Factories\ActionFactoryTest; use Throwable; /** + * Factory for creating action objects based on configuration. + * + * This factory manages a registry of actions that can be created from configuration arrays. + * It supports creating actions from: + * - Action class names + * - Action instances + * - Callable functions + * * @see ActionFactoryTest */ final class ActionFactory @@ -112,11 +121,20 @@ public function registerAction(string $name, ActionInterface|Closure|string $act $this->actions[$name] = $action; } + /** + * Creates a CallableAction from a callable function. + * + * @param callable $action The callable to wrap in a CallableAction + * @param string $actionName The name of the action (used for error messages) + * @param array $arguments The arguments to pass to the callable + * + * @return CallableAction The created action + */ private function makeCallable(callable $action, string $actionName, array $arguments): CallableAction { $reflection = new ReflectionFunction($action); - // remove 'context' name from getParameters + // Remove 'context' parameter as it will be provided during execution $parameters = array_filter($reflection->getParameters(), static fn ($parameter): bool => $parameter->getName() !== 'context'); $arguments = self::testParameters($parameters, $arguments, $actionName); @@ -124,6 +142,23 @@ private function makeCallable(callable $action, string $actionName, array $argum return new CallableAction($action, ...$arguments); } + /** + * Validates and prepares arguments for a callable or class constructor. + * + * This method: + * 1. Filters arguments to only include those that match parameter names + * 2. Validates argument types against parameter types + * 3. Applies default values for optional parameters + * 4. Checks that all required parameters have values + * + * @param list $parameters Reflection parameters to validate against + * @param array $arguments Arguments provided for the callable or constructor + * @param string $actionName Name of the action (for error messages) + * + * @return array Validated and prepared arguments + * + * @throws InvalidArgumentException If arguments are missing or of invalid types + */ private static function testParameters(array $parameters, array $arguments, string $actionName): array { $arguments = array_intersect_key($arguments, array_flip(array_map(static fn ($p) => $p->getName(), $parameters))); @@ -154,7 +189,7 @@ private static function testParameters(array $parameters, array $arguments, stri $parameterTypeName = $parameterType?->getName(); $argumentType = self::gettype($arguments[$name]); - // Provjerite da li je tip nullable ili odgovara tipu argumenta + // Check if the type is nullable or matches the argument type if (! \in_array($parameterTypeName, [null, 'mixed'], true) && (! ($parameterType?->allowsNull() && null === $arguments[$name]) && $parameterTypeName !== $argumentType)) { throw new InvalidArgumentException("Invalid type for argument: '{$name}'. Expected: {$parameterTypeName}, got: " . $argumentType); } @@ -175,15 +210,25 @@ private static function testParameters(array $parameters, array $arguments, stri return $arguments; } + /** + * Gets the normalized type name of a value. + * + * This method converts PHP's gettype() output to match the type names used in reflection. + * For example, 'integer' becomes 'int', 'boolean' becomes 'bool', etc. + * + * @param mixed $value The value to get the type of + * + * @return string The normalized type name + */ private static function gettype(mixed $value): string { $result = \gettype($value); - // Mapirajte gettype izlaz u odgovarajuće nazive refleksije + // Map gettype output to corresponding reflection type names $typeMap = [ 'integer' => 'int', 'boolean' => 'bool', - 'double' => 'float', // gettype() vraća 'double' za float + 'double' => 'float', // gettype() returns 'double' for float ]; if (\array_key_exists($result, $typeMap)) { @@ -193,9 +238,26 @@ private static function gettype(mixed $value): string return $result; } + /** + * Creates an action instance from a class name. + * + * This method uses reflection to: + * 1. Check the constructor parameters + * 2. Validate the provided arguments + * 3. Instantiate the action class with the correct arguments + * + * @param string $action The fully qualified class name of the action + * @param string $actionName The name of the action (for error messages) + * @param array $arguments The arguments to pass to the constructor + * + * @return ActionInterface The created action instance + * + * @throws InvalidArgumentException If the action creation fails + * @throws ReflectionException If reflection fails + */ private function makeActionClass(string $action, string $actionName, array $arguments): ActionInterface { - // reflection get constructor parameters and check if all required parameters are passed and correct type + // Use reflection to get constructor parameters and validate arguments $reflection = new ReflectionClass($action); $constructor = $reflection->getConstructor(); diff --git a/src/Factories/ConditionFactory.php b/src/Factories/ConditionFactory.php index 4f1057d..3907c86 100644 --- a/src/Factories/ConditionFactory.php +++ b/src/Factories/ConditionFactory.php @@ -19,6 +19,13 @@ use Maniaba\RuleEngine\Conditions\StartsWithCondition; use Maniaba\RuleEngine\Conditions\StringContainCondition; +/** + * Factory for creating condition objects based on configuration. + * + * This factory manages a registry of condition types that can be created + * from configuration arrays. It supports both built-in conditions and + * custom registered conditions. + */ final class ConditionFactory { /** @@ -39,6 +46,9 @@ final class ConditionFactory 'numericInRange' => NumericInRangeCondition::class, ]; + /** + * @var array> + */ private array $customConditions = []; /** @@ -99,10 +109,12 @@ public function registerConditions(array $conditions): void } /** - * Registruje uslov za datog operatora sa odgovarajućom klasom uslova. + * Registers a condition for the given operator with the corresponding condition class. + * + * @param string $operator the operator to register + * @param class-string $condition the class name that implements the condition * - * @param string $operator operator koji se registruje - * @param class-string $condition naziv klase koja implementira uslov + * @throws InvalidArgumentException if the condition class doesn't exist, doesn't implement ConditionInterface, or the operator is already registered */ public function registerCondition(string $operator, string $condition): void { diff --git a/tests/Builders/JsonBuilderTest.php b/tests/Builders/JsonBuilderTest.php index a383fc6..fe311aa 100644 --- a/tests/Builders/JsonBuilderTest.php +++ b/tests/Builders/JsonBuilderTest.php @@ -63,8 +63,8 @@ protected function getBuilder(): RuleSet private static function createTempJsonFile(): File { $tempFile = tempnam(sys_get_temp_dir(), 'json'); - helper('filesystem'); - write_file($tempFile, json_encode([ + + file_put_contents($tempFile, json_encode([ 'node' => 'action', 'actionName' => 'actionName1', 'arguments' => [ From 2e215a3c0b424aab8db5cc3c4eb0c28a53a25877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 17:44:57 +0200 Subject: [PATCH 03/16] feat: enhance documentation with PHPDoc comments for ArrayBuilder and related classes --- .github/workflows/deptrac.yml | 71 -------------- .github/workflows/infection.yml | 2 +- .github/workflows/phpcpd.yml | 2 +- .github/workflows/phpcsfixer.yml | 2 +- .github/workflows/phpstan.yml | 2 +- .github/workflows/phpunit.yml | 4 +- .github/workflows/psalm.yml | 2 +- .github/workflows/rector.yml | 2 +- .github/workflows/unused.yml | 2 +- composer.json | 3 +- deptrac.yaml | 156 ------------------------------- infection.json.dist | 15 +++ src/Builders/ArrayBuilder.php | 36 +++++-- src/Builders/JsonBuilder.php | 6 +- 14 files changed, 58 insertions(+), 247 deletions(-) delete mode 100644 .github/workflows/deptrac.yml delete mode 100644 deptrac.yaml diff --git a/.github/workflows/deptrac.yml b/.github/workflows/deptrac.yml deleted file mode 100644 index d44fb05..0000000 --- a/.github/workflows/deptrac.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Deptrac - -on: - pull_request: - branches: - - develop - paths: - - '**.php' - - 'composer.*' - - 'depfile.yaml' - - '.github/workflows/deptrac.yml' - push: - branches: - - develop - paths: - - '**.php' - - 'composer.*' - - 'depfile.yaml' - - '.github/workflows/deptrac.yml' - -jobs: - build: - name: Dependency Tracing - runs-on: ubuntu-latest - if: (! contains(github.event.head_commit.message, '[ci skip]')) - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - extensions: intl, json, mbstring, xml - coverage: none - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Get composer cache directory - run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - - - name: Cache composer dependencies - uses: actions/cache@v4 - with: - path: ${{ env.COMPOSER_CACHE_FILES_DIR }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Create Deptrac cache directory - run: mkdir -p build/ - - - name: Cache Deptrac results - uses: actions/cache@v4 - with: - path: build - key: ${{ runner.os }}-deptrac-${{ github.sha }} - restore-keys: ${{ runner.os }}-deptrac- - - - name: Install dependencies - run: | - if [ -f composer.lock ]; then - composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader - else - composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader - fi - - - name: Trace dependencies - run: | - composer require --dev qossmic/deptrac-shim - vendor/bin/deptrac analyze --cache-file=build/deptrac.cache diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml index 25ce362..cb6dda1 100644 --- a/.github/workflows/infection.yml +++ b/.github/workflows/infection.yml @@ -23,7 +23,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.1' tools: infection, phpunit extensions: intl, json, mbstring, gd, xml, sqlite3 coverage: xdebug diff --git a/.github/workflows/phpcpd.yml b/.github/workflows/phpcpd.yml index b6062a0..dc712eb 100644 --- a/.github/workflows/phpcpd.yml +++ b/.github/workflows/phpcpd.yml @@ -27,7 +27,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.1' tools: phpcpd extensions: dom, mbstring coverage: none diff --git a/.github/workflows/phpcsfixer.yml b/.github/workflows/phpcsfixer.yml index 45ab00c..bd74a5f 100644 --- a/.github/workflows/phpcsfixer.yml +++ b/.github/workflows/phpcsfixer.yml @@ -27,7 +27,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.1' extensions: json, tokenizer coverage: none env: diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 4a85dab..09b72cb 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -26,7 +26,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['8.3', '8.4'] + php-versions: ['8.1'] steps: - name: Checkout diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index b4d3cbf..c4358ee 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -25,7 +25,7 @@ jobs: if: (! contains(github.event.head_commit.message, '[ci skip]')) strategy: matrix: - php-versions: ['8.3', '8.4'] + php-versions: ['8.1'] steps: - name: Checkout @@ -65,7 +65,7 @@ jobs: TERM: xterm-256color TACHYCARDIA_MONITOR_GA: enabled - - if: matrix.php-versions == '8.3' + - if: matrix.php-versions == '8.1' name: Run Coveralls continue-on-error: true run: | diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index 3257113..985c0f3 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -31,7 +31,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.1' tools: phpstan, phpunit extensions: intl, json, mbstring, xml coverage: none diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index a30d73b..42c4a2f 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -26,7 +26,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['8.3', '8.4'] + php-versions: ['8.1'] steps: - name: Checkout diff --git a/.github/workflows/unused.yml b/.github/workflows/unused.yml index 255cb96..616d24b 100644 --- a/.github/workflows/unused.yml +++ b/.github/workflows/unused.yml @@ -29,7 +29,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.1' tools: composer, composer-unused extensions: intl, json, mbstring, xml coverage: none diff --git a/composer.json b/composer.json index 21af3f8..512abb2 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,8 @@ ], "homepage": "https://github.com/maniaba/rule-engine", "require": { - "php": "^8.1" + "php": "^8.1", + "symfony/polyfill-php83": "^1.32" }, "require-dev": { "icanhazstring/composer-unused": "dev-main", diff --git a/deptrac.yaml b/deptrac.yaml deleted file mode 100644 index ea116b1..0000000 --- a/deptrac.yaml +++ /dev/null @@ -1,156 +0,0 @@ -parameters: - paths: - - ./src/ - - ./vendor/codeigniter4/framework/system/ - exclude_files: - - '#.*test.*#i' - layers: - - name: Model - collectors: - - type: bool - must: - - type: className - regex: .*[A-Za-z]+Model$ - must_not: - - type: directory - regex: vendor/.* - - name: Vendor Model - collectors: - - type: bool - must: - - type: className - regex: .*[A-Za-z]+Model$ - - type: directory - regex: vendor/.* - - name: Controller - collectors: - - type: bool - must: - - type: className - regex: .*\/Controllers\/.* - must_not: - - type: directory - regex: vendor/.* - - name: Vendor Controller - collectors: - - type: bool - must: - - type: className - regex: .*\/Controllers\/.* - - type: directory - regex: vendor/.* - - name: Config - collectors: - - type: bool - must: - - type: directory - regex: src/Config/.* - must_not: - - type: className - regex: .*Services - - type: directory - regex: vendor/.* - - name: Vendor Config - collectors: - - type: bool - must: - - type: directory - regex: vendor/.*/Config/.* - must_not: - - type: className - regex: .*Services - - name: Entity - collectors: - - type: bool - must: - - type: directory - regex: src/Entities/.* - must_not: - - type: directory - regex: vendor/.* - - name: Vendor Entity - collectors: - - type: bool - must: - - type: directory - regex: vendor/.*/Entities/.* - - name: View - collectors: - - type: bool - must: - - type: directory - regex: src/Views/.* - must_not: - - type: directory - regex: vendor/.* - - name: Vendor View - collectors: - - type: bool - must: - - type: directory - regex: vendor/.*/Views/.* - - name: Service - collectors: - - type: className - regex: .*Services.* - ruleset: - Entity: - - Config - - Model - - Service - - Vendor Config - - Vendor Entity - - Vendor Model - Config: - - Service - - Vendor Config - Model: - - Config - - Entity - - Service - - Vendor Config - - Vendor Entity - - Vendor Model - Service: - - Config - - Vendor Config - - # Ignore anything in the Vendor layers - Vendor Model: - - Config - - Service - - Vendor Config - - Vendor Controller - - Vendor Entity - - Vendor Model - - Vendor View - Vendor Controller: - - Service - - Vendor Config - - Vendor Controller - - Vendor Entity - - Vendor Model - - Vendor View - Vendor Config: - - Config - - Service - - Vendor Config - - Vendor Controller - - Vendor Entity - - Vendor Model - - Vendor View - Vendor Entity: - - Service - - Vendor Config - - Vendor Controller - - Vendor Entity - - Vendor Model - - Vendor View - Vendor View: - - Service - - Vendor Config - - Vendor Controller - - Vendor Entity - - Vendor Model - - Vendor View - skip_violations: diff --git a/infection.json.dist b/infection.json.dist index e69de29..4777f1d 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -0,0 +1,15 @@ +{ + "source": { + "directories": [ + "src/" + ], + "excludes": [ + ] + }, + "logs": { + "text": "build/infection.log" + }, + "mutators": { + "@default": true + } +} diff --git a/src/Builders/ArrayBuilder.php b/src/Builders/ArrayBuilder.php index 062a5e7..888e4c8 100644 --- a/src/Builders/ArrayBuilder.php +++ b/src/Builders/ArrayBuilder.php @@ -152,9 +152,13 @@ private function buildRule(array $config): Rule } /** - * Kreira ConditionInterface na osnovu konfiguracije. + * Creates a ConditionInterface from the provided configuration. * - * @param array $config konfiguracija uslova + * @param array $config The condition configuration array + * + * @return ConditionInterface The created condition + * + * @throws BuilderException If the configuration is invalid */ private function buildCondition(array $config): ConditionInterface { @@ -206,10 +210,23 @@ private function buildCondition(array $config): ConditionInterface } } + /** + * Builds either an action or a condition based on the configuration. + * + * This method is used for processing 'then' and 'else' branches in conditional nodes. + * It determines whether to build an action or a condition based on the node type. + * + * @param array &$config The configuration array to process (passed by reference) + * @param string $key The key to process ('then' or 'else') + * + * @return array The updated configuration array + * + * @throws BuilderException If multiple actions are found in a single action node + */ private function buildActionOrCondition(array &$config, string $key): array { if (\array_key_exists($key, $config)) { - // Ako je '$key' čvor akcija, inače je uslov + // If '$key' is an action node, otherwise it's a condition if (isset($config[$key]['node']) && 'action' === $config[$key]['node']) { $config[$key] = $this->buildActions($config[$key]); @@ -226,9 +243,16 @@ private function buildActionOrCondition(array &$config, string $key): array } /** - * Kreira niz ActionInterface ili ConditionInterface na osnovu konfiguracije. + * Creates a list of ActionInterface instances from the provided configuration. + * + * This method processes action configurations and creates the corresponding action objects. + * If a single action node is provided, it's converted to an array for uniform processing. * - * @return list + * @param mixed $actionsConfig The actions configuration (must be an array) + * + * @return list The list of created action instances + * + * @throws BuilderException If the configuration is invalid */ private function buildActions(mixed $actionsConfig): array { @@ -236,7 +260,7 @@ private function buildActions(mixed $actionsConfig): array throw new BuilderException('Actions configuration must be an array.'); } - // Ako je jedan čvor, pretvaramo ga u niz za uniformnost + // If it's a single node, convert it to an array for uniform processing if (isset($actionsConfig['node'])) { $actionsConfig = [$actionsConfig]; } diff --git a/src/Builders/JsonBuilder.php b/src/Builders/JsonBuilder.php index d165934..e2ce6c3 100644 --- a/src/Builders/JsonBuilder.php +++ b/src/Builders/JsonBuilder.php @@ -4,17 +4,16 @@ namespace Maniaba\RuleEngine\Builders; -use CodeIgniter\Files\File; use JsonException; use Maniaba\RuleEngine\Rules\RuleSet; -use Override; +use SplFileInfo; final class JsonBuilder extends ArrayBuilder { /** * @throws JsonException */ - public function parseFile(File $file): RuleSet + public function parseFile(SplFileInfo $file): RuleSet { $config = file_get_contents($file->getRealPath()); @@ -24,7 +23,6 @@ public function parseFile(File $file): RuleSet /** * @throws JsonException */ - #[Override] public function build(mixed $config): RuleSet { if (! \is_string($config) || json_validate($config) === false) { From d2500b50b98a7ed156baffa6d7011a1c189d9ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 17:46:42 +0200 Subject: [PATCH 04/16] feat: update failure message handling and improve PHPDoc for getFailureMessage method --- src/Conditions/CollectionCondition.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Conditions/CollectionCondition.php b/src/Conditions/CollectionCondition.php index 4482549..c294be9 100644 --- a/src/Conditions/CollectionCondition.php +++ b/src/Conditions/CollectionCondition.php @@ -68,7 +68,7 @@ public function isSatisfied(ContextInterface $context): bool if (\is_array($failureMessage)) { $this->failureMessages = array_merge($this->failureMessages, $failureMessage); - } elseif ('' !== $failureMessage && '0' !== $failureMessage && [] !== $failureMessage) { + } elseif (null !== $failureMessage && '' !== $failureMessage) { $this->failureMessages[] = $failureMessage; } } @@ -93,7 +93,7 @@ public function isSatisfied(ContextInterface $context): bool /** * Returns the failure messages of conditions that failed. * - * @return array|null an array of failure messages or null if no failure + * @return list|null an array of failure messages or null if no failure */ public function getFailureMessage(): ?array { From 5ce2959e422d44a05e2f8d3d106efbaf604853a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 17:51:35 +0200 Subject: [PATCH 05/16] feat: improve FieldSelector handling for stdClass and object properties --- .../FactoryTraits/OnlyValueFactoryTrait.php | 22 ------------------- src/Context/FieldSelector.php | 10 ++++++--- 2 files changed, 7 insertions(+), 25 deletions(-) delete mode 100644 src/Conditions/FactoryTraits/OnlyValueFactoryTrait.php diff --git a/src/Conditions/FactoryTraits/OnlyValueFactoryTrait.php b/src/Conditions/FactoryTraits/OnlyValueFactoryTrait.php deleted file mode 100644 index 6044e2d..0000000 --- a/src/Conditions/FactoryTraits/OnlyValueFactoryTrait.php +++ /dev/null @@ -1,22 +0,0 @@ -{$key}; } elseif (\is_object($data) && method_exists($data, 'get' . ucfirst($key))) { $data = $data->{'get' . ucfirst($key)}(); - } elseif ($data instanceof BaseEntity) { + } elseif (($data instanceof stdClass || is_object($data)) && property_exists($data, $key) + ) { $data = $data->{$key}; } else { throw new InvalidArgumentException("Key '{$key}' does not exist."); @@ -101,7 +102,10 @@ private static function resolveArray(array $array, string $filter): mixed $value = self::castValue($matches[3]); foreach ($array as $item) { - $itemValue = \is_array($item) ? $item[$key] ?? null : (property_exists($item, $key) ? $item->{$key} : ($item instanceof BaseEntity ? $item->{$key} : null)); + $itemValue = \is_array($item) + ? $item[$key] ?? null : + (property_exists($item, $key) ? $item->{$key} : + ($item instanceof stdClass ? $item->{$key} : null)); if (null === $itemValue) { continue; From 7155259cdc9dad90da3c9066457c369a43f2e731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 17:53:47 +0200 Subject: [PATCH 06/16] feat: update argument type validation in ActionFactory and upgrade PHP version in CI configuration --- .github/workflows/unused.yml | 2 +- src/Factories/ActionFactory.php | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unused.yml b/.github/workflows/unused.yml index 616d24b..255cb96 100644 --- a/.github/workflows/unused.yml +++ b/.github/workflows/unused.yml @@ -29,7 +29,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: '8.3' tools: composer, composer-unused extensions: intl, json, mbstring, xml coverage: none diff --git a/src/Factories/ActionFactory.php b/src/Factories/ActionFactory.php index f479b87..55039f7 100644 --- a/src/Factories/ActionFactory.php +++ b/src/Factories/ActionFactory.php @@ -173,7 +173,8 @@ private static function testParameters(array $parameters, array $arguments, stri $valid = false; foreach ($parameterType->getTypes() as $type) { - $validTypes = [...array_map(static fn ($type) => $type->getName(), [$type]), 'mixed']; + $typeName = $type instanceof ReflectionUnionType ? 'mixed' : ($type->getName() ?? 'mixed'); + $validTypes = [$typeName, 'mixed']; $argumentType = self::gettype($arguments[$name]); if (\in_array($argumentType, $validTypes, true)) { @@ -183,7 +184,13 @@ private static function testParameters(array $parameters, array $arguments, stri } if (! $valid) { - throw new InvalidArgumentException("Invalid type for argument: '{$name}'. Expected one of: " . implode(', ', array_map(static fn ($type) => $type->getName(), $parameterType->getTypes())) . ', got: ' . ($argumentType ?? \gettype($arguments[$name]))); + $typeNames = []; + + foreach ($parameterType->getTypes() as $type) { + $typeNames[] = $type instanceof ReflectionUnionType ? 'mixed' : ($type->getName() ?? 'mixed'); + } + + throw new InvalidArgumentException("Invalid type for argument: '{$name}'. Expected one of: " . implode(', ', $typeNames) . ', got: ' . ($argumentType ?? \gettype($arguments[$name]))); } } else { $parameterTypeName = $parameterType?->getName(); From ac2e5f61b57bc6a03fd06c9b40e248659099f3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 18:01:19 +0200 Subject: [PATCH 07/16] feat: update argument type validation in ActionFactory and upgrade PHP version in CI configuration --- .github/workflows/infection.yml | 2 +- src/Context/FieldSelector.php | 3 +-- src/Context/ObjectContext.php | 27 ++++++++++++++++++++- src/Evaluators/Results/EvaluationResult.php | 8 +++--- tests/Builders/ArrayBuilderTest.php | 6 ++--- tests/Builders/JsonBuilderTest.php | 6 ++--- 6 files changed, 37 insertions(+), 15 deletions(-) diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml index cb6dda1..25ce362 100644 --- a/.github/workflows/infection.yml +++ b/.github/workflows/infection.yml @@ -23,7 +23,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: '8.3' tools: infection, phpunit extensions: intl, json, mbstring, gd, xml, sqlite3 coverage: xdebug diff --git a/src/Context/FieldSelector.php b/src/Context/FieldSelector.php index bb5dca4..3db4162 100644 --- a/src/Context/FieldSelector.php +++ b/src/Context/FieldSelector.php @@ -104,8 +104,7 @@ private static function resolveArray(array $array, string $filter): mixed foreach ($array as $item) { $itemValue = \is_array($item) ? $item[$key] ?? null : - (property_exists($item, $key) ? $item->{$key} : - ($item instanceof stdClass ? $item->{$key} : null)); + (property_exists($item, $key) ? $item->{$key} : null); if (null === $itemValue) { continue; diff --git a/src/Context/ObjectContext.php b/src/Context/ObjectContext.php index 94f1e77..a469672 100644 --- a/src/Context/ObjectContext.php +++ b/src/Context/ObjectContext.php @@ -4,7 +4,9 @@ namespace Maniaba\RuleEngine\Context; -final readonly class ObjectContext implements ContextInterface +use JsonException; + +final class ObjectContext implements ContextInterface { public function __construct( private object $object, @@ -20,4 +22,27 @@ public function hasField(string $field): bool { return property_exists($this->object, $field); } + + public function getObject(): object + { + return $this->object; + } + + public function setObject(object $object): void + { + $this->object = $object; + } + + public function toArray(): array + { + return (array) $this->object; + } + + /** + * @throws JsonException + */ + public function __toString(): string + { + return json_encode($this->toArray(), JSON_THROW_ON_ERROR); + } } diff --git a/src/Evaluators/Results/EvaluationResult.php b/src/Evaluators/Results/EvaluationResult.php index 97f307f..cb27209 100644 --- a/src/Evaluators/Results/EvaluationResult.php +++ b/src/Evaluators/Results/EvaluationResult.php @@ -6,15 +6,15 @@ use Maniaba\RuleEngine\Rules\RuleInterface; -final readonly class EvaluationResult +final class EvaluationResult { /** * Konstruktor za kreiranje EvaluationResult. */ public function __construct( - public RuleInterface $rule, - public bool $result, - public array|string|null $failureMessage, + public readonly RuleInterface $rule, + public readonly bool $result, + public readonly array|string|null $failureMessage, ) { } } diff --git a/tests/Builders/ArrayBuilderTest.php b/tests/Builders/ArrayBuilderTest.php index 17c71a3..49f3259 100644 --- a/tests/Builders/ArrayBuilderTest.php +++ b/tests/Builders/ArrayBuilderTest.php @@ -118,7 +118,6 @@ public function testBuildValidConfigReturnsRuleSet(): void ]; $ruleSet = $builder->build($config); - $this->assertInstanceOf(RuleSet::class, $ruleSet, 'build treba vratiti RuleSet za validnu konfiguraciju'); $this->assertCount(1, $ruleSet->getRules(), 'Treba biti 1 rule u RuleSet-u'); } @@ -147,7 +146,7 @@ public function testBuildRuleSetWithNotCondition(): void // Kreiramo RuleSet iz konfiguracije $builder = new ArrayBuilder(); // dummy action - $builder->actions()->registerAction('rejectDeposit', new CallableAction(static fn (): null => null)); + $builder->actions()->registerAction('rejectDeposit', new CallableAction(static fn (ContextInterface $context): bool => true)); $ruleSet = $builder->build($config); @@ -200,7 +199,6 @@ public function testBuildActionWithArguments(): void ]; $ruleSet = $builder->build($config); - $this->assertInstanceOf(RuleSet::class, $ruleSet, 'build treba vratiti RuleSet za validnu konfiguraciju'); $this->assertCount(1, $ruleSet->getRules(), 'Treba biti 1 rule u RuleSet-u'); $context = $this->createMock(ContextInterface::class); @@ -212,7 +210,7 @@ protected function getBuilder(): RuleSet { $builder = new ArrayBuilder(); - $dummyAction = new CallableAction(static fn (): null => null); + $dummyAction = new CallableAction(static fn (ContextInterface $context): bool => true); $builder->actions()->registerAction('actionName1', $dummyAction); $builder->actions()->registerAction('actionName2', $dummyAction); $builder->actions()->registerAction('rejectDeposit', $dummyAction); diff --git a/tests/Builders/JsonBuilderTest.php b/tests/Builders/JsonBuilderTest.php index fe311aa..fa7859e 100644 --- a/tests/Builders/JsonBuilderTest.php +++ b/tests/Builders/JsonBuilderTest.php @@ -4,12 +4,12 @@ namespace Tests\Builders; -use CodeIgniter\Files\File; use JsonException; use Maniaba\RuleEngine\Actions\CallableAction; use Maniaba\RuleEngine\Builders\JsonBuilder; use Maniaba\RuleEngine\Rules\RuleSet; use PHPUnit\Framework\Attributes\Group; +use SplFileInfo; use Tests\Support\Actions\DummyArgumentsAction; use Tests\Support\TestCase; @@ -60,7 +60,7 @@ protected function getBuilder(): RuleSet return $builder->build(json_encode(self::configBuilder())); } - private static function createTempJsonFile(): File + private static function createTempJsonFile(): SplFileInfo { $tempFile = tempnam(sys_get_temp_dir(), 'json'); @@ -72,6 +72,6 @@ private static function createTempJsonFile(): File ], ])); - return new File($tempFile); + return new SplFileInfo($tempFile); } } From d5dadcc97eb2e0a460b30c71fa396ef382b17b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 18:05:18 +0200 Subject: [PATCH 08/16] feat: refactor ObjectContext to implement Stringable and update TestAction property names --- infection.json.dist | 2 -- src/Context/ObjectContext.php | 3 ++- tests/_support/Factories/TestAction.php | 22 +++++++++++++++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/infection.json.dist b/infection.json.dist index 4777f1d..dcf7b63 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -2,8 +2,6 @@ "source": { "directories": [ "src/" - ], - "excludes": [ ] }, "logs": { diff --git a/src/Context/ObjectContext.php b/src/Context/ObjectContext.php index a469672..3b66e2b 100644 --- a/src/Context/ObjectContext.php +++ b/src/Context/ObjectContext.php @@ -5,8 +5,9 @@ namespace Maniaba\RuleEngine\Context; use JsonException; +use Stringable; -final class ObjectContext implements ContextInterface +final class ObjectContext implements ContextInterface, Stringable { public function __construct( private object $object, diff --git a/tests/_support/Factories/TestAction.php b/tests/_support/Factories/TestAction.php index c2500cb..dfd9c63 100644 --- a/tests/_support/Factories/TestAction.php +++ b/tests/_support/Factories/TestAction.php @@ -11,7 +11,7 @@ final class TestAction implements ActionInterface { public function __construct( private readonly float|int|string|null $field, - private readonly ?string $nulla, + private readonly ?string $null, private readonly float|int|string $value = 12, private readonly ?string $valueNonSigned = null, ) { @@ -23,4 +23,24 @@ public function execute(ContextInterface $context): bool return true; } + + public function getField(): float|int|string|null + { + return $this->field; + } + + public function getNull(): ?string + { + return $this->null; + } + + public function getValue(): float|int|string + { + return $this->value; + } + + public function getValueNonSigned(): ?string + { + return $this->valueNonSigned; + } } From 8a0d98bad6aaa39dbc7c9f8614b2c655d04c836d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 18:08:47 +0200 Subject: [PATCH 09/16] feat: remove redundant ActionInterface assertions in tests --- tests/Builders/ArrayBuilderTest.php | 4 ++-- tests/Factories/ActionFactoryTest.php | 12 ++++-------- tests/Rules/RuleSetTest.php | 1 - 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/Builders/ArrayBuilderTest.php b/tests/Builders/ArrayBuilderTest.php index 49f3259..b2234a4 100644 --- a/tests/Builders/ArrayBuilderTest.php +++ b/tests/Builders/ArrayBuilderTest.php @@ -20,7 +20,7 @@ /** * @internal * - * @description Testiranje ArrayBuilder klase. Testira da izgradi RuleSet na osnovu konfiguracije iz niza self::configBuilder(). + * @description Testing the ArrayBuilder class. Tests building a RuleSet based on configuration from the self::configBuilder() array. */ #[Group('Others')] final class ArrayBuilderTest extends TestCase @@ -39,7 +39,7 @@ public function testBuildWithMissingNodeThrowsException(): void { $builder = new ArrayBuilder(); - // Nedostaje 'node' ključ + // Missing 'node' key $config = [ 'if' => [ 'node' => 'context', diff --git a/tests/Factories/ActionFactoryTest.php b/tests/Factories/ActionFactoryTest.php index b80b9a1..be21b7f 100644 --- a/tests/Factories/ActionFactoryTest.php +++ b/tests/Factories/ActionFactoryTest.php @@ -36,11 +36,10 @@ public function testCreateWithValidActionClass(): void // Act $action = $this->actionFactory->create([ 'actionName' => 'testAction', - 'arguments' => ['field' => 'testField', 'value' => 42, 'valueNonSigned' => 'optional', 'nulla' => null], + 'arguments' => ['field' => 'testField', 'value' => 42, 'valueNonSigned' => 'optional', 'null' => null], ]); // Assert - $this->assertInstanceOf(ActionInterface::class, $action); $this->assertInstanceOf(TestAction::class, $action); } @@ -105,11 +104,10 @@ public function testRegisterActions(): void // Act $action = $this->actionFactory->create([ 'actionName' => 'testAction', - 'arguments' => ['field' => 'testField', 'value' => 42, 'valueNonSigned' => 'optional', 'nulla' => null], + 'arguments' => ['field' => 'testField', 'value' => 42, 'valueNonSigned' => 'optional', 'null' => null], ]); // Assert - $this->assertInstanceOf(ActionInterface::class, $action); $this->assertInstanceOf(TestAction::class, $action); } @@ -177,7 +175,6 @@ public function testCreateActionWithoutConstructor(): void ]); // Assert - $this->assertInstanceOf(ActionInterface::class, $action); $this->assertInstanceOf(TestActionWithConstructorException::class, $action); } @@ -196,7 +193,6 @@ public function testCreateActionWithInstance(): void ]); // Assert - $this->assertInstanceOf(ActionInterface::class, $createdAction); $this->assertInstanceOf(TestAction::class, $createdAction); } @@ -225,7 +221,7 @@ public function testCreateWithMissingArguments(): void $this->actionFactory->registerAction('testAction', TestAction::class); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("Missing required argument: 'nulla' for action: 'testAction'"); + $this->expectExceptionMessage("Missing required argument: 'null' for action: 'testAction'"); // Act $this->actionFactory->create([ @@ -242,7 +238,7 @@ public function testRegisterAndUnregisterActions(): void // Assert Registered $action = $this->actionFactory->create([ 'actionName' => 'testAction', - 'arguments' => ['field' => 'testField', 'value' => 42, 'valueNonSigned' => 'optional', 'nulla' => null], + 'arguments' => ['field' => 'testField', 'value' => 42, 'valueNonSigned' => 'optional', 'null' => null], ]); $this->assertInstanceOf(TestAction::class, $action); diff --git a/tests/Rules/RuleSetTest.php b/tests/Rules/RuleSetTest.php index a626190..5958c10 100644 --- a/tests/Rules/RuleSetTest.php +++ b/tests/Rules/RuleSetTest.php @@ -62,7 +62,6 @@ public function testEvaluateWithAllPassingRules(): void $this->assertCount(2, $results, 'Treba biti 2 rezultata evaluacije'); foreach ($results as $result) { - $this->assertInstanceOf(EvaluationResult::class, $result, 'Svaki rezultat treba biti instanca EvaluationResult'); $this->assertTrue($result->result, 'Rezultat evaluacije treba biti true jer pravilo prolazi'); } From d4e602494839d5d497a19edc9acb53c8b9e11a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 18:15:10 +0200 Subject: [PATCH 10/16] feat: update ActionFactory to use callable type and improve test comments --- tests/Builders/ArrayBuilderTest.php | 38 +++++++++++----------- tests/Builders/JsonBuilderTest.php | 2 +- tests/Context/FieldSelectorTest.php | 10 +++--- tests/Evaluators/BasicEvaluatorTest.php | 2 -- tests/Evaluators/PriorityEvaluatorTest.php | 4 --- 5 files changed, 25 insertions(+), 31 deletions(-) diff --git a/tests/Builders/ArrayBuilderTest.php b/tests/Builders/ArrayBuilderTest.php index b2234a4..a453758 100644 --- a/tests/Builders/ArrayBuilderTest.php +++ b/tests/Builders/ArrayBuilderTest.php @@ -71,17 +71,17 @@ public function testBuildWithContextNodeMissingContextName(): void { $builder = new ArrayBuilder(); - // context node bez contextName + // context node without contextName $config = [ 'node' => 'context', 'operator' => 'equal', 'value' => 10, ]; - // U ConditionFactory očekujemo da se baca greška ako nema contextName. - // Ako ConditionFactory ne baca, ArrayBuilder treba baciti jer config nije validan. - // U ovom slučaju, ArrayBuilder će pokušati kreirati ConditionConfig i najvjerovatnije dobiti error jer - // contextName nije definisan. + // In ConditionFactory we expect an error to be thrown if contextName is missing. + // If ConditionFactory doesn't throw, ArrayBuilder should throw because the config is invalid. + // In this case, ArrayBuilder will try to create ConditionConfig and most likely get an error because + // contextName is not defined. $this->expectException(BuilderException::class); $this->expectExceptionMessage('Invalid node structure: Context name is required.'); @@ -92,17 +92,17 @@ public function testBuildConditionNodeWithoutIfThrowsException(): void { $builder = new ArrayBuilder(); - // condition node bez 'if' + // condition node without 'if' $config = [ 'node' => 'condition', - // 'if' nedostaje + // 'if' is missing ]; $this->expectException(BuilderException::class); $this->expectExceptionMessage("Invalid node structure: 'if' key is missing in 'condition' node."); - // Poruka bi trebala doći iz buildCondition metode kad se pokuša pristupiti $config['if'] koji ne postoji - // ili iz nepostojanja validnog condition. - // Možete prilagoditi poruku u kodu ako želite specifičniju. + // The message should come from the buildCondition method when trying to access $config['if'] which doesn't exist + // or from the absence of a valid condition. + // You can adjust the message in the code if you want it to be more specific. $builder->build($config); } @@ -118,12 +118,12 @@ public function testBuildValidConfigReturnsRuleSet(): void ]; $ruleSet = $builder->build($config); - $this->assertCount(1, $ruleSet->getRules(), 'Treba biti 1 rule u RuleSet-u'); + $this->assertCount(1, $ruleSet->getRules(), 'There should be 1 rule in the RuleSet'); } public function testBuildRuleSetWithNotCondition(): void { - // Konfiguracija pravila + // Rule configuration $config = [ [ 'node' => 'condition', @@ -143,31 +143,31 @@ public function testBuildRuleSetWithNotCondition(): void ], ]; - // Kreiramo RuleSet iz konfiguracije + // Create RuleSet from configuration $builder = new ArrayBuilder(); // dummy action $builder->actions()->registerAction('rejectDeposit', new CallableAction(static fn (ContextInterface $context): bool => true)); $ruleSet = $builder->build($config); - // Proveravamo da je kreiran RuleSet + // Check that RuleSet is created $this->assertInstanceOf(RuleSet::class, $ruleSet); $this->assertCount(1, $ruleSet->getRules()); - // Proveravamo prvu komponentu pravila + // Check the first component of the rule $rule = $ruleSet->getRules()[0]; /** * @var IfElseCondition $condition */ $condition = $this->getPrivateProperty($rule, 'condition'); - // Proveravamo da li je 'if' deo negacija + // Check if the 'if' part is a negation $this->assertInstanceOf(IfElseCondition::class, $condition); $conditionNot = $this->getPrivateProperty($condition, 'ifCondition'); $this->assertInstanceOf(NotCondition::class, $conditionNot); - // Proveravamo unutar NotCondition da li sadrži ispravan uslov + // Check inside NotCondition if it contains the correct condition $innerCondition = $this->getPrivateProperty($conditionNot, 'condition'); $this->assertInstanceOf(GreaterThanOrEqualCondition::class, $innerCondition); @@ -177,7 +177,7 @@ public function testBuildRuleSetWithNotCondition(): void $this->assertSame('depositCount', $contextName); $this->assertSame(5, $value); - // Proveravamo da li je 'then' deo akcija + // Check if the 'then' part is an action /** * @var ActionInterface $action */ @@ -199,7 +199,7 @@ public function testBuildActionWithArguments(): void ]; $ruleSet = $builder->build($config); - $this->assertCount(1, $ruleSet->getRules(), 'Treba biti 1 rule u RuleSet-u'); + $this->assertCount(1, $ruleSet->getRules(), 'There should be 1 rule in the RuleSet'); $context = $this->createMock(ContextInterface::class); diff --git a/tests/Builders/JsonBuilderTest.php b/tests/Builders/JsonBuilderTest.php index fa7859e..649c265 100644 --- a/tests/Builders/JsonBuilderTest.php +++ b/tests/Builders/JsonBuilderTest.php @@ -14,7 +14,7 @@ use Tests\Support\TestCase; /** - * Testiranje JsonBuilder klase. + * Testing the JsonBuilder class. * * @internal */ diff --git a/tests/Context/FieldSelectorTest.php b/tests/Context/FieldSelectorTest.php index 1af0dd1..54e79e6 100644 --- a/tests/Context/FieldSelectorTest.php +++ b/tests/Context/FieldSelectorTest.php @@ -20,10 +20,10 @@ final class FieldSelectorTest extends TestCase { /** - * Podaci za testiranje. + * Test data. * - * Struktura podataka je slična onoj iz primjera u objašnjenju, ali proširena - * dodatnim poljima kako bismo testirali više slučajeva. + * The data structure is similar to the one from the examples in the explanation, but expanded + * with additional fields to test more cases. */ private array $data; @@ -90,7 +90,7 @@ public function testEvaluateConditionInvalidOperator(): void public function testEvaluateConditionWithBackedEnum(): void { - // Testiranje BackedEnum vrijednosti + // Testing BackedEnum values $result = $this->callEvaluateCondition(EnumIntTest::ACTIVE, '=', 1); $this->assertTrue($result); @@ -106,7 +106,7 @@ public function testEvaluateConditionWithBackedEnum(): void public function testEvaluateConditionWithUnitEnum(): void { - // Testiranje UnitEnum vrijednosti (koristi `name`) + // Testing UnitEnum values (uses `name`) $result = $this->callEvaluateCondition(SimpleEnum::OPTION_ONE, '=', 'OPTION_ONE'); $this->assertTrue($result); diff --git a/tests/Evaluators/BasicEvaluatorTest.php b/tests/Evaluators/BasicEvaluatorTest.php index 4b3a13b..419e057 100644 --- a/tests/Evaluators/BasicEvaluatorTest.php +++ b/tests/Evaluators/BasicEvaluatorTest.php @@ -6,7 +6,6 @@ use Maniaba\RuleEngine\Context\ContextInterface; use Maniaba\RuleEngine\Evaluators\BasicEvaluator; -use Maniaba\RuleEngine\Evaluators\Results\EvaluationResult; use Maniaba\RuleEngine\Rules\RuleInterface; use Maniaba\RuleEngine\Rules\RuleSet; use PHPUnit\Framework\Attributes\Group; @@ -58,7 +57,6 @@ public function testEvaluateAllPassingRules(): void $this->assertCount(2, $results, 'Treba biti 2 rezultata evaluacije'); foreach ($results as $result) { - $this->assertInstanceOf(EvaluationResult::class, $result); $this->assertTrue($result->result, 'Rezultat treba biti true jer pravilo prolazi'); } diff --git a/tests/Evaluators/PriorityEvaluatorTest.php b/tests/Evaluators/PriorityEvaluatorTest.php index cb57ee2..4256192 100644 --- a/tests/Evaluators/PriorityEvaluatorTest.php +++ b/tests/Evaluators/PriorityEvaluatorTest.php @@ -6,7 +6,6 @@ use Maniaba\RuleEngine\Context\ContextInterface; use Maniaba\RuleEngine\Evaluators\PriorityEvaluator; -use Maniaba\RuleEngine\Evaluators\Results\EvaluationResult; use Maniaba\RuleEngine\Rules\RuleInterface; use Maniaba\RuleEngine\Rules\RuleSet; use PHPUnit\Framework\Attributes\Group; @@ -51,9 +50,6 @@ public function testEvaluateSortsRulesByPriority(): void // Provjeravamo redoslijed: najveći prioritet treba biti evaluiran prvi // Prema usort logici, redoslijed treba biti: highPriorityRule(10), midPriorityRule(5), lowPriorityRule(1). $this->assertCount(3, $results, 'Treba biti 3 rezultata evaluacije'); - $this->assertInstanceOf(EvaluationResult::class, $results[0]); - $this->assertInstanceOf(EvaluationResult::class, $results[1]); - $this->assertInstanceOf(EvaluationResult::class, $results[2]); $this->assertSame($highPriorityRule, $results[0]->rule, 'Prvo evaluirano pravilo treba imati najveći prioritet'); $this->assertSame($midPriorityRule, $results[1]->rule, 'Drugo evaluirano pravilo treba biti srednjeg prioriteta'); From f556eb04f6cb3995108d510b391d654add9ecda7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 18:16:15 +0200 Subject: [PATCH 11/16] feat: clean up test files by removing unused imports and updating comments --- tests/Context/FieldSelectorTest.php | 2 +- tests/Factories/ActionFactoryTest.php | 1 - tests/Rules/RuleSetTest.php | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Context/FieldSelectorTest.php b/tests/Context/FieldSelectorTest.php index 54e79e6..d78982a 100644 --- a/tests/Context/FieldSelectorTest.php +++ b/tests/Context/FieldSelectorTest.php @@ -122,7 +122,7 @@ public function testEvaluateConditionWithUnitEnum(): void public function testEvaluateConditionWithStandardValues(): void { - // Testiranje standardnih vrijednosti (brojevi, stringovi, itd.) + // Testing standard values (numbers, strings, etc.) $result = $this->callEvaluateCondition(5, '>', 3); $this->assertTrue($result); diff --git a/tests/Factories/ActionFactoryTest.php b/tests/Factories/ActionFactoryTest.php index be21b7f..a1d6f82 100644 --- a/tests/Factories/ActionFactoryTest.php +++ b/tests/Factories/ActionFactoryTest.php @@ -5,7 +5,6 @@ namespace Tests\Factories; use InvalidArgumentException; -use Maniaba\RuleEngine\Actions\ActionInterface; use Maniaba\RuleEngine\Actions\CallableAction; use Maniaba\RuleEngine\Context\ContextInterface; use Maniaba\RuleEngine\Factories\ActionFactory; diff --git a/tests/Rules/RuleSetTest.php b/tests/Rules/RuleSetTest.php index 5958c10..1617054 100644 --- a/tests/Rules/RuleSetTest.php +++ b/tests/Rules/RuleSetTest.php @@ -5,7 +5,6 @@ namespace Tests\Rules; use Maniaba\RuleEngine\Context\ContextInterface; -use Maniaba\RuleEngine\Evaluators\Results\EvaluationResult; use Maniaba\RuleEngine\Rules\RuleInterface; use Maniaba\RuleEngine\Rules\RuleSet; use PHPUnit\Framework\Attributes\Group; From ef047c68cd7199a8b2c4b19f89317ac277559494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 18:18:58 +0200 Subject: [PATCH 12/16] feat: update data provider return types to use Generator for improved type safety --- tests/Context/FieldSelectorTest.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/Context/FieldSelectorTest.php b/tests/Context/FieldSelectorTest.php index d78982a..a4b3188 100644 --- a/tests/Context/FieldSelectorTest.php +++ b/tests/Context/FieldSelectorTest.php @@ -4,6 +4,7 @@ namespace Tests\Context; +use Generator; use InvalidArgumentException; use Maniaba\RuleEngine\Context\FieldSelector; use PHPUnit\Framework\Attributes\DataProvider; @@ -149,7 +150,7 @@ public function testBasicIndexAccess(string $selector, array $expected): void /** * Data provider za testiranje osnovnog pristupa poljima preko numeričkog indeksa. * - * @return list + * @return Generator */ public static function provideBasicIndexAccess(): iterable { @@ -177,7 +178,7 @@ public function testEqualityFilter(string $selector, array $expected): void /** * Data provider za testiranje filtriranja po ključu i vrijednosti (operator `=`). * - * @return list + * @return Generator */ public static function provideEqualityFilter(): iterable { @@ -202,7 +203,7 @@ public function testNumericComparisons(string $selector, array $expected): void * Data provider za testiranje filtriranja po numeričkim usporedbama. * Testiramo `>`, `>=`, `<`, `<=`. * - * @return list + * @return Generator */ public static function provideNumericComparisons(): iterable { @@ -228,7 +229,7 @@ public function testBooleanFilter(string $selector, array $expected): void /** * Data provider za testiranje filtriranja po boolean vrijednostima. * - * @return list + * @return Generator */ public static function provideBooleanFilter(): iterable { @@ -238,7 +239,7 @@ public static function provideBooleanFilter(): iterable } /** - * Testira dohvaćanje duboko ugniježđenih vrijednosti (chaining). + * Testira dohvaćanje duboko ugniježdenih vrijednosti (chaining). * * Primjer: Dohvaćamo poruku loga s level=error. */ @@ -263,7 +264,7 @@ public function testInvalidSelectorsThrowExceptions(string $selector): void /** * Data provider za negativne slučajeve gdje očekujemo iznimku. * - * @return list + * @return Generator */ public static function provideInvalidSelectorsThrowExceptions(): iterable { From 5ce09319097ed1766c32d72d9ae2ed50e30af433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 18:22:36 +0200 Subject: [PATCH 13/16] feat: update ActionFactory type checks and improve test data provider return types --- src/Factories/ActionFactory.php | 5 +++-- tests/Builders/ArrayBuilderTest.php | 1 - tests/Builders/BuilderTestDemoConfigTrait.php | 8 ++++---- tests/Builders/JsonBuilderTest.php | 6 ++++-- tests/Context/FieldSelectorTest.php | 12 ++++++------ 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/Factories/ActionFactory.php b/src/Factories/ActionFactory.php index 55039f7..3ba15e2 100644 --- a/src/Factories/ActionFactory.php +++ b/src/Factories/ActionFactory.php @@ -12,6 +12,7 @@ use ReflectionClass; use ReflectionException; use ReflectionFunction; +use ReflectionNamedType; use ReflectionParameter; use ReflectionUnionType; use Tests\Factories\ActionFactoryTest; @@ -193,11 +194,11 @@ private static function testParameters(array $parameters, array $arguments, stri throw new InvalidArgumentException("Invalid type for argument: '{$name}'. Expected one of: " . implode(', ', $typeNames) . ', got: ' . ($argumentType ?? \gettype($arguments[$name]))); } } else { - $parameterTypeName = $parameterType?->getName(); + $parameterTypeName = $parameterType instanceof ReflectionNamedType ? $parameterType->getName() : null; $argumentType = self::gettype($arguments[$name]); // Check if the type is nullable or matches the argument type - if (! \in_array($parameterTypeName, [null, 'mixed'], true) && (! ($parameterType?->allowsNull() && null === $arguments[$name]) && $parameterTypeName !== $argumentType)) { + if (! \in_array($parameterTypeName, [null, 'mixed'], true) && (! ($parameterType && $parameterType->allowsNull() && null === $arguments[$name]) && $parameterTypeName !== $argumentType)) { throw new InvalidArgumentException("Invalid type for argument: '{$name}'. Expected: {$parameterTypeName}, got: " . $argumentType); } } diff --git a/tests/Builders/ArrayBuilderTest.php b/tests/Builders/ArrayBuilderTest.php index a453758..1f4316f 100644 --- a/tests/Builders/ArrayBuilderTest.php +++ b/tests/Builders/ArrayBuilderTest.php @@ -151,7 +151,6 @@ public function testBuildRuleSetWithNotCondition(): void $ruleSet = $builder->build($config); // Check that RuleSet is created - $this->assertInstanceOf(RuleSet::class, $ruleSet); $this->assertCount(1, $ruleSet->getRules()); // Check the first component of the rule diff --git a/tests/Builders/BuilderTestDemoConfigTrait.php b/tests/Builders/BuilderTestDemoConfigTrait.php index 95867d7..24bf002 100644 --- a/tests/Builders/BuilderTestDemoConfigTrait.php +++ b/tests/Builders/BuilderTestDemoConfigTrait.php @@ -38,16 +38,16 @@ public function testBuildRuleSet(): void $this->ifElseConditionActionElseAction($ifElseConditionAction); /** - * @var array{GreaterThanOrEqualCondition, GreaterThanOrEqualCondition, CollectionCondition} $conditions + * @var array{0: GreaterThanOrEqualCondition, 1: GreaterThanOrEqualCondition, 2: CollectionCondition} $conditions */ $conditions = $this->checkCollectionCondition($collection, CollectionConditionType::AND, 3); $this->checkInstanceCondition(GreaterThanOrEqualCondition::class, $conditions[0], 'depositCount', 2); $this->checkInstanceCondition(GreaterThanOrEqualCondition::class, $conditions[1], 'accountAge', 1); - $conditions = $this->checkCollectionCondition($conditions[2], CollectionConditionType::OR, 2); - $this->checkInstanceCondition(EqualsCondition::class, $conditions[0], 'hasVipStatus', true); - $this->checkInstanceCondition(LessThanOrEqualCondition::class, $conditions[1], 'withdrawalCount', 5); + $nestedConditions = $this->checkCollectionCondition($conditions[2], CollectionConditionType::OR, 2); + $this->checkInstanceCondition(EqualsCondition::class, $nestedConditions[0], 'hasVipStatus', true); + $this->checkInstanceCondition(LessThanOrEqualCondition::class, $nestedConditions[1], 'withdrawalCount', 5); } protected static function configBuilder(): mixed diff --git a/tests/Builders/JsonBuilderTest.php b/tests/Builders/JsonBuilderTest.php index 649c265..afea5f5 100644 --- a/tests/Builders/JsonBuilderTest.php +++ b/tests/Builders/JsonBuilderTest.php @@ -7,6 +7,7 @@ use JsonException; use Maniaba\RuleEngine\Actions\CallableAction; use Maniaba\RuleEngine\Builders\JsonBuilder; +use Maniaba\RuleEngine\Context\ContextInterface; use Maniaba\RuleEngine\Rules\RuleSet; use PHPUnit\Framework\Attributes\Group; use SplFileInfo; @@ -32,7 +33,8 @@ public function testParseFile(): void $ruleSet = $builder->parseFile($file); - $this->assertInstanceOf(RuleSet::class, $ruleSet); + /** @phpstan-ignore-next-line */ + $this->assertInstanceOf(RuleSet::class, $ruleSet, 'RuleSet should be an instance of RuleSet'); } // wrong json @@ -50,7 +52,7 @@ protected function getBuilder(): RuleSet { $builder = new JsonBuilder(); - $dummyAction = new CallableAction(static fn (): null => null); + $dummyAction = new CallableAction(static fn (ContextInterface $context): bool => true); $builder->actions()->registerAction('actionName1', $dummyAction); $builder->actions()->registerAction('actionName2', $dummyAction); $builder->actions()->registerAction('rejectDeposit', $dummyAction); diff --git a/tests/Context/FieldSelectorTest.php b/tests/Context/FieldSelectorTest.php index a4b3188..f2b7b3f 100644 --- a/tests/Context/FieldSelectorTest.php +++ b/tests/Context/FieldSelectorTest.php @@ -154,19 +154,19 @@ public function testBasicIndexAccess(string $selector, array $expected): void */ public static function provideBasicIndexAccess(): iterable { - yield 'prvi korisnik' => ['users[0]', ['id' => 1, 'name' => 'Alice', 'age' => 25, 'active' => true]]; + yield 'first user' => ['users[0]', ['id' => 1, 'name' => 'Alice', 'age' => 25, 'active' => true]]; - yield 'drugi korisnik' => ['users[1]', ['id' => 2, 'name' => 'Bob', 'age' => 30, 'active' => false]]; + yield 'second user' => ['users[1]', ['id' => 2, 'name' => 'Bob', 'age' => 30, 'active' => false]]; - yield 'treci korisnik' => ['users[2]', ['id' => 3, 'name' => 'Charlie', 'age' => 35, 'active' => true]]; + yield 'third user' => ['users[2]', ['id' => 3, 'name' => 'Charlie', 'age' => 35, 'active' => true]]; - yield 'prvi log' => ['logs[0]', ['id' => 1, 'level' => 'info', 'message' => 'Initialization successful.']]; + yield 'first log' => ['logs[0]', ['id' => 1, 'level' => 'info', 'message' => 'Initialization successful.']]; - yield 'drugi log' => ['logs[1]', ['id' => 2, 'level' => 'error', 'message' => 'Database connection failed.']]; + yield 'second log' => ['logs[1]', ['id' => 2, 'level' => 'error', 'message' => 'Database connection failed.']]; } /** - * Testira filtriranje jednakosti (`=`). + * Tests equality filtering (`=`). */ #[DataProvider('provideEqualityFilter')] public function testEqualityFilter(string $selector, array $expected): void From 3473101e4134b9983e7206ba59f2b311fc01f3f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 18:28:01 +0200 Subject: [PATCH 14/16] feat: update test comments and data provider descriptions for clarity --- tests/Conditions/ArrayContextTest.php | 2 -- tests/Context/FieldSelectorTest.php | 34 +++++++++++++-------------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/tests/Conditions/ArrayContextTest.php b/tests/Conditions/ArrayContextTest.php index 369756f..a388af1 100644 --- a/tests/Conditions/ArrayContextTest.php +++ b/tests/Conditions/ArrayContextTest.php @@ -5,7 +5,6 @@ namespace Tests\Conditions; use Maniaba\RuleEngine\Context\ArrayContext; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use stdClass; @@ -14,7 +13,6 @@ /** * @internal */ -#[CoversClass(ArrayContext::class)] #[Group('Others')] final class ArrayContextTest extends TestCase { diff --git a/tests/Context/FieldSelectorTest.php b/tests/Context/FieldSelectorTest.php index f2b7b3f..a2ae8de 100644 --- a/tests/Context/FieldSelectorTest.php +++ b/tests/Context/FieldSelectorTest.php @@ -182,15 +182,15 @@ public function testEqualityFilter(string $selector, array $expected): void */ public static function provideEqualityFilter(): iterable { - yield 'korisnik s id=2' => ['users[id:2]', ['id' => 2, 'name' => 'Bob', 'age' => 30, 'active' => false]]; + yield 'user with id=2' => ['users[id:2]', ['id' => 2, 'name' => 'Bob', 'age' => 30, 'active' => false]]; - yield 'korisnik s imenom Alice' => ['users[name:Alice]', ['id' => 1, 'name' => 'Alice', 'age' => 25, 'active' => true]]; + yield 'user with name Alice' => ['users[name:Alice]', ['id' => 1, 'name' => 'Alice', 'age' => 25, 'active' => true]]; - yield 'log s level=error' => ['logs[level:error]', ['id' => 2, 'level' => 'error', 'message' => 'Database connection failed.']]; + yield 'log with level=error' => ['logs[level:error]', ['id' => 2, 'level' => 'error', 'message' => 'Database connection failed.']]; } /** - * Testira numerička filtriranja (>, >=, <, <=). + * Tests numeric filtering (>, >=, <, <=). */ #[DataProvider('provideNumericComparisons')] public function testNumericComparisons(string $selector, array $expected): void @@ -200,24 +200,24 @@ public function testNumericComparisons(string $selector, array $expected): void } /** - * Data provider za testiranje filtriranja po numeričkim usporedbama. - * Testiramo `>`, `>=`, `<`, `<=`. + * Data provider for testing filtering by numeric comparisons. + * We test `>`, `>=`, `<`, `<=`. * * @return Generator */ public static function provideNumericComparisons(): iterable { - yield 'prvi korisnik s age>=30' => ['users[age:>=30]', ['id' => 2, 'name' => 'Bob', 'age' => 30, 'active' => false]]; + yield 'first user with age>=30' => ['users[age:>=30]', ['id' => 2, 'name' => 'Bob', 'age' => 30, 'active' => false]]; - yield 'prvi korisnik s age>25' => ['users[age:>25]', ['id' => 2, 'name' => 'Bob', 'age' => 30, 'active' => false]]; + yield 'first user with age>25' => ['users[age:>25]', ['id' => 2, 'name' => 'Bob', 'age' => 30, 'active' => false]]; - yield 'prvi korisnik s age<30' => ['users[age:<30]', ['id' => 1, 'name' => 'Alice', 'age' => 25, 'active' => true]]; + yield 'first user with age<30' => ['users[age:<30]', ['id' => 1, 'name' => 'Alice', 'age' => 25, 'active' => true]]; - yield 'prvi korisnik s age<=35' => ['users[age:<=35]', ['id' => 1, 'name' => 'Alice', 'age' => 25, 'active' => true]]; + yield 'first user with age<=35' => ['users[age:<=35]', ['id' => 1, 'name' => 'Alice', 'age' => 25, 'active' => true]]; } /** - * Testira filtriranje po boolean vrijednostima. + * Tests filtering by boolean values. */ #[DataProvider('provideBooleanFilter')] public function testBooleanFilter(string $selector, array $expected): void @@ -227,21 +227,21 @@ public function testBooleanFilter(string $selector, array $expected): void } /** - * Data provider za testiranje filtriranja po boolean vrijednostima. + * Data provider for testing filtering by boolean values. * * @return Generator */ public static function provideBooleanFilter(): iterable { - yield 'prvi korisnik s active=true' => ['users[active:true]', ['id' => 1, 'name' => 'Alice', 'age' => 25, 'active' => true]]; + yield 'first user with active=true' => ['users[active:true]', ['id' => 1, 'name' => 'Alice', 'age' => 25, 'active' => true]]; - yield 'prvi korisnik s active=false' => ['users[active:false]', ['id' => 2, 'name' => 'Bob', 'age' => 30, 'active' => false]]; + yield 'first user with active=false' => ['users[active:false]', ['id' => 2, 'name' => 'Bob', 'age' => 30, 'active' => false]]; } /** - * Testira dohvaćanje duboko ugniježdenih vrijednosti (chaining). + * Tests retrieving deeply nested values (chaining). * - * Primjer: Dohvaćamo poruku loga s level=error. + * Example: We retrieve the message of a log with level=error. */ public function testChainedSelectors(): void { @@ -262,7 +262,7 @@ public function testInvalidSelectorsThrowExceptions(string $selector): void } /** - * Data provider za negativne slučajeve gdje očekujemo iznimku. + * Data provider for negative cases where we expect an exception. * * @return Generator */ From a8255c1c648eb99365acbf569f313f29490cdafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 18:32:42 +0200 Subject: [PATCH 15/16] feat: improve type checks and update return type annotations for clarity --- psalm.xml | 1 - src/Factories/ActionFactory.php | 2 +- tests/Builders/BuilderTestDemoConfigTrait.php | 2 +- tests/Conditions/CollectionConditionTest.php | 3 ++- tests/Context/FieldSelectorTest.php | 10 +++++----- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/psalm.xml b/psalm.xml index 6136918..47cd59e 100644 --- a/psalm.xml +++ b/psalm.xml @@ -9,7 +9,6 @@ cacheDirectory="build/psalm/" findUnusedBaselineEntry="true" findUnusedCode="false" - errorBaseline="psalm-baseline.xml" > diff --git a/src/Factories/ActionFactory.php b/src/Factories/ActionFactory.php index 3ba15e2..692051f 100644 --- a/src/Factories/ActionFactory.php +++ b/src/Factories/ActionFactory.php @@ -198,7 +198,7 @@ private static function testParameters(array $parameters, array $arguments, stri $argumentType = self::gettype($arguments[$name]); // Check if the type is nullable or matches the argument type - if (! \in_array($parameterTypeName, [null, 'mixed'], true) && (! ($parameterType && $parameterType->allowsNull() && null === $arguments[$name]) && $parameterTypeName !== $argumentType)) { + if (! \in_array($parameterTypeName, [null, 'mixed'], true) && (! ($parameterType !== null && $parameterType->allowsNull() && null === $arguments[$name]) && $parameterTypeName !== $argumentType)) { throw new InvalidArgumentException("Invalid type for argument: '{$name}'. Expected: {$parameterTypeName}, got: " . $argumentType); } } diff --git a/tests/Builders/BuilderTestDemoConfigTrait.php b/tests/Builders/BuilderTestDemoConfigTrait.php index 24bf002..a67f8e4 100644 --- a/tests/Builders/BuilderTestDemoConfigTrait.php +++ b/tests/Builders/BuilderTestDemoConfigTrait.php @@ -166,7 +166,7 @@ protected static function configBuilder(): mixed abstract protected function getBuilder(): RuleSet; /** - * @return array{array, mixed, mixed} + * @return array{CollectionCondition, mixed, mixed} * * @throws ReflectionException */ diff --git a/tests/Conditions/CollectionConditionTest.php b/tests/Conditions/CollectionConditionTest.php index 355b3a3..782083e 100644 --- a/tests/Conditions/CollectionConditionTest.php +++ b/tests/Conditions/CollectionConditionTest.php @@ -92,7 +92,8 @@ public function testInvalidConditionThrowsException(): void $this->expectExceptionMessage('Condition must implement ConditionInterface.'); $invalidCondition = new stdClass(); - $collection = new CollectionCondition(CollectionConditionType::AND, [$invalidCondition]); + // @phpstan-ignore-next-line - Intentionally passing invalid type to test error handling + $collection = new CollectionCondition(CollectionConditionType::AND, [$invalidCondition]); $context = $this->createMockContext([]); $collection->isSatisfied($context); diff --git a/tests/Context/FieldSelectorTest.php b/tests/Context/FieldSelectorTest.php index a2ae8de..0c986e7 100644 --- a/tests/Context/FieldSelectorTest.php +++ b/tests/Context/FieldSelectorTest.php @@ -268,15 +268,15 @@ public function testInvalidSelectorsThrowExceptions(string $selector): void */ public static function provideInvalidSelectorsThrowExceptions(): iterable { - yield 'nepostojeći indeks' => ['users[99]']; + yield 'non-existent index' => ['users[99]']; - yield 'nepostojeći ključ' => ['users[id:9999]']; + yield 'non-existent key' => ['users[id:9999]']; - yield 'neispravan filter' => ['users[invalidFilter]']; // Nepoznat format + yield 'invalid filter' => ['users[invalidFilter]']; // Unknown format - yield 'nepoznati operator' => ['users[id:<>5]']; // Operator <> nije podržan + yield 'unknown operator' => ['users[id:<>5]']; // Operator <> is not supported - yield 'nepostojeći property' => ['nonexistent[key:value]']; + yield 'non-existent property' => ['nonexistent[key:value]']; } public function testDeepNestedAccess(): void From a3bdd914e25a919aa5ff432e0de103aaff6c1930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sat, 19 Jul 2025 18:43:41 +0200 Subject: [PATCH 16/16] feat: update comments for clarity and add phpstan baseline configuration --- phpstan-baseline.neon | 19 +++++++++++++++++++ phpstan.neon.dist | 1 + psalm.xml | 4 ++++ tests/Context/FieldSelectorTest.php | 2 +- .../_support/Actions/DummyArgumentsAction.php | 2 +- 5 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 phpstan-baseline.neon diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..a8e2a5d --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,19 @@ +parameters: + ignoreErrors: + - + message: '#^Parameter \#2 \$action of method Maniaba\\RuleEngine\\Factories\\ActionFactory\:\:registerAction\(\) expects class\-string\\|\(Closure\(Maniaba\\RuleEngine\\Context\\ContextInterface\)\: mixed\)\|Maniaba\\RuleEngine\\Actions\\ActionInterface, Closure\(Maniaba\\RuleEngine\\Context\\ContextInterface, mixed, mixed\=\)\: string given\.$#' + identifier: argument.type + count: 1 + path: tests/Factories/ActionFactoryTest.php + + - + message: '#^Parameter \#2 \$action of method Maniaba\\RuleEngine\\Factories\\ActionFactory\:\:registerAction\(\) expects class\-string\\|\(Closure\(Maniaba\\RuleEngine\\Context\\ContextInterface\)\: mixed\)\|Maniaba\\RuleEngine\\Actions\\ActionInterface, Closure\(Maniaba\\RuleEngine\\Context\\ContextInterface, string, int\)\: non\-falsy\-string given\.$#' + identifier: argument.type + count: 3 + path: tests/Factories/ActionFactoryTest.php + + - + message: '#^Parameter \#2 \$action of method Maniaba\\RuleEngine\\Factories\\ActionFactory\:\:registerAction\(\) expects class\-string\\|\(Closure\(Maniaba\\RuleEngine\\Context\\ContextInterface\)\: mixed\)\|Maniaba\\RuleEngine\\Actions\\ActionInterface, Closure\(Maniaba\\RuleEngine\\Context\\ContextInterface, string, int\=\)\: non\-falsy\-string given\.$#' + identifier: argument.type + count: 1 + path: tests/Factories/ActionFactoryTest.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index fcc4db8..31a47bd 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -17,3 +17,4 @@ parameters: matchingInheritedMethodNames: true treatPhpDocTypesAsCertain: false includes: + - phpstan-baseline.neon diff --git a/psalm.xml b/psalm.xml index 47cd59e..0b86726 100644 --- a/psalm.xml +++ b/psalm.xml @@ -17,4 +17,8 @@ + + + + diff --git a/tests/Context/FieldSelectorTest.php b/tests/Context/FieldSelectorTest.php index 0c986e7..c6e3bc6 100644 --- a/tests/Context/FieldSelectorTest.php +++ b/tests/Context/FieldSelectorTest.php @@ -138,7 +138,7 @@ public function testEvaluateConditionWithStandardValues(): void } /** - * Testira osnovni pristup indeksima u nizu. + * Tests basic index access in arrays. */ #[DataProvider('provideBasicIndexAccess')] public function testBasicIndexAccess(string $selector, array $expected): void diff --git a/tests/_support/Actions/DummyArgumentsAction.php b/tests/_support/Actions/DummyArgumentsAction.php index 4807635..ce6da45 100644 --- a/tests/_support/Actions/DummyArgumentsAction.php +++ b/tests/_support/Actions/DummyArgumentsAction.php @@ -20,7 +20,7 @@ public function __construct(...$arguments) public function execute(ContextInterface $context): bool { - // Obavezno postavi argumente u configu gde se koristi ovaj action + // Make sure to set arguments in the config where this action is used assertEquals(['arguments' => ['arg1', 'arg2']], $this->arguments); return true;