├── test └── Integration │ ├── Yaml │ ├── testNodeAssertStoppable.yaml │ ├── testNodeValue.yaml │ ├── testNodeContextResultFalse.yaml │ ├── testNodeContextResultTrue.yaml │ ├── testNodeContextWithOperatorEqual.yaml │ ├── testNodeContextWithOperatorLowerThan.yaml │ ├── testNodeContextWithOperatorNotEqual.yaml │ ├── testNodeContextWithOperatorGreaterThan.yaml │ ├── testNodeContextWithOperatorStringContain.yaml │ ├── testNodeContextWithOperatorLowerThanOrEqual.yaml │ ├── testNodeContextWithOperatorGreaterThanOrEqual.yaml │ ├── testNodeContextWithOperatorStringNotContain.yaml │ ├── testNodeContextWithOperatorArrayContain.yaml │ ├── testNodeContextWithParams.yaml │ ├── testNodeContextWithOperatorArrayNotContain.yaml │ ├── testNodeContextWithModifiers.yaml │ ├── testNodeContextWithModifiersAndOperator.yaml │ ├── testNodeContextWithOperatorEqualAndContextWithParams.yaml │ ├── testNodeConditionElseCase.yaml │ ├── testNodeConditionThenCase.yaml │ ├── testNodeRootWithStorage.yaml │ ├── testNodeAndCollectionAllFalse.yaml │ ├── testNodeOrCollectionAllFalse.yaml │ ├── testNodeOrCollectionAllTrue.yaml │ ├── testNodeOrCollectionOneTrue.yaml │ ├── testNodeAndCollectionAllTrue.yaml │ ├── testNodeAndCollectionOneFalse.yaml │ ├── testCombined1.yaml │ └── testNodeContextStoppable.yaml │ ├── Json │ ├── testNodeValue.json │ ├── testNodeContextResultFalse.json │ ├── testNodeContextResultTrue.json │ ├── testNodeContextWithOperatorEqual.json │ ├── testNodeContextWithOperatorNotEqual.json │ ├── testNodeContextWithOperatorLowerThan.json │ ├── testNodeContextWithOperatorGreaterThan.json │ ├── testNodeContextWithOperatorStringContain.json │ ├── testNodeContextWithOperatorGreaterThanOrEqual.json │ ├── testNodeContextWithOperatorLowerThanOrEqual.json │ ├── testNodeContextWithOperatorStringNotContain.json │ ├── testNodeContextWithOperatorArrayContain.json │ ├── testNodeContextWithOperatorArrayNotContain.json │ ├── testNodeContextWithModifiers.json │ ├── testNodeContextWithParams.json │ ├── testNodeContextWithModifiersAndOperator.json │ ├── testNodeContextWithOperatorEqualAndContextWithParams.json │ ├── testNodeConditionElseCase.json │ ├── testNodeConditionThenCase.json │ ├── testNodeOrCollectionAllFalse.json │ ├── testNodeAndCollectionAllFalse.json │ ├── testNodeOrCollectionAllTrue.json │ ├── testNodeOrCollectionOneTrue.json │ ├── testNodeAndCollectionAllTrue.json │ ├── testNodeAndCollectionOneFalse.json │ ├── testCombined1.json │ ├── testNodeContextStoppable.json │ └── actions.json │ ├── Contexts │ ├── Action1.php │ ├── Action2.php │ ├── VisitCount.php │ ├── DepositCount.php │ ├── ActionReturnInt.php │ ├── HasVipStatus.php │ ├── InGroup.php │ ├── UtmSource.php │ ├── WithdrawalCount.php │ ├── ActionWithParams.php │ └── ContextWithParams.php │ ├── OperatorsTest.php │ ├── yamlTest.php │ └── jsonTest.php ├── .gitignore ├── .travis.yml ├── src └── the-choice │ ├── Exception │ ├── LogicException.php │ ├── RuntimeException.php │ ├── InvalidArgumentException.php │ ├── InvalidContextCalculation.php │ ├── GeneralException.php │ └── ContainerNotFoundException.php │ ├── Node │ ├── Node.php │ ├── Sortable.php │ ├── AbstractChildNode.php │ ├── Value.php │ ├── AbstractNode.php │ ├── Condition.php │ ├── Root.php │ ├── Collection.php │ └── Context.php │ ├── Context │ ├── ContextInterface.php │ ├── ContextFactoryInterface.php │ ├── CallableContext.php │ └── ContextFactory.php │ ├── Operator │ ├── OperatorResolverInterface.php │ ├── GetValueTrait.php │ ├── SetValueTrait.php │ ├── OperatorInterface.php │ ├── Equal.php │ ├── LowerThan.php │ ├── NotEqual.php │ ├── GreaterThan.php │ ├── LowerThanOrEqual.php │ ├── GreaterThanOrEqual.php │ ├── StringNotContain.php │ ├── StringContain.php │ ├── ArrayContain.php │ ├── ArrayNotContain.php │ ├── OperatorResolver.php │ └── NumericInRange.php │ ├── NodeFactory │ ├── NodeFactoryResolverInterface.php │ ├── NodeFactoryInterface.php │ ├── NodeFactoryResolver.php │ ├── NodeValueFactory.php │ ├── NodeCollectionFactory.php │ ├── NodeRootFactory.php │ ├── NodeConditionFactory.php │ └── NodeContextFactory.php │ ├── Processor │ ├── ProcessorInterface.php │ ├── ProcessorResolverInterface.php │ ├── ValueProcessor.php │ ├── RootProcessor.php │ ├── ProcessorResolver.php │ ├── ConditionProcessor.php │ ├── AbstractProcessor.php │ ├── CollectionProcessor.php │ └── ContextProcessor.php │ ├── Builder │ ├── BuilderInterface.php │ ├── YamlBuilder.php │ ├── JsonBuilder.php │ └── ArrayBuilder.php │ └── Container.php ├── LICENSE ├── composer.json ├── phpunit.xml.dist ├── phpstan.neon ├── rector.php ├── .php-cs-fixer.php └── README.md /test/Integration/Yaml/testNodeAssertStoppable.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/Integration/Yaml/testNodeValue.yaml: -------------------------------------------------------------------------------- 1 | node: value 2 | value: 4 -------------------------------------------------------------------------------- /test/Integration/Json/testNodeValue.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "value", 3 | "value": 4 4 | } -------------------------------------------------------------------------------- /test/Integration/Yaml/testNodeContextResultFalse.yaml: -------------------------------------------------------------------------------- 1 | node: context 2 | contextName: action2 -------------------------------------------------------------------------------- /test/Integration/Yaml/testNodeContextResultTrue.yaml: -------------------------------------------------------------------------------- 1 | node: context 2 | contextName: action1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /var/ 3 | /composer.lock 4 | /build/ 5 | /.idea/ 6 | /.env 7 | /phpunit.xml 8 | -------------------------------------------------------------------------------- /test/Integration/Json/testNodeContextResultFalse.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "context", 3 | "contextName": "action2" 4 | } -------------------------------------------------------------------------------- /test/Integration/Json/testNodeContextResultTrue.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "context", 3 | "contextName": "action1" 4 | } -------------------------------------------------------------------------------- /test/Integration/Yaml/testNodeContextWithOperatorEqual.yaml: -------------------------------------------------------------------------------- 1 | node: context 2 | contextName: depositCount 3 | operator: equal 4 | value: 2 -------------------------------------------------------------------------------- /test/Integration/Yaml/testNodeContextWithOperatorLowerThan.yaml: -------------------------------------------------------------------------------- 1 | node: context 2 | contextName: depositCount 3 | operator: lowerThan 4 | value: 4 -------------------------------------------------------------------------------- /test/Integration/Yaml/testNodeContextWithOperatorNotEqual.yaml: -------------------------------------------------------------------------------- 1 | node: context 2 | contextName: depositCount 3 | operator: notEqual 4 | value: 3 -------------------------------------------------------------------------------- /test/Integration/Yaml/testNodeContextWithOperatorGreaterThan.yaml: -------------------------------------------------------------------------------- 1 | node: context 2 | contextName: depositCount 3 | operator: greaterThan 4 | value: 1 -------------------------------------------------------------------------------- /test/Integration/Yaml/testNodeContextWithOperatorStringContain.yaml: -------------------------------------------------------------------------------- 1 | node: context 2 | contextName: utmSource 3 | operator: stringContain 4 | value: abc -------------------------------------------------------------------------------- /test/Integration/Yaml/testNodeContextWithOperatorLowerThanOrEqual.yaml: -------------------------------------------------------------------------------- 1 | node: context 2 | contextName: depositCount 3 | operator: lowerThanOrEqual 4 | value: 2 -------------------------------------------------------------------------------- /test/Integration/Yaml/testNodeContextWithOperatorGreaterThanOrEqual.yaml: -------------------------------------------------------------------------------- 1 | node: context 2 | contextName: depositCount 3 | operator: greaterThanOrEqual 4 | value: 2 -------------------------------------------------------------------------------- /test/Integration/Yaml/testNodeContextWithOperatorStringNotContain.yaml: -------------------------------------------------------------------------------- 1 | node: context 2 | contextName: depositCount 3 | operator: stringNotContain 4 | value: def -------------------------------------------------------------------------------- /test/Integration/Json/testNodeContextWithOperatorEqual.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "context", 3 | "contextName": "depositCount", 4 | "operator": "equal", 5 | "value": 2 6 | } -------------------------------------------------------------------------------- /test/Integration/Json/testNodeContextWithOperatorNotEqual.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "context", 3 | "contextName": "depositCount", 4 | "operator": "notEqual", 5 | "value": 3 6 | } -------------------------------------------------------------------------------- /test/Integration/Json/testNodeContextWithOperatorLowerThan.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "context", 3 | "contextName": "depositCount", 4 | "operator": "lowerThan", 5 | "value": 4 6 | } -------------------------------------------------------------------------------- /test/Integration/Yaml/testNodeContextWithOperatorArrayContain.yaml: -------------------------------------------------------------------------------- 1 | node: context 2 | contextName: inGroup 3 | operator: arrayContain 4 | value: 5 | - testgroup 6 | - testgroup2 -------------------------------------------------------------------------------- /test/Integration/Yaml/testNodeContextWithParams.yaml: -------------------------------------------------------------------------------- 1 | node: context 2 | contextName: actionWithParams 3 | params: 4 | a: 1 5 | b: test 6 | c: 7 | - 2 8 | - 3 9 | - 4 -------------------------------------------------------------------------------- /test/Integration/Json/testNodeContextWithOperatorGreaterThan.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "context", 3 | "contextName": "depositCount", 4 | "operator": "greaterThan", 5 | "value": 1 6 | } -------------------------------------------------------------------------------- /test/Integration/Yaml/testNodeContextWithOperatorArrayNotContain.yaml: -------------------------------------------------------------------------------- 1 | node: context 2 | contextName: inGroup 3 | operator: arrayNotContain 4 | value: 5 | - testgroup2 6 | - testgroup3 -------------------------------------------------------------------------------- /test/Integration/Json/testNodeContextWithOperatorStringContain.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "context", 3 | "contextName": "utmSource", 4 | "operator": "stringContain", 5 | "value": "abc" 6 | } -------------------------------------------------------------------------------- /test/Integration/Json/testNodeContextWithOperatorGreaterThanOrEqual.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "context", 3 | "contextName": "depositCount", 4 | "operator": "greaterThanOrEqual", 5 | "value": 2 6 | } -------------------------------------------------------------------------------- /test/Integration/Json/testNodeContextWithOperatorLowerThanOrEqual.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "context", 3 | "contextName": "depositCount", 4 | "operator": "lowerThanOrEqual", 5 | "value": 2 6 | } -------------------------------------------------------------------------------- /test/Integration/Json/testNodeContextWithOperatorStringNotContain.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "context", 3 | "contextName": "depositCount", 4 | "operator": "stringNotContain", 5 | "value": "def" 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.2 5 | 6 | before_script: 7 | - curl -s http://getcomposer.org/installer | php 8 | - php composer.phar install --dev 9 | 10 | script: phpunit -------------------------------------------------------------------------------- /src/the-choice/Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | value; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/Integration/Contexts/Action1.php: -------------------------------------------------------------------------------- 1 | value = $value; 14 | 15 | return $this; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/the-choice/NodeFactory/NodeFactoryInterface.php: -------------------------------------------------------------------------------- 1 | root = $root; 14 | 15 | return $this; 16 | } 17 | 18 | public function getRoot(): Root 19 | { 20 | return $this->root; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/the-choice/Context/CallableContext.php: -------------------------------------------------------------------------------- 1 | context = $context; 15 | } 16 | 17 | public function getValue(): mixed 18 | { 19 | return ($this->context)(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/Integration/Yaml/testCombined1.yaml: -------------------------------------------------------------------------------- 1 | node: condition 2 | if: 3 | node: collection 4 | type: and 5 | nodes: 6 | - node: context 7 | contextName: withdrawalCount 8 | operator: equal 9 | value: 0 10 | - node: context 11 | contextName: inGroup 12 | operator: arrayContain 13 | value: 14 | - testgroup 15 | - testgroup2 16 | then: 17 | node: context 18 | contextName: action1 19 | else: 20 | node: context 21 | contextName: action2 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/the-choice/Builder/BuilderInterface.php: -------------------------------------------------------------------------------- 1 | value = $value; 14 | } 15 | 16 | public static function getNodeName(): string 17 | { 18 | return 'value'; 19 | } 20 | 21 | public function getValue(): mixed 22 | { 23 | return $this->value; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/the-choice/Operator/Equal.php: -------------------------------------------------------------------------------- 1 | getValue() === $this->getValue(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/the-choice/Operator/LowerThan.php: -------------------------------------------------------------------------------- 1 | getValue() < $this->getValue(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/the-choice/Operator/NotEqual.php: -------------------------------------------------------------------------------- 1 | getValue() !== $this->getValue(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/the-choice/Processor/ValueProcessor.php: -------------------------------------------------------------------------------- 1 | getValue(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/the-choice/Operator/GreaterThan.php: -------------------------------------------------------------------------------- 1 | getValue() > $this->getValue(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/the-choice/Node/AbstractNode.php: -------------------------------------------------------------------------------- 1 | description = $description; 16 | 17 | return $this; 18 | } 19 | 20 | public function getDescription(): string 21 | { 22 | return $this->description; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/Integration/Yaml/testNodeContextStoppable.yaml: -------------------------------------------------------------------------------- 1 | node: condition 2 | if: 3 | node: collection 4 | type: and 5 | nodes: 6 | - node: context 7 | contextName: withdrawalCount 8 | operator: equal 9 | value: 0 10 | - node: context 11 | contextName: inGroup 12 | operator: arrayContain 13 | value: 14 | - testgroup 15 | - testgroup2 16 | then: 17 | node: collection 18 | type: and 19 | nodes: 20 | - node: context 21 | contextName: actionReturnInt 22 | break: immediately 23 | - node: context 24 | contextName: action2 25 | -------------------------------------------------------------------------------- /src/the-choice/Operator/LowerThanOrEqual.php: -------------------------------------------------------------------------------- 1 | getValue() <= $this->getValue(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/the-choice/Operator/GreaterThanOrEqual.php: -------------------------------------------------------------------------------- 1 | getValue() >= $this->getValue(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/Integration/Contexts/ActionWithParams.php: -------------------------------------------------------------------------------- 1 | a) && 1 === $this->a 19 | && is_string($this->b) && 'test' === $this->b 20 | && is_array($this->c) && isset($this->c[0], $this->c[1], $this->c[2]) && $this->c[0] + $this->c[1] + $this->c[2] === 9; 21 | } 22 | 23 | public function setC($c): void 24 | { 25 | $this->c = $c; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/Integration/Json/testCombined1.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "condition", 3 | "if": { 4 | "node": "collection", 5 | "type": "and", 6 | "nodes": [ 7 | { 8 | "node": "context", 9 | "contextName": "withdrawalCount", 10 | "operator": "equal", 11 | "value": 0 12 | }, 13 | { 14 | "node": "context", 15 | "contextName": "inGroup", 16 | "operator": "arrayContain", 17 | "value": [ 18 | "testgroup", 19 | "testgroup2" 20 | ] 21 | } 22 | ] 23 | }, 24 | "then": { 25 | "node": "context", 26 | "contextName": "action1" 27 | }, 28 | "else": { 29 | "node": "context", 30 | "contextName": "action2" 31 | } 32 | } -------------------------------------------------------------------------------- /src/the-choice/Operator/StringNotContain.php: -------------------------------------------------------------------------------- 1 | getValue()) ? (string)$context->getValue() : ''; 22 | $searchValue = is_scalar($this->getValue()) ? (string)$this->getValue() : ''; 23 | 24 | return !str_contains($contextValue, $searchValue); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/Integration/Contexts/ContextWithParams.php: -------------------------------------------------------------------------------- 1 | a) && 1 === $this->a 18 | && is_string($this->b) && 'test' === $this->b 19 | && is_array($this->c) && isset($this->c[0], $this->c[1], $this->c[2]) && $this->c[0] + $this->c[1] + $this->c[2] === 9) { 20 | return 2; 21 | } 22 | 23 | return 0; 24 | } 25 | 26 | public function setC($c): void 27 | { 28 | $this->c = $c; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/the-choice/Operator/StringContain.php: -------------------------------------------------------------------------------- 1 | getValue(); 22 | $searchValue = $this->getValue(); 23 | 24 | if (!is_string($contextValue) || !is_string($searchValue)) { 25 | return false; 26 | } 27 | 28 | return str_contains($contextValue, $searchValue); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/the-choice/Processor/RootProcessor.php: -------------------------------------------------------------------------------- 1 | getRules(); 20 | 21 | $processor = $this->getProcessorByNode($rules); 22 | if (null === $processor) { 23 | return null; 24 | } 25 | 26 | $result = $processor->process($rules); 27 | if ($node->hasResult()) { 28 | return $node->getResult(); 29 | } 30 | 31 | return $result; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/Integration/Json/testNodeContextStoppable.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "condition", 3 | "if": { 4 | "node": "collection", 5 | "type": "and", 6 | "nodes": [ 7 | { 8 | "node": "context", 9 | "contextName": "withdrawalCount", 10 | "operator": "equal", 11 | "value": 0 12 | }, 13 | { 14 | "node": "context", 15 | "contextName": "inGroup", 16 | "operator": "arrayContain", 17 | "value": [ 18 | "testgroup", 19 | "testgroup2" 20 | ] 21 | } 22 | ] 23 | }, 24 | "then": { 25 | "node": "collection", 26 | "type": "and", 27 | "nodes": [ 28 | { 29 | "node": "context", 30 | "contextName": "actionReturnInt", 31 | "break": "immediately" 32 | }, 33 | { 34 | "node": "context", 35 | "contextName": "action2" 36 | } 37 | ] 38 | } 39 | } -------------------------------------------------------------------------------- /src/the-choice/Builder/YamlBuilder.php: -------------------------------------------------------------------------------- 1 | build($decoded); 21 | } 22 | 23 | public function parseFile(string $filename): mixed 24 | { 25 | if (!file_exists($filename)) { 26 | throw new InvalidArgumentException(sprintf('File "%s" not found', $filename)); 27 | } 28 | 29 | $content = file_get_contents($filename); 30 | if (false === $content) { 31 | throw new InvalidArgumentException(sprintf('File "%s" not found', $filename)); 32 | } 33 | 34 | return $this->parse($content); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/the-choice/Operator/ArrayContain.php: -------------------------------------------------------------------------------- 1 | value = $value; 28 | 29 | return $this; 30 | } 31 | 32 | public function assert(ContextInterface $context): bool 33 | { 34 | return in_array($context->getValue(), $this->getValue(), true); 35 | } 36 | 37 | public function getValue(): array // @phpstan-ignore-line 38 | { 39 | return (array)$this->value; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/the-choice/Operator/ArrayNotContain.php: -------------------------------------------------------------------------------- 1 | value = $value; 28 | 29 | return $this; 30 | } 31 | 32 | public function assert(ContextInterface $context): bool 33 | { 34 | return !in_array($context->getValue(), $this->getValue(), true); 35 | } 36 | 37 | public function getValue(): array // @phpstan-ignore-line 38 | { 39 | return (array)$this->value; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/the-choice/Processor/ProcessorResolver.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public function resolve(Node $node): string 21 | { 22 | return match ($node::class) { 23 | Root::class => RootProcessor::class, 24 | Condition::class => ConditionProcessor::class, 25 | Collection::class => CollectionProcessor::class, 26 | Value::class => ValueProcessor::class, 27 | Context::class => ContextProcessor::class, 28 | 29 | default => throw new InvalidArgumentException( 30 | sprintf('Unknown operator type "%s"', $node::class), 31 | ) 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Prokhorov Alexey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/the-choice/NodeFactory/NodeFactoryResolver.php: -------------------------------------------------------------------------------- 1 | NodeConditionFactory::class, 23 | Context::getNodeName() => NodeContextFactory::class, 24 | Collection::getNodeName() => NodeCollectionFactory::class, 25 | Root::getNodeName() => NodeRootFactory::class, 26 | Value::getNodeName() => NodeValueFactory::class, 27 | 28 | default => throw new InvalidArgumentException( 29 | sprintf('Node type "%s" is not supported.', $nodeType), 30 | ), 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/the-choice/Processor/ConditionProcessor.php: -------------------------------------------------------------------------------- 1 | getProcessorByNode($node->getIfNode()); 20 | if (null !== $processorIf && $processorIf->process($node->getIfNode())) { 21 | $processorThen = $this->getProcessorByNode($node->getThenNode()); 22 | if (null !== $processorThen) { 23 | return $processorThen->process($node->getThenNode()); 24 | } 25 | } 26 | 27 | if (null !== $node->getElseNode()) { 28 | $processorElse = $this->getProcessorByNode($node->getElseNode()); 29 | if (null !== $processorElse) { 30 | return $processorElse->process($node->getElseNode()); 31 | } 32 | } 33 | 34 | return false; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/the-choice/Node/Condition.php: -------------------------------------------------------------------------------- 1 | ifNode = $ifNode; 20 | $this->thenNode = $thenNode; 21 | $this->elseNode = $elseNode; 22 | } 23 | 24 | public static function getNodeName(): string 25 | { 26 | return 'condition'; 27 | } 28 | 29 | public function setPriority(int $priority): static 30 | { 31 | $this->priority = $priority; 32 | 33 | return $this; 34 | } 35 | 36 | public function getIfNode(): Node 37 | { 38 | return $this->ifNode; 39 | } 40 | 41 | public function getThenNode(): Node 42 | { 43 | return $this->thenNode; 44 | } 45 | 46 | public function getElseNode(): ?Node 47 | { 48 | return $this->elseNode; 49 | } 50 | 51 | public function getSortableValue(): int 52 | { 53 | return $this->priority; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/the-choice/NodeFactory/NodeValueFactory.php: -------------------------------------------------------------------------------- 1 | setRoot($builder->getRoot()); 20 | 21 | if (self::nodeHasDescription($structure)) { 22 | $description = $structure['description']; 23 | if (is_string($description)) { 24 | $node->setDescription($description); 25 | } 26 | } 27 | 28 | return $node; 29 | } 30 | 31 | private static function validate(array $structure): void 32 | { 33 | if (!array_key_exists('value', $structure)) { 34 | throw new LogicException('The "value" property is absent in node type "Value"!'); 35 | } 36 | } 37 | 38 | private static function nodeHasDescription(array $structure): bool 39 | { 40 | return array_key_exists('description', $structure); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prohalexey/the-choice", 3 | "license": "MIT", 4 | "minimum-stability": "dev", 5 | "prefer-stable": true, 6 | "authors": [ 7 | { 8 | "name": "Prokhorov Alexey", 9 | "email": "noemailcontact@example.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=8.0", 14 | "chriskonnertz/string-calc": "^2.0", 15 | "symfony/yaml": "^7.3", 16 | "ext-json": "*", 17 | "ext-mbstring": "*" 18 | }, 19 | "require-dev": { 20 | "friendsofphp/php-cs-fixer": "^v3.68", 21 | "phan/phan": "^5.4", 22 | "phpstan/phpstan": "^2.1", 23 | "phpstan/phpstan-phpunit": "^2.0", 24 | "phpstan/phpstan-strict-rules": "^2.0", 25 | "phpunit/phpunit": "^12.0.2", 26 | "psr/container": "^2.0.0", 27 | "rector/rector": "^2.0", 28 | "rector/swiss-knife": "^2.1.7", 29 | "roave/security-advisories": "dev-latest", 30 | "thecodingmachine/phpstan-safe-rule": "^1.3" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "TheChoice\\": "src/the-choice/", 35 | "TheChoice\\Tests\\Integration\\": "test/Integration/" 36 | } 37 | }, 38 | "config": { 39 | "optimize-autoloader": true, 40 | "sort-packages": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/the-choice/Processor/AbstractProcessor.php: -------------------------------------------------------------------------------- 1 | */ 15 | protected array $processorResolvingCache = []; 16 | 17 | public function setContainer(ContainerInterface $container): void 18 | { 19 | $this->container = $container; 20 | } 21 | 22 | public function getContainer(): ContainerInterface 23 | { 24 | return $this->container; 25 | } 26 | 27 | public function getProcessorByNode(Node $node): ?self 28 | { 29 | $nodeType = $node::class; 30 | if (!array_key_exists($nodeType, $this->processorResolvingCache)) { 31 | /** @var ProcessorResolverInterface $processorResolver */ 32 | $processorResolver = $this->getContainer()->get(ProcessorResolverInterface::class); 33 | $this->processorResolvingCache[$nodeType] = $processorResolver->resolve($node); 34 | } 35 | 36 | // @phpstan-ignore-next-line 37 | return $this->getContainer()->get($this->processorResolvingCache[$nodeType]); 38 | } 39 | 40 | abstract public function process(Node $node): mixed; 41 | } 42 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ./test 25 | 26 | 27 | 28 | 29 | 30 | src 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/the-choice/Builder/JsonBuilder.php: -------------------------------------------------------------------------------- 1 | build($decoded); 28 | } 29 | 30 | public function parseFile(string $filename, int $maxDepth = 512, int $options = 0): mixed 31 | { 32 | if (!file_exists($filename)) { 33 | throw new InvalidArgumentException(sprintf('File "%s" not found', $filename)); 34 | } 35 | 36 | $content = file_get_contents($filename); 37 | if (false === $content) { 38 | throw new InvalidArgumentException(sprintf('File "%s" not found', $filename)); 39 | } 40 | 41 | return $this->parse($content, $maxDepth, $options); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/the-choice/Operator/OperatorResolver.php: -------------------------------------------------------------------------------- 1 | ArrayContain::class, 18 | ArrayNotContain::getOperatorName() => ArrayNotContain::class, 19 | Equal::getOperatorName() => Equal::class, 20 | GreaterThan::getOperatorName() => GreaterThan::class, 21 | GreaterThanOrEqual::getOperatorName() => GreaterThanOrEqual::class, 22 | LowerThan::getOperatorName() => LowerThan::class, 23 | LowerThanOrEqual::getOperatorName() => LowerThanOrEqual::class, 24 | NotEqual::getOperatorName() => NotEqual::class, 25 | NumericInRange::getOperatorName() => NumericInRange::class, 26 | StringContain::getOperatorName() => StringContain::class, 27 | StringNotContain::getOperatorName() => StringNotContain::class, 28 | 29 | default => throw new InvalidArgumentException( 30 | sprintf('Operator "%s" is not supported.', $operatorType), 31 | ) 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | tmpDir: ./var/phpstan 3 | checkMissingTypehints: true 4 | checkMissingVarTagTypehint: true 5 | parallel: 6 | jobSize: 20 7 | maximumNumberOfProcesses: 32 8 | minimumNumberOfJobsPerProcess: 2 9 | level: 9 10 | paths: 11 | - ./src 12 | ignoreErrors: 13 | - 14 | identifier: missingType.iterableValue 15 | treatPhpDocTypesAsCertain: false 16 | strictRules: 17 | allRules: true 18 | disallowedLooseComparison: true 19 | booleansInConditions: false 20 | uselessCast: true 21 | requireParentConstructorCall: true 22 | disallowedBacktick: true 23 | disallowedEmpty: false 24 | disallowedImplicitArrayCreation: true 25 | disallowedShortTernary: false 26 | overwriteVariablesWithLoop: true 27 | closureUsesThis: true 28 | matchingInheritedMethodNames: true 29 | numericOperandsInArithmeticOperators: true 30 | strictFunctionCalls: false 31 | dynamicCallOnStaticMethod: true 32 | switchConditionsMatchingType: true 33 | noVariableVariables: false 34 | strictArrayFilter: false 35 | illegalConstructorMethodCall: true 36 | 37 | checkExplicitMixedMissingReturn: true 38 | checkDynamicProperties: false 39 | 40 | includes: 41 | - ./vendor/phpstan/phpstan-phpunit/extension.neon 42 | - ./vendor/phpstan/phpstan-strict-rules/rules.neon 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/the-choice/Processor/CollectionProcessor.php: -------------------------------------------------------------------------------- 1 | getRoot(); 22 | 23 | foreach ($node->sort()->all() as $item) { 24 | $processor = $this->getProcessorByNode($item); 25 | if (null === $processor) { 26 | continue; 27 | } 28 | 29 | $result = $processor->process($item); 30 | 31 | /* 32 | * If the "Root" node has a result, we should stop here. 33 | * It does not matter what we return; the result is already set to the "Root" node 34 | */ 35 | if ($rootNode->hasResult()) { 36 | return null; 37 | } 38 | 39 | if (false === $result && Collection::TYPE_AND === $node->getType()) { 40 | return false; 41 | } 42 | 43 | if (true === $result && Collection::TYPE_OR === $node->getType()) { 44 | return true; 45 | } 46 | } 47 | 48 | return $result; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/the-choice/Operator/NumericInRange.php: -------------------------------------------------------------------------------- 1 | value = $value; 35 | 36 | return $this; 37 | } 38 | 39 | public function assert(ContextInterface $context): bool 40 | { 41 | $contextValue = $context->getValue(); 42 | 43 | $value = $this->getValue(); 44 | if (!is_array($value) || 2 !== count($value)) { 45 | return false; 46 | } 47 | 48 | [$leftBoundary, $rightBoundary] = $value; 49 | 50 | if ($leftBoundary > $rightBoundary) { 51 | return $contextValue >= $rightBoundary && $contextValue <= $leftBoundary; 52 | } 53 | 54 | return $contextValue >= $leftBoundary && $contextValue <= $rightBoundary; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/the-choice/Node/Root.php: -------------------------------------------------------------------------------- 1 | rules; 25 | } 26 | 27 | public function setRules(Node $node): self 28 | { 29 | $this->rules = $node; 30 | 31 | return $this; 32 | } 33 | 34 | public function getResult(): mixed 35 | { 36 | return $this->result; 37 | } 38 | 39 | public function hasResult(): bool 40 | { 41 | return null !== $this->result; 42 | } 43 | 44 | public function setResult(mixed $result): self 45 | { 46 | $this->result = $result; 47 | 48 | return $this; 49 | } 50 | 51 | public function getStorage(): array 52 | { 53 | return $this->storage; 54 | } 55 | 56 | public function getStorageValue(string $name): mixed 57 | { 58 | return $this->storage[$name] ?? null; 59 | } 60 | 61 | public function setGlobal(string $key, mixed $value): mixed 62 | { 63 | if (!preg_match('#[a-z][a-z0-9_]+#i', $key)) { 64 | throw new InvalidArgumentException( 65 | 'The key in "storage" property of node type "Root" must be string(format: #[a-z][a-z0-9_]+#i)', 66 | ); 67 | } 68 | 69 | if ('context' === $key) { 70 | throw new InvalidArgumentException( 71 | 'The key "context" for root context is reserved and cannot be used', 72 | ); 73 | } 74 | 75 | return $this->storage[$key] = $value; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/Integration/Json/actions.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "condition", 3 | "if": { 4 | "node": "context", 5 | "contextName": "depositCount", 6 | "operator": "greaterThanOrEqual", 7 | "value": 2 8 | }, 9 | "then": { 10 | "node": "condition", 11 | "if": { 12 | "node": "collection", 13 | "type": "or", 14 | "nodes": [ 15 | { 16 | "node": "context", 17 | "contextName": "withdrawalCount", 18 | "operator": "equal", 19 | "value": 0 20 | }, 21 | { 22 | "node": "context", 23 | "contextName": "hasVipStatus", 24 | "operator": "equal", 25 | "value": true 26 | } 27 | ] 28 | }, 29 | "then": { 30 | "node": "context", 31 | "contextName": "actionName1" 32 | }, 33 | "else": { 34 | "node": "condition", 35 | "if": { 36 | "node": "collection", 37 | "type": "and", 38 | "nodes": [ 39 | { 40 | "node": "context", 41 | "contextName": "withdrawalCount", 42 | "operator": "equal", 43 | "value": 0 44 | }, 45 | { 46 | "node": "collection", 47 | "type": "or", 48 | "nodes": [ 49 | { 50 | "node": "context", 51 | "contextName": "withdrawalCount", 52 | "operator": "equal", 53 | "value": 0 54 | }, 55 | { 56 | "node": "context", 57 | "contextName": "hasVipStatus", 58 | "operator": "equal", 59 | "value": true 60 | } 61 | ] 62 | } 63 | ] 64 | }, 65 | "then": { 66 | "node": "context", 67 | "contextName": "actionName2" 68 | } 69 | } 70 | }, 71 | "else": { 72 | "node": "context", 73 | "contextName": "actionName3" 74 | } 75 | } -------------------------------------------------------------------------------- /src/the-choice/Node/Collection.php: -------------------------------------------------------------------------------- 1 | */ 18 | protected array $collection = []; 19 | 20 | protected int $priority = 0; 21 | 22 | public function __construct(string $type) 23 | { 24 | if (!in_array($type, [self::TYPE_AND, self::TYPE_OR], true)) { 25 | throw new LogicException(sprintf('Collection type must be "or" or "and". "%s" given', $type)); 26 | } 27 | 28 | $this->type = $type; 29 | } 30 | 31 | public static function getNodeName(): string 32 | { 33 | return 'collection'; 34 | } 35 | 36 | public function getType(): string 37 | { 38 | return $this->type; 39 | } 40 | 41 | public function add(Node $element): self 42 | { 43 | $this->collection[] = $element; 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * @return array 50 | */ 51 | public function all(): array 52 | { 53 | return $this->collection; 54 | } 55 | 56 | public function setPriority(int $priority): self 57 | { 58 | $this->priority = $priority; 59 | 60 | return $this; 61 | } 62 | 63 | public function sort(): self 64 | { 65 | usort($this->collection, static function ($element1, $element2): int { 66 | if (!$element2 instanceof Sortable) { 67 | return 1; 68 | } 69 | 70 | if (!$element1 instanceof Sortable) { 71 | return -1; 72 | } 73 | 74 | return $element1->getSortableValue() <=> $element2->getSortableValue(); 75 | }); 76 | 77 | return $this; 78 | } 79 | 80 | public function getSortableValue(): int 81 | { 82 | return $this->priority; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/the-choice/NodeFactory/NodeCollectionFactory.php: -------------------------------------------------------------------------------- 1 | setRoot($builder->getRoot()); 22 | 23 | if (self::nodeHasDescription($structure)) { 24 | $description = $structure['description']; 25 | if (is_string($description)) { 26 | $node->setDescription($description); 27 | } 28 | } 29 | 30 | if (self::nodeHasPriority($structure)) { 31 | $priority = $structure['priority']; 32 | if (is_numeric($priority)) { 33 | $node->setPriority((int)$priority); 34 | } 35 | } 36 | 37 | if (self::nodeHasChildNodes($structure)) { 38 | $nodes = $structure['nodes']; 39 | if (is_array($nodes)) { 40 | foreach ($nodes as $element) { 41 | if (is_array($element)) { 42 | $builtNode = $builder->build($element); 43 | $node->add($builtNode); 44 | } 45 | } 46 | } 47 | } 48 | 49 | return $node; 50 | } 51 | 52 | private static function nodeHasDescription(array $structure): bool 53 | { 54 | return array_key_exists('description', $structure); 55 | } 56 | 57 | private static function nodeHasPriority(array $structure): bool 58 | { 59 | return array_key_exists('priority', $structure); 60 | } 61 | 62 | private static function nodeHasChildNodes(array $structure): bool 63 | { 64 | return array_key_exists('nodes', $structure) && is_array($structure['nodes']); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/the-choice/NodeFactory/NodeRootFactory.php: -------------------------------------------------------------------------------- 1 | setRoot($rootNode); 19 | 20 | if (self::nodeHasDescription($structure)) { 21 | $description = $structure['description']; 22 | if (is_string($description)) { 23 | $rootNode->setDescription($description); 24 | } 25 | } 26 | 27 | if (self::nodeHasStorage($structure)) { 28 | $storage = $structure['storage']; 29 | if (is_array($storage)) { 30 | foreach ($storage as $key => $value) { 31 | if (is_string($key)) { 32 | $rootNode->setGlobal($key, $value); 33 | } 34 | } 35 | } 36 | } 37 | 38 | if (!self::nodeHasRules($structure)) { 39 | throw new LogicException('The "rules" property is absent in node type "Root"!'); 40 | } 41 | 42 | $rulesStructure = $structure['rules']; 43 | if (!is_array($rulesStructure)) { 44 | throw new InvalidArgumentException('Rules structure must be an array'); 45 | } 46 | 47 | $rules = $builder->build($rulesStructure); 48 | $rootNode->setRules($rules); 49 | 50 | return $rootNode; 51 | } 52 | 53 | private static function nodeHasDescription(array $structure): bool 54 | { 55 | return array_key_exists('description', $structure); 56 | } 57 | 58 | private static function nodeHasStorage(array $structure): bool 59 | { 60 | return array_key_exists('storage', $structure); 61 | } 62 | 63 | private static function nodeHasRules(array $structure): bool 64 | { 65 | return array_key_exists('rules', $structure) && is_array($structure['rules']); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/the-choice/Builder/ArrayBuilder.php: -------------------------------------------------------------------------------- 1 | container = $container; 26 | } 27 | 28 | public function build(array &$structure): Node 29 | { 30 | if (!array_key_exists('node', $structure)) { 31 | throw new InvalidArgumentException('The "node" property is absent!'); 32 | } 33 | 34 | $this->nodesCount++; 35 | 36 | // Workaround for short syntax if the root node is omitted 37 | if (1 === $this->nodesCount && $structure['node'] !== Root::getNodeName()) { 38 | $structure = [ 39 | 'node' => Root::getNodeName(), 40 | 'rules' => $structure, 41 | ]; 42 | 43 | $this->nodesCount--; 44 | 45 | return $this->build($structure); 46 | } 47 | 48 | if (!is_string($structure['node'])) { 49 | throw new InvalidArgumentException('Node type must be a string'); 50 | } 51 | 52 | if (1 !== $this->nodesCount && $structure['node'] === Root::getNodeName()) { 53 | throw new LogicException('The node "Root" cannot be not root node!'); 54 | } 55 | 56 | /** @var NodeFactoryResolverInterface $nodeFactoryResolver */ 57 | $nodeFactoryResolver = $this->container->get(NodeFactoryResolverInterface::class); 58 | $nodeFactoryType = $nodeFactoryResolver->resolve($structure['node']); 59 | 60 | /** @var NodeFactoryInterface $nodeFactory */ 61 | $nodeFactory = $this->container->get($nodeFactoryType); 62 | 63 | return $nodeFactory->build($this, $structure); 64 | } 65 | 66 | public function getContainer(): ContainerInterface 67 | { 68 | return $this->container; 69 | } 70 | 71 | public function setRoot(Root $rootNode): BuilderInterface 72 | { 73 | $this->rootNode = $rootNode; 74 | 75 | return $this; 76 | } 77 | 78 | public function getRoot(): Root 79 | { 80 | return $this->rootNode; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/the-choice/NodeFactory/NodeConditionFactory.php: -------------------------------------------------------------------------------- 1 | build($ifStructure); 27 | $thenNode = $builder->build($thenStructure); 28 | $elseNode = null !== $elseStructure ? $builder->build($elseStructure) : null; 29 | 30 | $node = new Condition($ifNode, $thenNode, $elseNode); 31 | $node->setRoot($builder->getRoot()); 32 | 33 | if (self::nodeHasDescription($structure)) { 34 | $description = $structure['description']; 35 | if (is_string($description)) { 36 | $node->setDescription($description); 37 | } 38 | } 39 | 40 | if (self::nodeHasPriority($structure)) { 41 | $priority = $structure['priority']; 42 | if (is_numeric($priority)) { 43 | $node->setPriority((int)$priority); 44 | } 45 | } 46 | 47 | return $node; 48 | } 49 | 50 | private static function validate(array $structure): void 51 | { 52 | $keysThatMustBePresent = [ 53 | 'if', 54 | 'then', 55 | ]; 56 | 57 | foreach ($keysThatMustBePresent as $key) { 58 | if (!array_key_exists($key, $structure)) { 59 | throw new LogicException(sprintf('The "%s" property is absent in node type "Condition"!', $key)); 60 | } 61 | } 62 | } 63 | 64 | private static function nodeHasElseBranch(array $structure): bool 65 | { 66 | return array_key_exists('else', $structure); 67 | } 68 | 69 | private static function nodeHasDescription(array $structure): bool 70 | { 71 | return array_key_exists('description', $structure); 72 | } 73 | 74 | private static function nodeHasPriority(array $structure): bool 75 | { 76 | return array_key_exists('priority', $structure); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/the-choice/Node/Context.php: -------------------------------------------------------------------------------- 1 | */ 26 | protected array $modifiers = []; 27 | 28 | public static function getNodeName(): string 29 | { 30 | return 'context'; 31 | } 32 | 33 | public function getSortableValue(): int 34 | { 35 | return $this->priority; 36 | } 37 | 38 | public function setPriority(int $priority): static 39 | { 40 | $this->priority = $priority; 41 | 42 | return $this; 43 | } 44 | 45 | public function getOperator(): ?OperatorInterface 46 | { 47 | return $this->operator; 48 | } 49 | 50 | public function getContextName(): ?string 51 | { 52 | return $this->contextName; 53 | } 54 | 55 | public function setContextName(string $contextName): static 56 | { 57 | $this->contextName = $contextName; 58 | 59 | return $this; 60 | } 61 | 62 | public function setOperator(OperatorInterface $operator): static 63 | { 64 | $this->operator = $operator; 65 | 66 | return $this; 67 | } 68 | 69 | public function getStoppableType(): ?string 70 | { 71 | return $this->stoppableType; 72 | } 73 | 74 | public function setStoppableType(?string $type): static 75 | { 76 | if (self::STOP_IMMEDIATELY !== $type) { 77 | throw new LogicException(sprintf('Stoppable type must be one of (%s). "%s" given', implode(', ', static::getStopModes()), $type)); 78 | } 79 | 80 | $this->stoppableType = $type; 81 | 82 | return $this; 83 | } 84 | 85 | public function isStoppable(): bool 86 | { 87 | return null !== $this->stoppableType; 88 | } 89 | 90 | public function getParams(): array 91 | { 92 | return $this->params; 93 | } 94 | 95 | public function setParams(array $params): void 96 | { 97 | $this->params = $params; 98 | } 99 | 100 | /** 101 | * @return array 102 | */ 103 | public function getModifiers(): array 104 | { 105 | return $this->modifiers; 106 | } 107 | 108 | public function setModifiers(array $modifiers): void 109 | { 110 | if (false === $this->checkModifiers($modifiers)) { 111 | throw new InvalidArgumentException('Context modifier must be string type'); 112 | } 113 | 114 | /** @var array $modifiers */ 115 | $this->modifiers = $modifiers; 116 | } 117 | 118 | public static function getStopModes(): array 119 | { 120 | return [self::STOP_IMMEDIATELY]; 121 | } 122 | 123 | private function checkModifiers(array $modifiers): bool 124 | { 125 | return array_reduce($modifiers, static fn ($carry, $modifier) => $carry && is_string($modifier), true); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/the-choice/NodeFactory/NodeContextFactory.php: -------------------------------------------------------------------------------- 1 | setRoot($builder->getRoot()); 19 | 20 | if (self::nodeHasOperator($structure)) { 21 | $operatorResolver = $builder->getContainer()->get(OperatorResolverInterface::class); 22 | if ($operatorResolver instanceof OperatorResolverInterface) { 23 | $operatorType = $structure['operator']; 24 | if (is_string($operatorType)) { 25 | $operatorType = $operatorResolver->resolve($operatorType); 26 | $operator = $builder->getContainer()->get($operatorType); 27 | if ($operator instanceof OperatorInterface) { 28 | $operator->setValue($structure['value']); 29 | $node->setOperator($operator); 30 | } 31 | } 32 | } 33 | } 34 | 35 | if (self::nodeHasContextName($structure)) { 36 | $contextName = $structure['contextName']; 37 | if (is_string($contextName)) { 38 | $node->setContextName($contextName); 39 | } 40 | } 41 | 42 | if (self::nodeHasDescription($structure)) { 43 | $description = $structure['description']; 44 | if (is_string($description)) { 45 | $node->setDescription($description); 46 | } 47 | } 48 | 49 | if (self::nodeHasPriority($structure)) { 50 | $priority = $structure['priority']; 51 | if (is_numeric($priority)) { 52 | $node->setPriority((int)$priority); 53 | } 54 | } 55 | 56 | if (self::nodeHasParams($structure)) { 57 | $params = $structure['params']; 58 | if (is_array($params)) { 59 | $node->setParams($params); 60 | } 61 | } 62 | 63 | if (self::nodeHasModifiers($structure)) { 64 | $node->setModifiers($structure['modifiers']); 65 | } 66 | 67 | if (self::isNodeStoppable($structure)) { 68 | $node->setStoppableType(Context::STOP_IMMEDIATELY); 69 | } 70 | 71 | return $node; 72 | } 73 | 74 | private static function nodeHasDescription(array $structure): bool 75 | { 76 | return array_key_exists('description', $structure); 77 | } 78 | 79 | private static function nodeHasPriority(array $structure): bool 80 | { 81 | return array_key_exists('priority', $structure); 82 | } 83 | 84 | private static function nodeHasParams(array $structure): bool 85 | { 86 | return array_key_exists('params', $structure); 87 | } 88 | 89 | private static function nodeHasModifiers(array $structure): bool 90 | { 91 | return array_key_exists('modifiers', $structure); 92 | } 93 | 94 | private static function isNodeStoppable(array $structure): bool 95 | { 96 | return array_key_exists('break', $structure); 97 | } 98 | 99 | private static function nodeHasOperator(array $structure): bool 100 | { 101 | return array_key_exists('operator', $structure) && array_key_exists('value', $structure); 102 | } 103 | 104 | private static function nodeHasContextName(array $structure): bool 105 | { 106 | return array_key_exists('contextName', $structure); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/the-choice/Context/ContextFactory.php: -------------------------------------------------------------------------------- 1 | contextMap = $contexts; 20 | } 21 | 22 | public function setContainer(ContainerInterface $container): void 23 | { 24 | $this->container = $container; 25 | } 26 | 27 | public function createContextFromContextNode(Context $node): ContextInterface 28 | { 29 | $contextName = $node->getContextName(); 30 | if (null === $contextName) { 31 | throw new InvalidArgumentException('Context name cannot be null'); 32 | } 33 | 34 | $context = $this->getContext($contextName); 35 | 36 | return $this->setParamsToObject($context, $node->getParams()); 37 | } 38 | 39 | private function getContext(string $contextType): ContextInterface 40 | { 41 | if (!array_key_exists($contextType, $this->contextMap)) { 42 | throw new InvalidArgumentException(sprintf('Context type "%s" not found', $contextType)); 43 | } 44 | 45 | $context = $this->contextMap[$contextType]; 46 | 47 | if (is_object($context)) { 48 | if (!$context instanceof ContextInterface) { 49 | throw new InvalidArgumentException( 50 | sprintf('Object "%s" not implements ContextInterface', $context::class), 51 | ); 52 | } 53 | 54 | return $context; 55 | } 56 | 57 | if (is_string($context)) { 58 | return $this->getContextFromString($context); 59 | } 60 | 61 | if (is_callable($context)) { 62 | return new CallableContext($context); 63 | } 64 | 65 | throw new InvalidArgumentException(sprintf('Unknown context type "%s"', $contextType)); 66 | } 67 | 68 | private function setParamsToObject(object $object, array $params): ContextInterface 69 | { 70 | foreach ($params as $paramName => $paramValue) { 71 | $commonSetterName = sprintf('set%s', ucfirst($paramName)); 72 | if (method_exists($object, $commonSetterName)) { 73 | $object->{$commonSetterName}($paramValue); 74 | } elseif (property_exists($object, $paramName)) { 75 | $object->{$paramName} = $paramValue; 76 | } else { 77 | trigger_error(vsprintf('Object %s does not have public property %s or %s setter', [ 78 | $object::class, 79 | $paramName, 80 | $commonSetterName, 81 | ])); 82 | } 83 | } 84 | 85 | if (!$object instanceof ContextInterface) { 86 | throw new InvalidArgumentException('Object must implement ContextInterface'); 87 | } 88 | 89 | return $object; 90 | } 91 | 92 | private function getContextFromString(string $context): ContextInterface 93 | { 94 | if (null !== $this->container && $this->container->has($context)) { 95 | $result = $this->container->get($context); 96 | if (!$result instanceof ContextInterface) { 97 | throw new InvalidArgumentException('Container returned object that does not implement ContextInterface'); 98 | } 99 | 100 | return $result; 101 | } 102 | 103 | if (class_exists($context)) { 104 | $result = new $context(); 105 | if (!$result instanceof ContextInterface) { 106 | throw new InvalidArgumentException('Class does not implement ContextInterface'); 107 | } 108 | 109 | return $result; 110 | } 111 | 112 | throw new InvalidArgumentException(sprintf('Cannot find "%s" context', $context)); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/the-choice/Processor/ContextProcessor.php: -------------------------------------------------------------------------------- 1 | contextFactory = $contextFactory; 27 | 28 | return $this; 29 | } 30 | 31 | public function process(Node $node): mixed 32 | { 33 | if (!$node instanceof Context) { 34 | throw new InvalidArgumentException('Node must be an instance of Context'); 35 | } 36 | 37 | if (null === $this->contextFactory) { 38 | throw new RuntimeException('Context factory not configured'); 39 | } 40 | 41 | $hash = [ 42 | $node->getContextName(), 43 | ]; 44 | 45 | $params = $node->getParams(); 46 | if ([] !== $params) { 47 | $hash[] = hash('md5', serialize($params)); 48 | } 49 | 50 | $operator = $node->getOperator(); 51 | if (null !== $operator) { 52 | /** @var OperatorInterface $operator */ 53 | $operatorValue = $operator->getValue(); 54 | 55 | $hash[] = $operator::class; 56 | $hash[] = is_array($operatorValue) || is_object($operatorValue) 57 | ? hash('md5', serialize($operatorValue)) 58 | : $operatorValue; 59 | } 60 | 61 | $modifiers = $node->getModifiers(); 62 | if ([] !== $modifiers) { 63 | $hash[] = hash('md5', serialize($modifiers)); 64 | } 65 | 66 | $hash = implode('', $hash); 67 | 68 | if (!isset($this->processedContext[$hash])) { 69 | $context = $this->contextFactory->createContextFromContextNode($node); 70 | 71 | if (null !== $operator) { 72 | if ([] !== $modifiers) { 73 | $context = new CallableContext( 74 | fn (): mixed => $this->processContextModifiers($context->getValue(), $node), 75 | ); 76 | } 77 | 78 | $this->processedContext[$hash] = $operator->assert($context); 79 | } else { 80 | $this->processedContext[$hash] = $this->processContextModifiers($context->getValue(), $node); 81 | } 82 | } 83 | 84 | if ($node->isStoppable()) { 85 | $node->getRoot()->setResult($this->processedContext[$hash]); 86 | 87 | /* 88 | * If the "Root" node has a result, we should stop here. 89 | * It does not matter what we return; the result is already set to the "Root" node 90 | */ 91 | if (Context::STOP_IMMEDIATELY === $node->getStoppableType()) { 92 | return null; 93 | } 94 | } 95 | 96 | return $this->processedContext[$hash]; 97 | } 98 | 99 | private function processContextModifiers(mixed $value, Context $node): mixed 100 | { 101 | $vars = ['$context' => $value]; 102 | 103 | $storage = $node->getRoot()->getStorage(); 104 | $vars = array_merge($vars, $storage); 105 | 106 | foreach ($node->getModifiers() as $modifier) { 107 | $search = array_keys($vars); 108 | $replace = array_values($vars); 109 | /** @var array $search */ 110 | /** @var array $replace */ 111 | $modifier = str_replace($search, $replace, $modifier); 112 | 113 | try { 114 | $value = (new StringCalc())->calculate($modifier); 115 | } catch (Exception $exception) { 116 | throw new InvalidContextCalculation($exception->getMessage()); 117 | } 118 | 119 | $vars['$context'] = $value; 120 | } 121 | 122 | return $value; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 44 | __DIR__ . '/src', 45 | __DIR__ . '/test', 46 | ]) 47 | ->withPreparedSets( 48 | codeQuality: true, 49 | codingStyle: true, 50 | typeDeclarations: true, 51 | phpunitCodeQuality: true, 52 | doctrineCodeQuality: true, 53 | symfonyCodeQuality: true, 54 | symfonyConfigs: true, 55 | ) 56 | ->withComposerBased( 57 | twig: true, 58 | doctrine: true, 59 | phpunit: true, 60 | symfony: true, 61 | ) 62 | ->withCache(__DIR__ . '/var/rector') 63 | ->withParallel(240) 64 | ->withPhpVersion(PhpVersion::PHP_80) 65 | ->withSkip([ 66 | ActionSuffixRemoverRector::class, 67 | CatchExceptionNameMatchingTypeRector::class, 68 | ChangeSwitchToMatchRector::class, 69 | ClassPropertyAssignToConstructorPromotionRector::class, 70 | CombineIfRector::class, 71 | CompactToVariablesRector::class, 72 | ConstraintOptionsToNamedArgumentsRector::class, 73 | DisallowedEmptyRuleFixerRector::class, 74 | EncapsedStringsToSprintfRector::class, 75 | ExplicitBoolCompareRector::class, 76 | FlipTypeControlToUseExclusiveTypeRector::class, 77 | ForeachToArrayAnyRector::class, 78 | ForeachToArrayFindRector::class, 79 | GetFiltersToAsTwigFilterAttributeRector::class, 80 | GetFunctionsToAsTwigFunctionAttributeRector::class, 81 | InlineClassRoutePrefixRector::class, 82 | InlineConstructorDefaultToPropertyRector::class, 83 | InvokableCommandInputAttributeRector::class, 84 | LocallyCalledStaticMethodToNonStaticRector::class, 85 | MakeInheritedMethodVisibilitySameAsParentRector::class, 86 | NullToStrictStringFuncCallArgRector::class, 87 | PreferPHPUnitThisCallRector::class, 88 | RemoveDataProviderParamKeysRector::class, 89 | RenameClassConstFetchRector::class, 90 | RestoreDefaultNullToNullableTypePropertyRector::class, 91 | SimplifyIfElseToTernaryRector::class, 92 | ThrowWithPreviousExceptionRector::class, 93 | YieldDataProviderRector::class, 94 | ]) 95 | ->withSets([ 96 | LevelSetList::UP_TO_PHP_80, 97 | PHPUnitSetList::PHPUNIT_CODE_QUALITY, 98 | SetList::CODE_QUALITY, 99 | SetList::CODING_STYLE, 100 | SetList::TYPE_DECLARATION, 101 | ]) 102 | ->withRules([ 103 | ArrayDimFetchAssignToAddCollectionCallRector::class, 104 | AddClosureParamTypeForArrayMapRector::class, 105 | AddLiteralSeparatorToNumberRector::class, 106 | ]) 107 | ; 108 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | exclude([ 11 | 'var', 12 | 'vendor', 13 | ]) 14 | ->name('*.php') 15 | ->ignoreDotFiles(true) 16 | ->ignoreVCS(true) 17 | ->in(__DIR__) 18 | ; 19 | 20 | return (new Config()) 21 | ->setUnsupportedPhpVersionAllowed(true) 22 | ->setParallelConfig(ParallelConfigFactory::detect()) 23 | ->setCacheFile(__DIR__ . '/var/.php-cs-fixer.cache') 24 | ->setRiskyAllowed(true) 25 | ->setRules([ 26 | '@PhpCsFixer' => true, 27 | '@PHPUnit100Migration:risky' => true, 28 | '@Symfony:risky' => true, 29 | 'binary_operator_spaces' => ['operators' => ['=>' => 'align_single_space_minimal']], 30 | 'blank_line_after_namespace' => true, 31 | 'blank_line_after_opening_tag' => true, 32 | 'blank_line_before_statement' => ['statements' => ['exit', 'return']], 33 | 'blank_lines_before_namespace' => true, 34 | 'cast_spaces' => ['space' => 'none'], 35 | 'class_attributes_separation' => [ 36 | 'elements' => ['method' => 'one', 'property' => 'one'], 37 | ], 38 | 'comment_to_phpdoc' => [ 39 | 'ignored_tags' => [ 40 | 'codeCoverageIgnoreEnd', 41 | 'codeCoverageIgnoreStart', 42 | 'phan-file-suppress', 43 | 'phan-suppress-current-line', 44 | 'phan-suppress-next-line', 45 | 'phpstan-ignore-line', 46 | 'phpstan-ignore-next-line', 47 | 'todo', 48 | ], 49 | ], 50 | 'concat_space' => ['spacing' => 'one'], 51 | 'declare_equal_normalize' => ['space' => 'none'], 52 | 'doctrine_annotation_indentation' => true, 53 | 'doctrine_annotation_spaces' => true, 54 | 'elseif' => true, 55 | 'encoding' => true, 56 | 'full_opening_tag' => true, 57 | 'function_declaration' => true, 58 | 'global_namespace_import' => ['import_classes' => true, 'import_functions' => true], 59 | 'increment_style' => ['style' => 'post'], 60 | 'lowercase_cast' => true, 61 | 'lowercase_keywords' => true, 62 | 'lowercase_static_reference' => true, 63 | 'mb_str_functions' => false, 64 | 'native_constant_invocation' => false, 65 | 'native_function_invocation' => false, 66 | 'no_blank_lines_after_class_opening' => true, 67 | 'no_closing_tag' => true, 68 | 'no_leading_import_slash' => true, 69 | 'no_spaces_after_function_name' => true, 70 | 'no_trailing_whitespace' => true, 71 | 'no_trailing_whitespace_in_comment' => true, 72 | 'no_unset_cast' => false, 73 | 'no_unused_imports' => true, 74 | 'no_useless_return' => true, 75 | 'no_whitespace_in_blank_line' => true, 76 | 'ordered_imports' => [ 77 | 'sort_algorithm' => 'alpha', 78 | 'imports_order' => ['const', 'class', 'function'], 79 | ], 80 | 'ordered_types' => ['null_adjustment' => 'always_first', 'sort_algorithm' => 'none'], 81 | 'php_unit_internal_class' => ['types' => []], 82 | 'php_unit_test_class_requires_covers' => false, 83 | 'phpdoc_summary' => false, 84 | 'phpdoc_to_comment' => ['ignored_tags' => ['var', 'see']], 85 | 'phpdoc_types' => true, 86 | 'phpdoc_types_order' => ['null_adjustment' => 'always_first', 'sort_algorithm' => 'none'], 87 | 'self_static_accessor' => true, 88 | 'short_scalar_cast' => true, 89 | 'simplified_null_return' => false, 90 | 'single_blank_line_at_eof' => true, 91 | 'single_class_element_per_statement' => true, 92 | 'single_import_per_statement' => true, 93 | 'single_line_after_imports' => true, 94 | 'single_line_empty_body' => false, 95 | 'single_quote' => true, 96 | 'spaces_inside_parentheses' => ['space' => 'none'], 97 | 'static_lambda' => true, 98 | 'switch_case_semicolon_to_colon' => true, 99 | 'switch_case_space' => true, 100 | 'ternary_operator_spaces' => true, 101 | 'trailing_comma_in_multiline' => ['elements' => ['arrays', 'arguments', 'parameters']], 102 | 'use_arrow_functions' => true, 103 | 'void_return' => true, 104 | 'phpdoc_array_type' => true, 105 | 'attribute_empty_parentheses' => true, 106 | 'multiline_whitespace_before_semicolons' => [ 107 | 'strategy' => 'new_line_for_chained_calls', 108 | ], 109 | 'ordered_attributes' => true, 110 | 'ordered_interfaces' => true, 111 | ]) 112 | ->setFinder($finder) 113 | ; 114 | -------------------------------------------------------------------------------- /src/the-choice/Container.php: -------------------------------------------------------------------------------- 1 | */ 89 | protected array $services = []; 90 | 91 | /** @var array */ 92 | protected array $classMap; 93 | 94 | protected array $contexts; 95 | 96 | public function __construct(array $contexts) 97 | { 98 | $this->contexts = $contexts; 99 | 100 | $this->classMap = array_merge( 101 | $this->builders, 102 | $this->operators, 103 | $this->nodeFactories, 104 | $this->processors, 105 | $this->interfaces, 106 | ); 107 | } 108 | 109 | /** 110 | * @throws ContainerNotFoundException 111 | */ 112 | public function get(string $id): object 113 | { 114 | if (NodeFactoryResolverInterface::class === $id) { 115 | if (!array_key_exists(NodeFactoryResolverInterface::class, $this->services)) { 116 | $this->services[NodeFactoryResolverInterface::class] = new NodeFactoryResolver(); 117 | } 118 | 119 | return $this->services[NodeFactoryResolverInterface::class]; 120 | } 121 | 122 | if (OperatorResolverInterface::class === $id) { 123 | if (!array_key_exists(OperatorResolverInterface::class, $this->services)) { 124 | $this->services[OperatorResolverInterface::class] = new OperatorResolver(); 125 | } 126 | 127 | return $this->services[OperatorResolverInterface::class]; 128 | } 129 | 130 | if (ProcessorResolverInterface::class === $id) { 131 | if (!array_key_exists(ProcessorResolverInterface::class, $this->services)) { 132 | $this->services[ProcessorResolverInterface::class] = new ProcessorResolver(); 133 | } 134 | 135 | return $this->services[ProcessorResolverInterface::class]; 136 | } 137 | 138 | if (in_array($id, $this->nodeFactories, true)) { 139 | if (!array_key_exists($id, $this->services)) { 140 | $this->services[$id] = new $id(); 141 | } 142 | 143 | return $this->services[$id]; 144 | } 145 | 146 | if (in_array($id, $this->builders, true)) { 147 | return new $id($this); 148 | } 149 | 150 | if (in_array($id, $this->operators, true)) { 151 | return new $id(); 152 | } 153 | 154 | if (in_array($id, $this->processors, true)) { 155 | /** @var AbstractProcessor $processor */ 156 | $processor = new $id(); 157 | $processor->setContainer($this); 158 | 159 | if (ContextProcessor::class === $id) { 160 | /** @var ContextProcessor $processor */ 161 | $contextFactory = $this->get(ContextFactoryInterface::class); 162 | if ($contextFactory instanceof ContextFactoryInterface) { 163 | $processor->setContextFactory($contextFactory); 164 | } 165 | } 166 | 167 | return $processor; 168 | } 169 | 170 | if (ContextFactoryInterface::class === $id) { 171 | $contextFactory = new ContextFactory($this->contexts); 172 | $contextFactory->setContainer($this); 173 | 174 | return $contextFactory; 175 | } 176 | 177 | throw new ContainerNotFoundException(sprintf('There is no configuration for "%s" item in the container', $id)); 178 | } 179 | 180 | public function has(string $id): bool 181 | { 182 | return in_array($id, $this->classMap, true); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /test/Integration/OperatorsTest.php: -------------------------------------------------------------------------------- 1 | setValue(1)->assert($this->getContext(1))); 24 | self::assertFalse((new Equal())->setValue('1')->assert($this->getContext(1))); 25 | self::assertFalse((new Equal())->setValue(2)->assert($this->getContext(1))); 26 | 27 | self::assertTrue((new Equal())->setValue('1')->assert($this->getContext('1'))); 28 | self::assertFalse((new Equal())->setValue(1)->assert($this->getContext('1'))); 29 | self::assertFalse((new Equal())->setValue('2')->assert($this->getContext(1))); 30 | 31 | self::assertTrue((new Equal())->setValue([1])->assert($this->getContext([1]))); 32 | self::assertFalse((new Equal())->setValue([1])->assert($this->getContext([2]))); 33 | } 34 | 35 | public function testNotEqualTest(): void 36 | { 37 | self::assertFalse((new NotEqual())->setValue(1)->assert($this->getContext(1))); 38 | self::assertTrue((new NotEqual())->setValue('1')->assert($this->getContext(1))); 39 | self::assertTrue((new NotEqual())->setValue(2)->assert($this->getContext(1))); 40 | 41 | self::assertFalse((new NotEqual())->setValue('1')->assert($this->getContext('1'))); 42 | self::assertTrue((new NotEqual())->setValue(1)->assert($this->getContext('1'))); 43 | self::assertTrue((new NotEqual())->setValue('2')->assert($this->getContext(1))); 44 | 45 | self::assertFalse((new NotEqual())->setValue([1])->assert($this->getContext([1]))); 46 | self::assertTrue((new NotEqual())->setValue([1])->assert($this->getContext([2]))); 47 | } 48 | 49 | public function testGreaterThanTest(): void 50 | { 51 | self::assertTrue((new GreaterThan())->setValue(1)->assert($this->getContext(2))); 52 | self::assertFalse((new GreaterThan())->setValue(2)->assert($this->getContext(1))); 53 | } 54 | 55 | public function testGreaterThanOrEqualTest(): void 56 | { 57 | self::assertTrue((new GreaterThanOrEqual())->setValue(1)->assert($this->getContext(2))); 58 | self::assertTrue((new GreaterThanOrEqual())->setValue(1)->assert($this->getContext(1))); 59 | self::assertFalse((new GreaterThanOrEqual())->setValue(2)->assert($this->getContext(1))); 60 | } 61 | 62 | public function testLowerThanTest(): void 63 | { 64 | self::assertFalse((new LowerThan())->setValue(1)->assert($this->getContext(2))); 65 | self::assertTrue((new LowerThan())->setValue(2)->assert($this->getContext(1))); 66 | } 67 | 68 | public function testLowerThanOrEqualTest(): void 69 | { 70 | self::assertFalse((new LowerThanOrEqual())->setValue(1)->assert($this->getContext(2))); 71 | self::assertTrue((new LowerThanOrEqual())->setValue(1)->assert($this->getContext(1))); 72 | self::assertTrue((new LowerThanOrEqual())->setValue(2)->assert($this->getContext(1))); 73 | } 74 | 75 | public function testStringContainTest(): void 76 | { 77 | $operator = (new StringContain())->setValue('test'); 78 | 79 | self::assertTrue($operator->assert($this->getContext('test'))); 80 | self::assertTrue($operator->assert($this->getContext('atest'))); 81 | self::assertTrue($operator->assert($this->getContext('testa'))); 82 | self::assertFalse($operator->assert($this->getContext('aa'))); 83 | } 84 | 85 | public function testStringNotContainTest(): void 86 | { 87 | $operator = (new StringNotContain())->setValue('test'); 88 | 89 | self::assertFalse($operator->assert($this->getContext('test'))); 90 | self::assertFalse($operator->assert($this->getContext('atest'))); 91 | self::assertFalse($operator->assert($this->getContext('testa'))); 92 | self::assertTrue($operator->assert($this->getContext('aa'))); 93 | } 94 | 95 | public function testArrayContainTest(): void 96 | { 97 | $operator = (new ArrayContain())->setValue([1, 2, 3, 'a']); 98 | 99 | self::assertTrue($operator->assert($this->getContext(1))); 100 | self::assertTrue($operator->assert($this->getContext(2))); 101 | self::assertTrue($operator->assert($this->getContext(3))); 102 | self::assertTrue($operator->assert($this->getContext('a'))); 103 | self::assertFalse($operator->assert($this->getContext('b'))); 104 | self::assertFalse($operator->assert($this->getContext(4))); 105 | } 106 | 107 | public function testArrayNotContainTest(): void 108 | { 109 | $operator = (new ArrayNotContain())->setValue([1, 2, 3, 'a']); 110 | 111 | self::assertFalse($operator->assert($this->getContext(1))); 112 | self::assertFalse($operator->assert($this->getContext(2))); 113 | self::assertFalse($operator->assert($this->getContext(3))); 114 | self::assertFalse($operator->assert($this->getContext('a'))); 115 | self::assertTrue($operator->assert($this->getContext('b'))); 116 | self::assertTrue($operator->assert($this->getContext(4))); 117 | } 118 | 119 | public function testArrayNumericInRangeTest(): void 120 | { 121 | $operator = (new NumericInRange())->setValue([1, 5]); 122 | 123 | self::assertTrue($operator->assert($this->getContext(1))); 124 | self::assertTrue($operator->assert($this->getContext(3))); 125 | self::assertTrue($operator->assert($this->getContext(5))); 126 | self::assertFalse($operator->assert($this->getContext(0))); 127 | self::assertFalse($operator->assert($this->getContext(6))); 128 | self::assertTrue($operator->assert($this->getContext(1))); 129 | self::assertTrue($operator->assert($this->getContext(3))); 130 | self::assertTrue($operator->assert($this->getContext(5))); 131 | self::assertFalse($operator->assert($this->getContext(0))); 132 | self::assertFalse($operator->assert($this->getContext(6))); 133 | } 134 | 135 | private function getContext(int|string|array $value): ContextInterface 136 | { 137 | return new class($value) implements ContextInterface { 138 | private $value; 139 | 140 | public function __construct($value) 141 | { 142 | $this->value = $value; 143 | } 144 | 145 | public function getValue(): mixed 146 | { 147 | return $this->value; 148 | } 149 | }; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TheChoice - Business Rule Engine 2 | 3 | [![Build Status](https://travis-ci.org/prohalexey/TheChoice.png)](https://travis-ci.org/prohalexey/TheChoice) 4 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/prohalexey/TheChoice/master/LICENSE) 5 | 6 | A powerful and flexible Business Rule Engine for PHP that allows you to separate business logic from your application code. 7 | 8 | ## Features 9 | 10 | This library helps you simplify the implementation of complex business rules such as: 11 | - Complex discount calculations 12 | - Customer bonus systems 13 | - User permission resolution 14 | - Dynamic pricing strategies 15 | 16 | **Why use TheChoice?** If you find yourself constantly modifying business conditions in your code, this library allows you to move those conditions to external configuration sources. You can even create a web interface to edit configurations dynamically. 17 | 18 | ### Key Benefits 19 | - ✅ Rules written in JSON or YAML format 20 | - ✅ Store rules in files or databases 21 | - ✅ Serializable and cacheable configurations 22 | - ✅ PSR-11 compatible container support 23 | - ✅ Extensible with custom operators and contexts 24 | 25 | ## Installation 26 | 27 | ```bash 28 | composer require prohalexey/the-choice 29 | ``` 30 | 31 | ## Quick Start 32 | 33 | ### JSON Configuration Example 34 | 35 | ```json 36 | { 37 | "node": "condition", 38 | "if": { 39 | "node": "collection", 40 | "type": "and", 41 | "elements": [ 42 | { 43 | "node": "context", 44 | "context": "withdrawalCount", 45 | "operator": "equal", 46 | "value": 0 47 | }, 48 | { 49 | "node": "context", 50 | "context": "inGroup", 51 | "operator": "arrayContain", 52 | "value": [ 53 | "testgroup", 54 | "testgroup2" 55 | ] 56 | } 57 | ] 58 | }, 59 | "then": { 60 | "node": "context", 61 | "description": "Giving 10% of deposit sum as discount for the next order", 62 | "context": "getDepositSum", 63 | "modifiers": [ 64 | "$context * 0.1" 65 | ], 66 | "params": { 67 | "discountType": "VIP client" 68 | } 69 | }, 70 | "else": { 71 | "node": "value", 72 | "description": "Giving 5% discount for the next order", 73 | "value": "5" 74 | } 75 | } 76 | ``` 77 | 78 | ### YAML Configuration Example 79 | 80 | ```yaml 81 | node: condition 82 | if: 83 | node: collection 84 | type: and 85 | elements: 86 | - node: context 87 | context: withdrawalCount 88 | operator: equal 89 | value: 0 90 | - node: context 91 | context: inGroup 92 | operator: arrayContain 93 | value: 94 | - testgroup 95 | - testgroup2 96 | then: 97 | node: context 98 | context: getDepositSum 99 | description: "Giving 10% of deposit sum as discount for the next order" 100 | modifiers: 101 | - "$context * 0.1" 102 | params: 103 | discountType: "VIP client" 104 | else: 105 | node: value 106 | description: "Giving 5% discount for the next order" 107 | value: 5 108 | ``` 109 | 110 | ### PHP Usage 111 | 112 | ```php 113 | VisitCount::class, 120 | 'hasVipStatus' => HasVipStatus::class, 121 | 'inGroup' => InGroup::class, 122 | 'withdrawalCount' => WithdrawalCount::class, 123 | 'depositCount' => DepositCount::class, 124 | 'utmSource' => UtmSource::class, 125 | 'contextWithParams' => ContextWithParams::class, 126 | 'action1' => Action1::class, 127 | 'action2' => Action2::class, 128 | 'actionReturnInt' => ActionReturnInt::class, 129 | 'actionWithParams' => ActionWithParams::class, 130 | ]); 131 | 132 | // Create a parser 133 | $parser = $container->get(JsonBuilder::class); 134 | 135 | // Load rules from file or other sources 136 | $rules = $parser->parseFile('rules/discount-rules.json'); 137 | 138 | // Get the processor 139 | $resolver = $container->get(ProcessorResolverInterface::class); 140 | $processor = $resolver->resolve($rules); 141 | 142 | // Execute the rules 143 | $result = $processor->process($rules); 144 | ``` 145 | 146 | ## Core Concepts 147 | 148 | ### Node Types 149 | 150 | Each node has a `node` property that describes its type and an optional `description` property for UI purposes. 151 | 152 | #### Root Node 153 | The root of the rules tree that maintains state and stores execution results. 154 | 155 | **Properties:** 156 | - `storage` - Container for variables 157 | - `rules` - Contains the first node to be processed 158 | 159 | **Example:** 160 | ```yaml 161 | node: root 162 | description: "Discount settings" 163 | rules: 164 | node: value 165 | value: 5 166 | ``` 167 | 168 | #### Value Node 169 | Returns a static value. 170 | 171 | **Properties:** 172 | - `value` - The value to return (can be array, string, or numeric) 173 | 174 | **Example:** 175 | ```yaml 176 | node: value 177 | description: "5% discount for next order" 178 | value: 5 179 | ``` 180 | 181 | #### Context Node 182 | Executes callable objects and can modify the global state which is stored in the "Root" node. 183 | 184 | **Properties:** 185 | - `break` - Special property to stop execution (`"immediately"` stops and returns context result) 186 | - `context` - Name of the context for calculations 187 | - `modifiers` - Array of mathematical modifiers 188 | - `operator` - Operator for calculations or comparisons 189 | - `params` - Parameters to set in context 190 | - `priority` - Priority for collection sorting 191 | - `value` - Default value for the `$context` variable 192 | 193 | **Example:** 194 | ```yaml 195 | node: context 196 | context: getDepositSum 197 | description: "Calculate 10% of deposit sum" 198 | modifiers: 199 | - "$context * 0.1" 200 | params: 201 | discountType: "VIP client" 202 | priority: 5 203 | ``` 204 | 205 | **With Operator Example:** 206 | ```yaml 207 | node: context 208 | context: withdrawalCount 209 | operator: equal 210 | value: 0 211 | ``` 212 | 213 | #### Collection Node 214 | Contains multiple nodes with AND/OR logic. 215 | 216 | **Properties:** 217 | - `type` - Collection type (`and` or `or`) 218 | - `elements` - Array of child nodes 219 | - `priority` - Priority for nested collections 220 | 221 | **Example:** 222 | ```yaml 223 | node: collection 224 | type: and 225 | elements: 226 | - node: context 227 | context: withdrawalCount 228 | operator: equal 229 | value: 0 230 | - node: context 231 | context: inGroup 232 | operator: arrayContain 233 | value: 234 | - testgroup 235 | - testgroup2 236 | ``` 237 | 238 | #### Condition Node 239 | Conditional logic with if-then-else structure. 240 | 241 | **Properties:** 242 | - `if` - Condition node (expects boolean result) 243 | - `then` - Node to execute if condition is true 244 | - `else` - Node to execute if condition is false 245 | 246 | ### Built-in Operators 247 | 248 | The following operators are available for context nodes: 249 | 250 | - `ArrayContain` - Check if array contains value 251 | - `ArrayNotContain` - Check if array doesn't contain value 252 | - `Equal` - Equality comparison 253 | - `GreaterThan` - Greater than comparison 254 | - `GreaterThanOrEqual` - Greater than or equal comparison 255 | - `LowerThan` - Less than comparison 256 | - `LowerThanOrEqual` - Less than or equal comparison 257 | - `NotEqual` - Not equal comparison 258 | - `NumericInRange` - Check if number is within range 259 | - `StringContain` - Check if string contains substring 260 | - `StringNotContain` - Check if string doesn't contain substring 261 | 262 | ### Modifiers 263 | 264 | Modifiers allow you to transform context values using mathematical expressions. Use the predefined `$context` variable in your expressions. 265 | 266 | For more information about calculations, see: https://github.com/chriskonnertz/string-calc 267 | 268 | ## Advanced Features 269 | 270 | ### Custom Contexts 271 | Create custom context classes by implementing the required interface and register them in the container. 272 | 273 | ### Custom Operators 274 | Extend the system by creating custom operators and adding them to the container. 275 | 276 | ### Caching 277 | Configurations can be serialized and cached for improved performance. 278 | 279 | ## Examples and Testing 280 | 281 | For more detailed examples and usage patterns, see the test files in the `test/` directory, especially the container configuration examples. 282 | 283 | ## Contributing 284 | 285 | Contributions are welcome! Please feel free to submit a Pull Request. 286 | 287 | ## License 288 | 289 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 290 | -------------------------------------------------------------------------------- /test/Integration/yamlTest.php: -------------------------------------------------------------------------------- 1 | testFilesDir = ''; 34 | if ('TheChoice' === basename(getcwd())) { 35 | $this->testFilesDir = './test/Integration/'; 36 | } 37 | 38 | $container = new Container([ 39 | 'visitCount' => VisitCount::class, 40 | 'hasVipStatus' => HasVipStatus::class, 41 | 'inGroup' => InGroup::class, 42 | 'withdrawalCount' => WithdrawalCount::class, 43 | 'depositCount' => DepositCount::class, 44 | 'utmSource' => UtmSource::class, 45 | 'contextWithParams' => ContextWithParams::class, 46 | 'action1' => Action1::class, 47 | 'action2' => Action2::class, 48 | 'actionReturnInt' => ActionReturnInt::class, 49 | 'actionWithParams' => ActionWithParams::class, 50 | ]); 51 | 52 | $this->parser = $container->get(YamlBuilder::class); 53 | $this->rootProcessor = $container->get(RootProcessor::class); 54 | } 55 | 56 | public function testNodeContextWithOperatorArrayContainTest(): void 57 | { 58 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextWithOperatorArrayContain.yaml'); 59 | $result = $this->rootProcessor->process($node); 60 | self::assertTrue($result); 61 | } 62 | 63 | public function testNodeContextWithOperatorArrayNotContainTest(): void 64 | { 65 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextWithOperatorArrayNotContain.yaml'); 66 | $result = $this->rootProcessor->process($node); 67 | self::assertTrue($result); 68 | } 69 | 70 | public function testNodeContextWithOperatorEqualTest(): void 71 | { 72 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextWithOperatorEqual.yaml'); 73 | $result = $this->rootProcessor->process($node); 74 | self::assertTrue($result); 75 | } 76 | 77 | public function testNodeContextWithOperatorEqualAndContextWithParamsTest(): void 78 | { 79 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextWithOperatorEqualAndContextWithParams.yaml'); 80 | $result = $this->rootProcessor->process($node); 81 | self::assertTrue($result); 82 | } 83 | 84 | public function testNodeContextWithOperatorGreaterThanTest(): void 85 | { 86 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextWithOperatorGreaterThan.yaml'); 87 | $result = $this->rootProcessor->process($node); 88 | self::assertTrue($result); 89 | } 90 | 91 | public function testNodeContextWithOperatorGreaterThanOrEqualTest(): void 92 | { 93 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextWithOperatorGreaterThanOrEqual.yaml'); 94 | $result = $this->rootProcessor->process($node); 95 | self::assertTrue($result); 96 | } 97 | 98 | public function testNodeContextWithOperatorLowerThanTest(): void 99 | { 100 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextWithOperatorLowerThan.yaml'); 101 | $result = $this->rootProcessor->process($node); 102 | self::assertTrue($result); 103 | } 104 | 105 | public function testNodeContextWithOperatorLowerThanOrEqualTest(): void 106 | { 107 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextWithOperatorLowerThanOrEqual.yaml'); 108 | $result = $this->rootProcessor->process($node); 109 | self::assertTrue($result); 110 | } 111 | 112 | public function testNodeContextWithOperatorNotEqualTest(): void 113 | { 114 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextWithOperatorNotEqual.yaml'); 115 | $result = $this->rootProcessor->process($node); 116 | self::assertTrue($result); 117 | } 118 | 119 | public function testNodeContextWithOperatorStringContainTest(): void 120 | { 121 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextWithOperatorStringContain.yaml'); 122 | $result = $this->rootProcessor->process($node); 123 | self::assertTrue($result); 124 | } 125 | 126 | public function testNodeContextWithOperatorStringNotContainTest(): void 127 | { 128 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextWithOperatorStringNotContain.yaml'); 129 | $result = $this->rootProcessor->process($node); 130 | self::assertTrue($result); 131 | } 132 | 133 | public function testNodeContextResultTrueTest(): void 134 | { 135 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextResultTrue.yaml'); 136 | $result = $this->rootProcessor->process($node); 137 | self::assertTrue($result); 138 | } 139 | 140 | public function testNodeContextResultFalseTest(): void 141 | { 142 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextResultFalse.yaml'); 143 | $result = $this->rootProcessor->process($node); 144 | self::assertFalse($result); 145 | } 146 | 147 | public function testNodeContextWithParamsTest(): void 148 | { 149 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextWithParams.yaml'); 150 | $result = $this->rootProcessor->process($node); 151 | self::assertTrue($result); 152 | } 153 | 154 | public function testNodeContextWithModifiersTest(): void 155 | { 156 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextWithModifiers.yaml'); 157 | $result = $this->rootProcessor->process($node); 158 | self::assertSame(4, $result); 159 | } 160 | 161 | public function testNodeRootWithStorageTest(): void 162 | { 163 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeRootWithStorage.yaml'); 164 | $result = $this->rootProcessor->process($node); 165 | self::assertSame(4, $result); 166 | } 167 | 168 | public function testNodeContextWithModifiersAndOperatorTest(): void 169 | { 170 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextWithModifiersAndOperator.yaml'); 171 | $result = $this->rootProcessor->process($node); 172 | self::assertTrue($result); 173 | } 174 | 175 | public function testNodeContextStoppableTest(): void 176 | { 177 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeContextStoppable.yaml'); 178 | $result = $this->rootProcessor->process($node); 179 | self::assertSame(5, $result); 180 | } 181 | 182 | public function testNodeConditionThenCaseTest(): void 183 | { 184 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeConditionThenCase.yaml'); 185 | $result = $this->rootProcessor->process($node); 186 | self::assertTrue($result); 187 | } 188 | 189 | public function testNodeConditionElseCaseTest(): void 190 | { 191 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeConditionElseCase.yaml'); 192 | $result = $this->rootProcessor->process($node); 193 | self::assertFalse($result); 194 | } 195 | 196 | public function testNodeAndCollectionAllFalseTest(): void 197 | { 198 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeAndCollectionAllFalse.yaml'); 199 | $result = $this->rootProcessor->process($node); 200 | self::assertFalse($result); 201 | } 202 | 203 | public function testNodeAndCollectionOneFalseTest(): void 204 | { 205 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeAndCollectionOneFalse.yaml'); 206 | $result = $this->rootProcessor->process($node); 207 | self::assertFalse($result); 208 | } 209 | 210 | public function testNodeAndCollectionAllTrueTest(): void 211 | { 212 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeAndCollectionAllTrue.yaml'); 213 | $result = $this->rootProcessor->process($node); 214 | self::assertTrue($result); 215 | } 216 | 217 | public function testNodeOrCollectionAllFalseTest(): void 218 | { 219 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeOrCollectionAllFalse.yaml'); 220 | $result = $this->rootProcessor->process($node); 221 | self::assertFalse($result); 222 | } 223 | 224 | public function testNodeOrCollectionOneFalseTest(): void 225 | { 226 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeOrCollectionOneTrue.yaml'); 227 | $result = $this->rootProcessor->process($node); 228 | self::assertTrue($result); 229 | } 230 | 231 | public function testNodeOrCollectionAllTrueTest(): void 232 | { 233 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeOrCollectionAllTrue.yaml'); 234 | $result = $this->rootProcessor->process($node); 235 | self::assertTrue($result); 236 | } 237 | 238 | public function testCombined1Test(): void 239 | { 240 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testCombined1.yaml'); 241 | $result = $this->rootProcessor->process($node); 242 | self::assertTrue($result); 243 | } 244 | 245 | public function testNodeValueTest(): void 246 | { 247 | $node = $this->parser->parseFile($this->testFilesDir . 'Yaml/testNodeValue.yaml'); 248 | $result = $this->rootProcessor->process($node); 249 | self::assertSame(4, $result); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /test/Integration/jsonTest.php: -------------------------------------------------------------------------------- 1 | testFilesDir = ''; 34 | if ('TheChoice' === basename(getcwd())) { 35 | $this->testFilesDir = './test/Integration/'; 36 | } 37 | 38 | $container = new Container([ 39 | 'visitCount' => VisitCount::class, 40 | 'hasVipStatus' => HasVipStatus::class, 41 | 'inGroup' => InGroup::class, 42 | 'withdrawalCount' => WithdrawalCount::class, 43 | 'depositCount' => DepositCount::class, 44 | 'utmSource' => UtmSource::class, 45 | 'contextWithParams' => ContextWithParams::class, 46 | 'action1' => Action1::class, 47 | 'action2' => Action2::class, 48 | 'actionReturnInt' => ActionReturnInt::class, 49 | 'actionWithParams' => ActionWithParams::class, 50 | ]); 51 | 52 | $this->parser = $container->get(JsonBuilder::class); 53 | $this->rootProcessor = $container->get(RootProcessor::class); 54 | } 55 | 56 | public function testNodeContextWithOperatorArrayContainTestXX(): void 57 | { 58 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextWithOperatorArrayContain.json'); 59 | $result = $this->rootProcessor->process($node); 60 | self::assertTrue($result); 61 | } 62 | 63 | public function testNodeContextWithOperatorArrayContainTest(): void 64 | { 65 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextWithOperatorArrayContain.json'); 66 | $result = $this->rootProcessor->process($node); 67 | self::assertTrue($result); 68 | } 69 | 70 | public function testNodeContextWithOperatorArrayNotContainTest(): void 71 | { 72 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextWithOperatorArrayNotContain.json'); 73 | $result = $this->rootProcessor->process($node); 74 | self::assertTrue($result); 75 | } 76 | 77 | public function testNodeContextWithOperatorEqualTest(): void 78 | { 79 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextWithOperatorEqual.json'); 80 | $result = $this->rootProcessor->process($node); 81 | self::assertTrue($result); 82 | } 83 | 84 | public function testNodeContextWithOperatorEqualAndContextWithParamsTest(): void 85 | { 86 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextWithOperatorEqualAndContextWithParams.json'); 87 | $result = $this->rootProcessor->process($node); 88 | self::assertTrue($result); 89 | } 90 | 91 | public function testNodeContextWithOperatorGreaterThanTest(): void 92 | { 93 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextWithOperatorGreaterThan.json'); 94 | $result = $this->rootProcessor->process($node); 95 | self::assertTrue($result); 96 | } 97 | 98 | public function testNodeContextWithOperatorGreaterThanOrEqualTest(): void 99 | { 100 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextWithOperatorGreaterThanOrEqual.json'); 101 | $result = $this->rootProcessor->process($node); 102 | self::assertTrue($result); 103 | } 104 | 105 | public function testNodeContextWithOperatorLowerThanTest(): void 106 | { 107 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextWithOperatorLowerThan.json'); 108 | $result = $this->rootProcessor->process($node); 109 | self::assertTrue($result); 110 | } 111 | 112 | public function testNodeContextWithOperatorLowerThanOrEqualTest(): void 113 | { 114 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextWithOperatorLowerThanOrEqual.json'); 115 | $result = $this->rootProcessor->process($node); 116 | self::assertTrue($result); 117 | } 118 | 119 | public function testNodeContextWithOperatorNotEqualTest(): void 120 | { 121 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextWithOperatorNotEqual.json'); 122 | $result = $this->rootProcessor->process($node); 123 | self::assertTrue($result); 124 | } 125 | 126 | public function testNodeContextWithOperatorStringContainTest(): void 127 | { 128 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextWithOperatorStringContain.json'); 129 | $result = $this->rootProcessor->process($node); 130 | self::assertTrue($result); 131 | } 132 | 133 | public function testNodeContextWithOperatorStringNotContainTest(): void 134 | { 135 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextWithOperatorStringNotContain.json'); 136 | $result = $this->rootProcessor->process($node); 137 | self::assertTrue($result); 138 | } 139 | 140 | public function testNodeContextResultTrueTest(): void 141 | { 142 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextResultTrue.json'); 143 | $result = $this->rootProcessor->process($node); 144 | self::assertTrue($result); 145 | } 146 | 147 | public function testNodeContextResultFalseTest(): void 148 | { 149 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextResultFalse.json'); 150 | $result = $this->rootProcessor->process($node); 151 | self::assertFalse($result); 152 | } 153 | 154 | public function testNodeContextWithParamsTest(): void 155 | { 156 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextWithParams.json'); 157 | $result = $this->rootProcessor->process($node); 158 | self::assertTrue($result); 159 | } 160 | 161 | public function testNodeContextWithModifiersTest(): void 162 | { 163 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextWithModifiers.json'); 164 | $result = $this->rootProcessor->process($node); 165 | self::assertSame(4, $result); 166 | } 167 | 168 | public function testNodeContextWithModifiersAndOperatorTest(): void 169 | { 170 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextWithModifiersAndOperator.json'); 171 | $result = $this->rootProcessor->process($node); 172 | self::assertTrue($result); 173 | } 174 | 175 | public function testNodeContextStoppableTest(): void 176 | { 177 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeContextStoppable.json'); 178 | $result = $this->rootProcessor->process($node); 179 | self::assertSame(5, $result); 180 | } 181 | 182 | public function testNodeConditionThenCaseTest(): void 183 | { 184 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeConditionThenCase.json'); 185 | $result = $this->rootProcessor->process($node); 186 | self::assertTrue($result); 187 | } 188 | 189 | public function testNodeConditionElseCaseTest(): void 190 | { 191 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeConditionElseCase.json'); 192 | $result = $this->rootProcessor->process($node); 193 | self::assertFalse($result); 194 | } 195 | 196 | public function testNodeAndCollectionAllFalseTest(): void 197 | { 198 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeAndCollectionAllFalse.json'); 199 | $result = $this->rootProcessor->process($node); 200 | self::assertFalse($result); 201 | } 202 | 203 | public function testNodeAndCollectionOneFalseTest(): void 204 | { 205 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeAndCollectionOneFalse.json'); 206 | $result = $this->rootProcessor->process($node); 207 | self::assertFalse($result); 208 | } 209 | 210 | public function testNodeAndCollectionAllTrueTest(): void 211 | { 212 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeAndCollectionAllTrue.json'); 213 | $result = $this->rootProcessor->process($node); 214 | self::assertTrue($result); 215 | } 216 | 217 | public function testNodeOrCollectionAllFalseTest(): void 218 | { 219 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeOrCollectionAllFalse.json'); 220 | $result = $this->rootProcessor->process($node); 221 | self::assertFalse($result); 222 | } 223 | 224 | public function testNodeOrCollectionOneFalseTest(): void 225 | { 226 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeOrCollectionOneTrue.json'); 227 | $result = $this->rootProcessor->process($node); 228 | self::assertTrue($result); 229 | } 230 | 231 | public function testNodeOrCollectionAllTrueTest(): void 232 | { 233 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeOrCollectionAllTrue.json'); 234 | $result = $this->rootProcessor->process($node); 235 | self::assertTrue($result); 236 | } 237 | 238 | public function testCombined1Test(): void 239 | { 240 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testCombined1.json'); 241 | $result = $this->rootProcessor->process($node); 242 | self::assertTrue($result); 243 | } 244 | 245 | public function testNodeValueTest(): void 246 | { 247 | $node = $this->parser->parseFile($this->testFilesDir . 'Json/testNodeValue.json'); 248 | $result = $this->rootProcessor->process($node); 249 | self::assertSame(4, $result); 250 | } 251 | } 252 | --------------------------------------------------------------------------------