Rule engine is a symfony bundle that allows you to build complex condition sets using parameters and evaluators that you define to match your objects and needs. It offers a friendly front end interface and simple integration with Doctrine entities and their corresponding Sonata admins. A demo site?, (*1)
You can install it through composer., (*2)
{ "require":{ "zitec/rule-engine-bundle": "~1.0" } }
Write a class implementing RuleContextInterface, and add some methods that will return the parameters you wish to include in your rules. To make things super-easy, a basic context class already exists: ContextBase. It already offers getters for 3 parameters: current_date, current_day, and current_time. Your custom context can extend this., (*3)
namespace MyBundle/RuleEngine/Context; use Zitec/RuleEngineBundle/Service/ContextBase; class MyFirstContext extends ContextBase { protected $dataSource; // Using a custom name will help you identify the context in built expressions. public function getContextObjectKey(): string { return 'my-name'; } // The context will need a data source to read from. We will set the data source when we evaluate the expression. public function setMyDataSource($dataSource) { $this->dataSource = $dataSource; } // Added a parameter. public function getMyParameter(): string { return $this->dataSource->getTheData(); } }
For each parameter (method) in the context object, create a Condition object. The condition services for the three date.time parameters supported by the ContextBase class are already defined in the rule engine bundle. We'll add another one in our new bundle for the added parameter (my_parameter). We can write a condition from scratch, but it's much easier to extend one of the two Abstract condition classes that offer support for basic operators for single-value and array parameters. If "my_parameter" is in fact an array, we can do somthing like this:, (*4)
namespace MyBundle/RuleEngine/Conditions; use Zitec/RuleEngineBundle/Conditions/AbstractArrayCondition; class MyParameter extends AbstractArrayCondition { // When we ask the context object for the method for this condition name, // it should respond with getMyCondition, since the ContextBase does simple snake_case to getCamelCase. protected $name = 'my_parameter'; // This is the name the users will see in admin pages. protected $label = 'My Parameter'; // A help text displayed in the condition builder if the user selects this parameter. protected $description = 'Set conditions for my parameter'; // Here is where we decide on the operators that will be available for this parameter. protected function getOperatorDefinitions(): array { $options = [ ['key' => 'one', 'label' => 'Option one'], ['key' => 'two', 'label' => 'Option two'], ]; // Details about the operator definition structure can be found in the Operator definition section. return [ [ 'label' => 'match ANY of the following', 'name' => $this::INTERSECTING, 'fieldType' => 'select', 'fieldOptions' => [ 'multiple' => true, 'options' => $options, ], ], [ 'label' => 'match NONE of the following', 'name' => $this::DISJOINT, // Details about the autocomplete feature in the Autocomplete section of this readme. 'fieldType' => 'autocomplete', 'fieldOptions' => [ 'autocomplete' => 'my_autocomplete_key', ], ], ]; } }
Now that we have our context object and the matching definitions, let's create a service using RuleConditionsManager. We need to give it your context object as argument and the conditions as âaddSupportedConditionâ calls. Since we extended the ContextBase class, we can use the datetime conditions defined by rule engine too. Below is an example of services defined using the classes above., (*5)
my_bundle.rule_engine.context.my_first_context: class: MyBundle\RuleEngine\Context\MyFirstContext shared: false my_bundle.rule_engine.condition.my_parameter: class: MyBundle\RuleEngine\Conditions\MyParameter public: false my_bundle.rule_engine.manager.my_object: class: Zitec\RuleEngineBundle\Service\RuleConditionsManager arguments: ['@my_bundle.rule_engine.context.my_first_context'] calls: - [addSupportedCondition, ["@rule_engine.condition.current_date"]] - [addSupportedCondition, ["@rule_engine.condition.current_day"]] - [addSupportedCondition, ["@rule_engine.condition.current_time"]] - [addSupportedCondition, ["@my_bundle.rule_engine.condition.my_parameter"]]
Now we need to use the context and conditions to render the front end conditions builder. This can be done by adding a RuleEngineType form type to a form and setting the rule manager service on the 'rule_manager' key in the field options., (*6)
But in most probability, the actions you want to associate with the rules need some data of their own. For instance, we could decide to send emails to various addresses based on the parameters in our object. In that case, we can define a doctrine entity with an email field, to define the addresses, and set it as a rule entity. Doing that is very simple: just add "implements RuleInterface" to your class, and add a "use RuleTrait" statement to actually implement the interface., (*7)
class EmailAddress implements RuleInterface { use RuleTrait; // Your entity's properties and getters/setters follow. }
Update the doctrine schema and notice the brand new relation with the Rule entity that will hold the expressions for your EmailAddress entity., (*8)
Your rule-integrated entity will need to be associated with a context and conditions set, i.e. a rule conditions manager. To do that, you have to add a tag to the conditions manager service declaration. The service above becomes:, (*9)
my_bundle.rule_engine.manager.my_object: class: Zitec\RuleEngineBundle\Service\RuleConditionsManager arguments: ['@my_bundle.rule_engine.context.my_first_context'] calls: - [addSupportedCondition, ["@rule_engine.condition.current_date"]] - [addSupportedCondition, ["@rule_engine.condition.current_day"]] - [addSupportedCondition, ["@rule_engine.condition.current_time"]] - [addSupportedCondition, ["@my_bundle.rule_engine.condition.my_parameter"]] tags: - { name: rule_engine.conditions_manager, entity: "MyBundle:EmailAddress" }
Onwards to the admin section., (*10)
I will assume that you are using SonataAdmin to manage your doctrine entity. If so, this is what you need to do:, (*11)
In the Admin class, add a "use RuleAdminTrait" statement, and use the relevant methods:, (*12)
class EmailAddressAdmin extends AbstractAdmin { use RuleAdminTrait; protected function configureFormFields(FormMapper $formMapper) { // Add the rule admin, using the method from the trait: $this->addRuleFormElement($formMapper); // Add the rest of your fields. } protected function configureListFields(ListMapper $list) { // Add the columns from the rule entity. On dev environments, the generated espression will also be visible. $this->addRuleListColumns($list); // Add the rest of your columns and actions. }
Congratulations! You can now see it in action and set addresses for various cases, using complex rules!, (*13)
Somewhere in your business flow you will need to extract the email address(es) that match your object. The code for that will use the RuleEvaluator service and could look something like this:, (*14)
use Doctrine\ORM\EntityRepository; use MyBundle\RuleEngine\Context\MyFirstContext; use Zitec\RuleEngineBundle\Service\RuleEvaluator; class RecipientChooserService { /** * The entity repository for your EmailAddress entity * @var EntityRepository */ protected $emailAddressRepository; /** * @var RuleEvaluator */ protected $evaluator; /** * @var MyFirstContext */ protected $context; public function __construct( EntityRepository $emailAddressRepository, RuleEvaluator $evaluator, MyFirstContext $context ) { $this->emailAddressRepository = $emailAddressRepository; $this->evaluator = $evaluator; $this->context = $context; } public function getRecipientAddresses($myDataSource) { // Load and filter email addresses. $this->context->setMyDataSource($myDataSource); /** @var EmailAddress[] $emailAddresses */ $emailAddresses = $this->emailAddressRepository->findAll(); // Determine the applicable addresses. $recipients = []; foreach ($emailAddresses as $entity) { if ($this->evaluator->evaluate($entity->getRule(), $this->context)) { $recipients[] = $entity->getEmail(); } } return $recipients; } }
That's it! Done!, (*15)
An operator is an array with these keys: * name (mandatory): the machine name, used to identify the selected operator in a condition * label (mandatory): the text that the user sees in the admin pages * fieldType (mandatory): see possible values below * fieldOptions (optional): see details below * value_transform (optional): a callback to apply to the value received from the rule builder before generating the expression * value_view_transform (optional): a callback to apply to the value received from the rule builder before generating the rule admin view, (*16)
In order to use an autocomplete field, you need to define a data source by implementing the Zitec\RuleEngineBundle\Autocomplete\AutocompleteInterface. If you want to have an autocomplete of doctrine entities, you can extend the AbstractAutocompleteEntity class:, (*17)
use MyBundle\Entity\MyEntity; use Zitec\RuleEngineBundle\Autocomplete\AbstractAutocompleteEntity; class MyEntityAutocomplete extends AbstractAutocompleteEntity { protected function getEntityClass(): string { return MyEntity::class; } protected function getIdField(): string { return 'id'; // This will be the value used in the built expression } protected function getTextField(): string { return 'name'; // This will be the value displayed to the user. } }
and declare the service using a "rule_engine.autocomplete.data_source" tag:, (*18)
my_bundle.rule_engine.autocomplete.my_autocomplete: class: MyBundle\RuleEngine\Autocomplete\MyEntityAutocomplete arguments: ["@doctrine.orm.default_entity_manager"] tags: - { name: rule_engine.autocomplete.data_source, key: my_autocomplete_key }
The value for the key is what you have to use in the fieldOptions for the autocomplete type., (*19)
Don't forget to add the routing info in the routing.yml file of your app:, (*20)
rule_engine: resource: "@ZitecRuleEngineBundle/Resources/config/routing.yml" prefix: /