From Spreadsheets to Business Rules

floflax.io

🧑‍💼

Jean Marc

To Do

In Progress

Done

Approved

Installation costs

Installation is free if the customer orders at least 60 days before the event.

floflax.io

To Do

In Progress

Done

Approved

Installation costs

Installation is free if the customer orders at least 60 days before the event.

final class EarlyBirdInstallationFeeChecker
{
  public function isFree(Order $order): bool
  {
    $deadline = ($order->getEvent()->getStartDate())
      ->modify('-60 days');

    return $order->getCreatedAt() <= $deadline;
  }
}
floflax.io

To Do

In Progress

Done

Approved

Installation costs v2

Installation is free if the exhibitor has exhibited 3 or more consecutive years, or if their booth surface exceeds 50m².

Installation costs

Installation is free if the customer orders at least 60 days before the event.

😮‍💨

floflax.io

To Do

In Progress

Done

Approved

Installation costs v2

Installation is free if the exhibitor has exhibited 3 or more consecutive years, or if their booth surface exceeds 50m².

Installation costs

Installation is free if the customer orders at least 60 days before the event.

floflax.io
final class LoyaltyInstallationFeeChecker
{
  public function isFree(Order $order): bool
  {
    $customer = $order->getCustomer();
  
    return $this->getConsecutiveYears($customer) >= 3
      || $order->getBooth()->getSurface() > 50;
  }
  
  private function getConsecutiveYears(
    Customer $customer
  ): int {
    // ...
  }
}

To Do

In Progress

Done

Approved

Installation costs v3

Installation is free if the exhibitor has exhibited 2 or more consecutive years and the exhibitor is a sponsor of the event.

Installation costs

Installation is free if the customer orders at least 60 days before the event.

Installation costs v2

Installation is free if the exhibitor has exhibited 3 or more consecutive years, or if their booth surface exceeds 50m².

🤯

floflax.io
floflax.io
formula 🔮
floflax.io
floflax.io
my-website.com
formula 🔮
floflax.io

Hi, I'm Florian

  • PHP & Symfony Dev
  • Sylius Core Team
  • Dev at baksla.sh

eval()

floflax.io

😨

Expression language

floflax.io

Expression language

floflax.io
$services->set(IndexBuilder::class)
  ->arg(
    '$indexName',
    expr("env('index_prefix') ~ '_' ~ parameter('main_index_name')"),
  );
floflax.io
use Symfony\Component\Security\Http\Attribute\IsGranted;

final class ViewPostController
{
  #[IsGranted('"ROLE_ADMIN" in roles or is_granted("VIEW", subject)', subject: 'post')]
  public function __invoke(Post $post): Response
  {
    return new Response('ok');
  }
}
floflax.io

Expression language

floflax.io
$el = new ExpressionLanguage();

$isAGreatCustomer = $el->evaluate(
  '((100 * order.discount) / order.total) < 5',
  ['order' => $customer->getLastOrder()]),
);
floflax.io
$customer->orders
  ->filter(/* placed in 2025 */)
  ->count() > 0

&& $loyaltyProgram->customers
  ->contains($customer)

&& $customer->billingAddress
  ->country() === 'France';

The customer placed an order in 2025, and he is registered to the loyalty program and his billing address is located in France

floflax.io
$el = new ExpressionLanguage();

$ruleToBeEligible = <<<EOT
  placed_an_order_in(customer, 2025)
  &&
  is_registered_to_the_loyalty_program(customer)
  &&
  billing_address_located_in(customer, "France")
EOT;

$eligible = $el->evaluate(
  $ruleToBeEligible,
  ['customer' => $customer],
);

The customer placed an order in 2025, and he is registered to the loyalty program and his billing address is located in France

floflax.io
$el = new ExpressionLanguage();

$eligible = $el->evaluate(
  $rules->findEligibleForDiscount(),
  ['customer' => $customer],
);

The customer placed an order in 2025, and he is registered to the loyalty program and his billing address is located in France

floflax.io
$el = new ExpressionLanguage();

$eligible = $el->evaluate(
  $rules->findEligibleForDiscount(),
  ['customer' => $customer],
);
floflax.io

The customer placed an order in 2025, and he is registered to the loyalty program and his billing address is located in France

Syntax tree

floflax.io

Symfony is a great framework

floflax.io

Symfony is a great framework

VERB

NOUN

ADJ

NOUN

DET

floflax.io

NOUN PHRASE

Symfony is a great framework

VERB

NOUN

ADJ

NOUN

DET

VERB

NOUN

floflax.io

VERBAL PHRASE

NOUN PHRASE

Symfony is a great framework

VERB

NOUN

ADJ

NOUN

DET

VERB

NOUN

NOUN

floflax.io

PHRASE

VERBAL PHRASE

NOUN PHRASE

Symfony is a great framework

VERB

NOUN

ADJ

NOUN

DET

VERB

NOUN

NOUN

floflax.io
P
N
VP
V
NP
D
A
N

PHRASE

VERBAL PHRASE

NOUN PHRASE

Symfony is a great framework

VERB

NOUN

ADJ

NOUN

DET

VERB

NOUN

NOUN

floflax.io
(boost + ratings.average) / 2
floflax.io
(boost + ratings.average) / 2

VAR.

OP.

(

VAR.

)

OP.

CST.

.

CST.

floflax.io

VAR.

OP.

(

PROPERTY ACCESSOR

)

OP.

CST.

(boost + ratings.average) / 2

VAR.

OP.

(

VAR.

)

OP.

CST.

.

CST.

floflax.io

ADDITION OPERATOR

OP.

CST.

VAR.

OP.

(

PROPERTY ACCESSOR

)

OP.

CST.

(boost + ratings.average) / 2

VAR.

OP.

(

VAR.

)

OP.

CST.

.

CST.

floflax.io

DIVISION OPERATOR

ADDITION OPERATOR

OP.

CST.

VAR.

OP.

(

PROPERTY ACCESSOR

)

OP.

CST.

(boost + ratings.average) / 2

VAR.

OP.

(

VAR.

)

OP.

CST.

.

CST.

floflax.io
/
+
C
V
P
V
C

DIVISION OPERATOR

ADDITION OPERATOR

OP.

CST.

VAR.

OP.

(

PROPERTY ACCESSOR

)

OP.

CST.

(boost + ratings.average) / 2

VAR.

OP.

(

VAR.

)

OP.

CST.

.

CST.

floflax.io

Lexer

foo === bar.baz
// String
floflax.io

Lexer

foo === bar.baz
f
o
o
=
=
=
b
a
r
.
b
a
z
// Character stream
_
_
// String
floflax.io

Lexer

foo === bar.baz
f
o
o
=
=
=
b
a
r
.
b
a
z
foo
===
bar
.
baz
// Tokens

VAR.

OP.

VAR.

.

CST.

// Character stream
// String
floflax.io

Parser

foo
===
bar
.
baz

PROPERTY ACCESSOR

VAR.

OP.

VAR.

CST.

.

VAR.

OP.

IDENTICAL OPERATOR

===
V
P
V
C
// Tokens
floflax.io

Expression Language

===
V
P
V
C
// Syntax tree
// Magic
floflax.io
floflax.io
foo === bar.baz

Lexer

breaks into tokens

Parser

builds the syntax tree

Compiler

walks the tree for a result

customer in loyaltyProgram.registered
floflax.io

🗣️ Language

🧩 Exposing objects

specific syntax

in

internal structure

.registered
is_registred_to_the_loyalty_program(customer)
$loyaltyProgram->registered->contains($customer)
floflax.io

🗣️ Language

new ExpressionFunction(

  name: 'is_registred_to_the_loyalty_program',
  
  compiler: static function (string $s): string {
    // ...
  },

  evaluator: static function (array $args, Customer $c): bool {
    return $args['loyaltyProgram']->registred->contains($c);
  },

)
floflax.io

🗣️ Language

final class LoyaltyProgramFunctionProvider implements ExpressionFunctionProviderInterface
{
    public function getFunctions(): array
    {
        return [
            new ExpressionFunction('is_registred_to_the_loyalty_program', ...),
            new ExpressionFunction(...),
            // ...
        ];
    }
}
floflax.io

🗣️ Language

use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseEL;

final class ECommerceExpressionLanguage extends BaseEL
{
    public function __construct(
      CacheItemPoolInterface $cache = null,
      array $providers = [],
    ) {
        array_unshift($providers, new LoyaltyProgramFunctionProvider());
        array_unshift($providers, new CouponsFunctionProvider());

        parent::__construct($cache, $providers);
    }
}
floflax.io

⚙️

🗣️ Language

final class EligibilityChecker
{
    public function __construct(
        private ECommerceExpressionLanguage $el,
    ) {}

    public function isEligible(string $condition, Customer $customer): bool
    {
        return $this->el->evaluate($condition, [
            'customer' => $customer,
        ]);
    }
}
floflax.io
placed_an_order_in(customer, 2025)
&& is_registered_to_the_loyalty_program(customer)
&& billing_address_located_in(customer, "France")'

🗣️ Language

floflax.io

🧩 Exposing objects

final class EligibilityChecker
{
    public function __construct(
        private ECommerceExpressionLanguage $el,
        private NormalizerInterface $normalizer,
    ) {}

    public function isEligible(string $condition, Customer $customer): bool
    {
        return $this->el->evaluate($condition, [
            'customer' => $this->normalizer->normalize($customer),
        ]);
    }
}
floflax.io
https://jsfiddle.net/FloFlax/8fukw2yo/latest/
final class UpdateIsEligibleForDiscountRuleAction extends AbstractController
{
    public function __invoke(Request $request, RuleRepository $rules): Response
    {
        $expression = $rules->getEligibleForDiscount();

        $form = $this->createForm(ExpressionType::class, $expression);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $rules->saveEligibleForDiscount($form->getData());

            return $this->redirectToRoute('rules');
        }

        return $this->render('update_is_eligible_for_discount_rule.html.twig', [
            'form' => $form,
        ]);
    }
}
floflax.io
$el->lint($expression);
floflax.io
class ExpressionLanguage
{
	/**
     * Validates the syntax of an expression.
     *
     * @param array $names The list of acceptable variable names in the expression
     * @param int-mask-of<Parser::IGNORE_*> $flags
     *
     * @throws SyntaxError When the passed expression is invalid
     */
    public function lint(Expression|string $expression, array $names, int $flags = 0): void
}
['order', 'customer']
readonly final class ApplyDiscountCommandHandler
{
  public function __construct(
    private ECommerceExpressionLanguage $el,
    private RuleRepository $rules,
    private LoyaltyProgram $loyaltyProgram,
  ) {
  }

  public function __invoke(ApplyDiscountCommand $command): void
  {
    $eligible = $el->evaluate($rules->findEligibleForDiscount(), [
      'customer' => $command->customer,
      'loyaltyProgram' => $this->loyaltyProgram,
    ]);
    
    if (!$eligible) {
      return;
    }
    
    // ...
  }
}
floflax.io
floflax.io

Traps

$el->lint($expression);

🐛

$cache = new RedisAdapter(...);
$expressionLanguage = new ExpressionLanguage($cache);

🚀

is_registered_to_the_loyalty_program(customer) or is_sponsor(customer)

🧠

total / items.count

🔍

dividing by 0 ?

Thanks !

floflax.io

[SymfonyDay Montreal 2026] Expression Language

By Florian Merle

[SymfonyDay Montreal 2026] Expression Language

  • 49