Skip to content

Commit

Permalink
Merge pull request #3 from prohalexey/node_processors
Browse files Browse the repository at this point in the history
Totally refactored. Splitted treeProcessor into several different processors
  • Loading branch information
prohalexey authored Apr 3, 2020
2 parents c70c28c + 2720331 commit 585d2fc
Show file tree
Hide file tree
Showing 84 changed files with 1,518 additions and 1,081 deletions.
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
language: php

php:
- 7.0
- 7.1
- 7.2

before_script:
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2018 prohalexey
Copyright (c) 2018 Prokhorov Alexey

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
136 changes: 102 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
[![Build Status](https://travis-ci.org/prohalexey/TheChoice.png)](https://travis-ci.org/prohalexey/TheChoice)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/prohalexey/TheChoice/master/LICENSE)

Small "Business Rule Engine" on PHP

This library allows you to simplify the writing of rules for business processes, such as complex discounts calculation or giving bonuses to your customers.
This can be useful for you if you frequently change certain conditions in your code.
It allows you to move these conditions to configuration files or create web interface that can edit configurations.
You can write rules in JSON or YAML format and store them into files or in the some database.
"Business Rule Engine" on PHP

This library allows you to simplify the writing of rules for business processes, such as:
- complex discounts calculation
- giving bonuses to your customers
- resolving user permissions

This can be useful for you if you change certain conditions in your code over and over againg.
It allows you to move these conditions to configuration sources. You can even create a web interface that can edit configurations.
You can write rules in JSON or YAML format and store them into files or in the database.
Configuration can be serialized and cached.

# Installation

Expand Down Expand Up @@ -98,38 +103,67 @@ else:
# Usage in PHP
```PHP
// Create a parser
$parser = new JsonBuilder(new OperatorFactory());

// Load rules from a file or other sources
$node = $parser->parseFile('Json/testOneNodeWithRuleGreaterThan.json');

// Define contexts
$contextFactory = new ContextFactory([
'getDepositSum' => InGroup::class,
'withdrawalCount' => WithdrawalCount::class,
'depositCount' => DepositCount::class,
'utmSource' => UtmSource::class,
use TheChoice\Container;

// Passing contexts to the PSR-11 compatible container
$container = new Container([
'visitCount' => VisitCount::class,
'hasVipStatus' => HasVipStatus::class,
'inGroup' => InGroup::class,
'withdrawalCount' => WithdrawalCount::class,
'depositCount' => DepositCount::class,
'utmSource' => UtmSource::class,
'contextWithParams' => ContextWithParams::class,
'action1' => Action1::class,
'action2' => Action2::class,
'actionReturnInt' => ActionReturnInt::class,
'actionWithParams' => ActionWithParams::class,
]);

// Here you can use PSR-11 container to resolve objects, callable or just class names
$contextFactory->setContainer($container);
// Creating a parser
$parser = $container->get(JsonBuilder::class);

// Load rules from a file or other sources
$rules = $parser->parseFile('Json/testOneNodeWithRuleGreaterThan.json');

// Instantiating tree(rules) processor
$treeProcessor = (new TreeProcessor())->setContextFactory($contextFactory);
// Loading processor
$resolver = $container->get(ProcessorResolverInterface::class);
$processor = $resolver->resolve($rules);

// And process the rules
$result = $treeProcessor->process($node);
// Process the rules
$result = $processor->process($rules);
```

# Core functionality

## Node types
Each node has a “node” property that describes the type of node.
And also each node has a “description” property which can be used to store description for UI.

### Root
This is a rules' tree root. It has a state and it stores a result of execution.

######Node properties
`storage` - Simple container for variables.

`rules` - This property contain the first node that will be processed. Actually even if you omit this node it will be created automatically.

######Example
```
node: root
description: "Discount settings"
nodes:
node: value
value: 5
```

### Value
This is a simple node that just return some value.

This is a simple node that return value.
######Node properties
`value` - Simple value

######Example
```
node: value
description: "Giving 5% for the next order"
Expand All @@ -140,8 +174,24 @@ value: 5
### Context

This is node associated with some callable object and return some values or this callable can change the system state.
This is node associated with some callable object and return some values as result of execution that callable objects. This node can change the global state which stored in the "Root" node.

######Node properties
`break` - Is a special property that can stop rules processor after execution this node. For now the only allowed value is "immediately" which stop rules execution and return the context result as final result.

`contextName` - The name of context to be used for calculations.

`modifiers` - Array of modifiers.

`operator` - An operator to be used for calculations or comparisons.

`params` - An array of parameters to be set in context.

`priority` - Priority node. If this node will be used in the collection, then the elements in the collection will be sorted according to this value.

`value` - Default value for the **$context** variable;

######Example
```
node: context
context: getDepositSum
Expand All @@ -150,13 +200,13 @@ modifiers:
- "$context * 0.1"
params:
discountType: "VIP client"
priority;
priority: 5
```

> You can set the parameters to "callable" if this "callable" is object.
Parameters will be set via setters or public properties before executing the rule tree
Parameters will be set via setters or public properties before executing the rules

>You can use modifiers for modify return value from context. Use meta-variable `$context`
>You can use modifiers for modify return value from context. Use predefined variable `$context`
For more information about calculations please read this https://github.com/chriskonnertz/string-calc

```
Expand All @@ -166,11 +216,14 @@ operator: equal
value: 0
```

> The result will be stored in the storage or the result will be returned to the "root" node.

You can use Built-in Operators to test returning value of CONTEXT node against some value.

> Operators must return boolean values
This Built-in operators can be used or you can register new custom operators and add them to `OperatorFactory`
This Built-in operators can be used or you can register new custom operators and add them to the container.

```
ArrayContain
Expand All @@ -189,8 +242,18 @@ StringNotContain
### Collection

Collection is a node that contains other nodes.
Available types of collection are AND and OR.

######Node properties

`type` - Available types of collection are AND and OR.

`nodes` - An array of nodes.

`priority` - Priority node. If this node will be used in the collection, then the elements in the collection will be sorted according to this value.

> PRIORITY property is used if this node is in the another collection (e.g collection of collections)
######Example
```
node: collection
type: and
Expand All @@ -211,8 +274,13 @@ node: collection
### Condition

`if` is expecting boolean value
`then`, `else` - Any other nodes include another IF node
A condition node used to test some conditions.

`if` - Is expecting boolean value from inner nodes.

`then` - Any other nodes include another IF node. Executing if result is TRUE

`else` - Any other nodes include another IF node. Executing if result is FALSE

### Any Questions ?
For more usages please see tests
See the tests and especially the container for more details.
13 changes: 7 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@
}
],
"require": {
"php": ">=7",
"chriskonnertz/string-calc": "^1.0"
"php": ">=7.1",
"chriskonnertz/string-calc": "^1.0",
"symfony/yaml": "^4.3",
"ext-json": "*",
"ext-mbstring": "*"
},
"require-dev": {
"phpunit/phpunit": "^5.7",
"roave/security-advisories": "dev-master",
"phpunit/phpunit": "7.0.0",
"psr/container": "1.0.0"
},
"suggest": {
"symfony/yaml": "To use YAML rules"
},
"autoload": {
"psr-4": {
"TheChoice\\": "src/the-choice/",
Expand Down
92 changes: 49 additions & 43 deletions src/the-choice/Builder/ArrayBuilder.php
Original file line number Diff line number Diff line change
@@ -1,71 +1,77 @@
<?php

declare(strict_types=1);

namespace TheChoice\Builder;

use TheChoice\Factory\NodeConditionFactory;
use TheChoice\Factory\NodeCollectionFactory;
use TheChoice\Factory\NodeContextFactory;
use TheChoice\Factory\NodeTreeFactory;
use TheChoice\Factory\NodeValueFactory;
use Psr\Container\ContainerInterface;

use TheChoice\Contract\OperatorFactoryInterface;
use TheChoice\Contract\BuilderInterface;
use TheChoice\Exception\InvalidArgumentException;
use TheChoice\Exception\LogicException;
use TheChoice\Node\Root;
use TheChoice\NodeFactory\NodeFactoryInterface;
use TheChoice\NodeFactory\NodeFactoryResolverInterface;

class ArrayBuilder implements BuilderInterface
{
private $_nodesCount = 0;
protected $rootNode;

private $_nodeTreeFactory;
private $_nodeConditionFactory;
private $_nodeCollectionFactory;
private $_nodeContextFactory;
private $_nodeValueFactory;
protected $nodesCount = 0;

private $_tree;
protected $container;

public function __construct(OperatorFactoryInterface $operatorFactory)
public function __construct(ContainerInterface $container)
{
$this->_nodeTreeFactory = new NodeTreeFactory();
$this->_nodeConditionFactory = new NodeConditionFactory();
$this->_nodeCollectionFactory = new NodeCollectionFactory();
$this->_nodeContextFactory = new NodeContextFactory($operatorFactory);
$this->_nodeValueFactory = new NodeValueFactory();
$this->container = $container;
}

public function build(&$structure)
{
if (!array_key_exists('node', $structure)) {
throw new \InvalidArgumentException('The "node" property is absent!');
throw new InvalidArgumentException('The "node" property is absent!');
}

$this->_nodesCount++;
$this->nodesCount++;

if ($structure['node'] === 'tree') {
if ($this->_nodesCount !== 1) {
throw new \LogicException('Node of type "Tree" must be a root node!');
}
/**
* Workaround for short syntax if the root node is omitted
*/
if ($this->nodesCount === 1 && $structure['node'] !== Root::getNodeName()) {
$structure = [
'node' => Root::getNodeName(),
'rules' => $structure
];

$this->_tree = $this->_nodeTreeFactory->build($this, $structure);
$this->_tree->setNodes($this->_nodeTreeFactory->buildNodes($this, $structure));
return $this->_tree;
}
$this->nodesCount--;

if ($structure['node'] === 'condition') {
$node = $this->_nodeConditionFactory->build($this, $structure);
} elseif ($structure['node'] === 'collection') {
$node = $this->_nodeCollectionFactory->build($this, $structure);
} elseif ($structure['node'] === 'context') {
$node = $this->_nodeContextFactory->build($this, $structure);
} elseif ($structure['node'] === 'value') {
$node = $this->_nodeValueFactory->build($this, $structure);
} else {
throw new \InvalidArgumentException(sprintf('Unknown node type "%s"', $structure['node']));
return $this->build($structure);
}

if (null !== $this->_tree) {
$node->setTree($this->_tree);
if ($this->nodesCount !== 1 && $structure['node'] === Root::getNodeName()) {
throw new LogicException('The node "Root" cannot be not root node!');
}

return $node;
$nodeFactoryResolver = $this->container->get(NodeFactoryResolverInterface::class);
$nodeFactoryType = $nodeFactoryResolver->resolve($structure['node']);

/** @var NodeFactoryInterface $nodeFactory */
$nodeFactory = $this->container->get($nodeFactoryType);
return $nodeFactory->build($this, $structure);
}

public function getContainer(): ContainerInterface
{
return $this->container;
}

public function setRoot(Root $rootNode): BuilderInterface
{
$this->rootNode = $rootNode;
return $this;
}

public function getRoot(): Root
{
return $this->rootNode;
}
}
17 changes: 17 additions & 0 deletions src/the-choice/Builder/BuilderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace TheChoice\Builder;

use Psr\Container\ContainerInterface;
use TheChoice\Node\Root;

interface BuilderInterface
{
public function build(&$structure);

public function getContainer(): ContainerInterface;

public function setRoot(Root $rootNode): BuilderInterface;

public function getRoot(): Root;
}
Loading

0 comments on commit 585d2fc

Please sign in to comment.