PHP code example of prohalexey / the-choice

1. Go to this page and download the library: Download prohalexey/the-choice library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

prohalexey / the-choice example snippets




use TheChoice\Builder\JsonBuilder;
use TheChoice\Container;
use TheChoice\Processor\RootProcessor;

// 1. Configure contexts — map names to classes that implement ContextInterface
$container = new Container([
    'visitCount'      => VisitCount::class,
    'hasVipStatus'    => HasVipStatus::class,
    'inGroup'         => InGroup::class,
    'withdrawalCount' => WithdrawalCount::class,
    'depositCount'    => DepositCount::class,
    'getDepositSum'   => GetDepositSum::class,
]);

// 2. Parse rules from a JSON file — returns a Root node (the rule tree)
$parser = $container->get(JsonBuilder::class);
$node = $parser->parseFile('rules/discount-rules.json');

// 3. Execute the rules
$rootProcessor = $container->get(RootProcessor::class);
$result = $rootProcessor->process($node);

use TheChoice\Engine\RuleEngine;

$engine = new RuleEngine($container);

$engine->addRule('vip_discount', $jsonBuilder->parseFile('rules/vip.json'), priority: 10);
$engine->addRule('loyal_discount', $jsonBuilder->parseFile('rules/loyal.json'), priority: 5);
$engine->addRule('fraud_block', $jsonBuilder->parseFile('rules/fraud.json'));

$report = $engine->run();

// Iterate over fired rules
foreach ($report->getFired() as $name => $ruleResult) {
    echo "{$name}: {$ruleResult->result}\n";
}

// Check a specific rule
if ($report->hasFired('vip_discount')) {
    $discount = $report->getResult('vip_discount')->result;
}

// Get skipped rules (result was null or false)
$skipped = $report->getSkipped();

use TheChoice\Registry\RuleRegistry;

$registry = new RuleRegistry();

$registry->register(
    name:        'vip_discount',
    node:        $jsonBuilder->parseFile('rules/vip.json'),
    tags:        ['discount', 'vip'],
    version:     '2.1',
    description: 'VIP discount: 10% of last deposit',
    priority:    10,
);

// Lookup by name
$entry = $registry->get('vip_discount');

// Filter by tag
$discountRules = $registry->findByTag('discount');

// Load into the engine for batch evaluation
$engine->loadFromRegistry($registry);
$report = $engine->run();

use TheChoice\Validator\RuleValidator;

$validator = new RuleValidator(
    contexts:  ['withdrawalCount', 'inGroup', 'getDepositSum'],
    operators: ['equal', 'arrayContain', 'greaterThan'],
);

// Validate and inspect errors
$result = $validator->validate($node);

if (!$result->isValid()) {
    foreach ($result->getErrors() as $error) {
        echo $error->toString();
        // [root > rules > collection[0]] Context "getDisconut" is not registered (did you mean "getDepositSum"?)
    }
}

// Or throw on the first invalid rule (useful in CI/CD)
$validator->validateOrThrow($node); // throws ValidationException

// Only validate operators, allow any context name
$validator = new RuleValidator(contexts: [], operators: ['equal', 'greaterThan']);

use TheChoice\Exception\ValidationException;

try {
    $validator->validateOrThrow($node);
} catch (ValidationException $e) {
    $result = $e->getValidationResult();
    $errors = $result->getErrors(); // array<ValidationError>
    echo $result->toString();       // all errors as a multi-line string
}

$trace = $rootProcessor->processWithTrace($node);

// The result is the same as $rootProcessor->process($node)
echo $trace->getValue(); // e.g. 10.5

// Human-readable explanation
echo $trace->explain();
// Root[root] → 10.5
//   Condition[condition] → 10.5
//     Collection[and] → TRUE
//       Context[withdrawalCount equal] → TRUE
//       Context[inGroup arrayContain] → TRUE
//     Context[getDepositSum] → 10.5

$rootEntry = $trace->getTrace();

echo $rootEntry->getNodeType(); // "Root"
echo $rootEntry->getNodeName(); // "root"
echo $rootEntry->getResult();   // 10.5

foreach ($rootEntry->getChildren() as $child) {
    echo $child->getNodeType();  // "Condition", "Collection", "Context", "Value"
    echo $child->getNodeName();  // e.g. "withdrawalCount equal"
    echo $child->getResult();    // the value returned by this node

    // Children can be nested (e.g. Collection → Context children)
    foreach ($child->getChildren() as $grandChild) {
        // ...
    }
}

use Symfony\Component\EventDispatcher\EventDispatcher;
use TheChoice\Engine\RuleEngine;
use TheChoice\Event\ContextEvaluatedEvent;
use TheChoice\Event\EngineRunAfterEvent;
use TheChoice\Event\RuleFiredEvent;
use TheChoice\Event\RuleErrorEvent;

$dispatcher = new EventDispatcher();

// Metrics — total run time and fired count
$dispatcher->addListener(EngineRunAfterEvent::class, function (EngineRunAfterEvent $e): void {
    echo sprintf("Engine finished in %.2f ms, %d rules fired\n", $e->elapsedMs, count($e->report->getFired()));
});

// Audit — log every fired rule
$dispatcher->addListener(RuleFiredEvent::class, function (RuleFiredEvent $e): void {
    echo sprintf("Rule '%s' fired with result: %s (%.2f ms)\n", $e->ruleName, var_export($e->result->result, true), $e->elapsedMs);
});

// Debug — see why a context evaluated the way it did
$dispatcher->addListener(ContextEvaluatedEvent::class, function (ContextEvaluatedEvent $e): void {
    echo sprintf("  %s = %s (%s %s) → %s\n",
        $e->contextName,
        var_export($e->contextValue, true),
        $e->operatorName ?? 'no operator',
        var_export($e->operatorValue, true),
        var_export($e->result, true),
    );
});

// Error handling
$dispatcher->addListener(RuleErrorEvent::class, function (RuleErrorEvent $e): void {
    echo sprintf("Rule '%s' failed: %s\n", $e->ruleName, $e->exception->getMessage());
});

$engine = new RuleEngine($container, $dispatcher);
$engine->addRule('vip_discount', $jsonBuilder->parseFile('rules/vip.json'), priority: 10);
$report = $engine->run();

$rootProcessor = $container->get(RootProcessor::class);
$rootProcessor->setEventDispatcher($dispatcher);

$result = $rootProcessor->process($node); // context & switch events will fire

use Psr\SimpleCache\CacheInterface;
use TheChoice\Builder\CachedJsonBuilder;

/** @var CacheInterface $cache */ // any PSR-16 adapter (Symfony Cache, Laravel Cache, etc.)

$builder = new CachedJsonBuilder(
    container: $container,
    cache: $cache,
    ttl: 3600,            // optional, seconds or \DateInterval
    keyPrefix: 'rules.',  // optional
);

// First call: parse → serialize → store
// Subsequent calls: deserialize from cache, no parsing
$node = $builder->parseFile('rules/discount.json');

$container->registerShared('my.shared.service', fn (): object => new MySharedService());
$container->registerTransient('my.transient.service', fn (): object => new MyTransientService());

$customResolver = new App\Rules\CustomOperatorResolver();
$container->registerShared(\TheChoice\Operator\OperatorResolverInterface::class, static fn () => $customResolver);
$resolver = $container->get(\TheChoice\Operator\OperatorResolverInterface::class);

use TheChoice\Context\ContextInterface;

class MyContext implements ContextInterface
{
    public function getValue(): mixed
    {
        return 42; // your business logic here
    }
}

$container = new Container(['myContext' => MyContext::class]);

use TheChoice\Context\ContextInterface;
use TheChoice\Operator\AbstractOperator;

class BetweenExclusive extends AbstractOperator
{
    public static function getOperatorName(): string
    {
        return 'betweenExclusive';
    }

    public function assert(ContextInterface $context): bool
    {
        $value = $context->getValue();
        [$min, $max] = $this->getValue();

        return $value > $min && $value < $max;
    }
}

// Register in the resolver
$resolver = $container->get(\TheChoice\Operator\OperatorResolverInterface::class);
$resolver->register('betweenExclusive', BetweenExclusive::class);

use TheChoice\Builder\RuleBuilder;
use TheChoice\Node\Collection;

$root = RuleBuilder::root()
    ->rules(
        RuleBuilder::condition()
            ->if(
                RuleBuilder::collection(Collection::TYPE_AND)
                    ->add(RuleBuilder::context('withdrawalCount')->equal(0))
                    ->add(RuleBuilder::context('inGroup')->arrayContain(['vip', 'premium']))
            )
            ->then(
                RuleBuilder::context('getDepositSum')->modifier('$context * 0.1')
            )
            ->else(RuleBuilder::value(5))
    )
    ->build();

$result = $rootProcessor->process($root);

RuleBuilder::context('depositCount')
    ->equal(2)               // strict equality
    ->notEqual(0)
    ->greaterThan(100)
    ->greaterThanOrEqual(100)
    ->lowerThan(1000)
    ->lowerThanOrEqual(999)
    ->numericInRange([1, 100])        // inclusive range
    ->arrayContain('vip')
    ->arrayNotContain('banned')
    ->containsKey('discount')
    ->countEqual(3)
    ->countGreaterThan(0)
    ->stringContain('prefix')
    ->stringNotContain('spam')
    ->startsWith('VIP-')
    ->endsWith('.ru')
    ->matchesRegex('/^\d{4}$/')
    ->isEmpty()              // no value needed
    ->isNull()               // no value needed
    ->isInstanceOf(MyClass::class);

RuleBuilder::context('amount')
    ->modifier('$context * 0.1')           // append one modifier
    ->modifiers(['$context * 2', '...'])   // replace all modifiers
    ->params(['discountType' => 'vip'])    // context parameters
    ->priority(10)                         // sort priority in collections
    ->description('10% of deposit')
    ->stoppable()                          // store result on Root and stop
    ->build();

RuleBuilder::collection(Collection::TYPE_AT_LEAST)
    ->count(2)
    ->add(RuleBuilder::context('withdrawalCount')->equal(0))
    ->add(RuleBuilder::context('visitCount')->greaterThan(5))
    ->add(RuleBuilder::context('hasVipStatus')->equal(true))
    ->build();

// Equality dispatch (default operator: equal)
RuleBuilder::switch('userRole')
    ->case('admin', RuleBuilder::value(100))
    ->case('manager', RuleBuilder::value(50))
    ->default(RuleBuilder::value(0));

// Range dispatch with named operator
RuleBuilder::switch('depositSum')
    ->caseOp('greaterThan', 10000, RuleBuilder::value('platinum'))
    ->caseOp('greaterThan', 5000,  RuleBuilder::value('gold'))
    ->caseOp('greaterThan', 1000,  RuleBuilder::value('silver'))
    ->default(RuleBuilder::value('bronze'));

// Pre-configured operator instance
RuleBuilder::switch('amount')
    ->caseWith(new GreaterThan()->setValue(5000), RuleBuilder::value('gold'));

RuleBuilder::root()
    ->rules(NodeBuilderInterface $node)   // n('Discount rules v3')
    ->build(); // → Root (with all child roots propagated)

use TheChoice\Exporter\JsonNodeExporter;
use TheChoice\Exporter\NodeSerializer;
use TheChoice\Exporter\YamlNodeExporter;

$serializer   = new NodeSerializer();
$jsonExporter = new JsonNodeExporter($serializer);
$yamlExporter = new YamlNodeExporter($serializer);

// From a parsed file, a RuleBuilder tree, or any other Node source:
$node = $jsonBuilder->parseFile('rules/discount.json');

// ── JSON ──────────────────────────────────────────────────────────────
$pretty  = $jsonExporter->export($node);               // pretty-printed (default)
$compact = $jsonExporter->export($node, pretty: false); // compact / minified

$jsonExporter->exportToFile($node, 'rules/discount.json');         // pretty
$jsonExporter->exportToFile($node, 'rules/discount.min.json', pretty: false);

// ── YAML ──────────────────────────────────────────────────────────────
$yaml = $yamlExporter->export($node);                  // default: inline=4, indent=2
$yaml = $yamlExporter->export($node, inline: 6);       // expand deeper before going inline

$yamlExporter->exportToFile($node, 'rules/discount.yaml');

$array = $serializer->toArray($node);
// → ['node' => 'root', 'rules' => ['node' => 'condition', ...]]