├── 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 | [](https://travis-ci.org/prohalexey/TheChoice)
4 | [](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 |
--------------------------------------------------------------------------------