"Afternoon swing" - 1978

"Afternoon swing" - 1978

Symfony Online June 2023

story

A communication

@florianm__

user

customer

seller

patient

student

@florianm__

admin

user

user

admin

@florianm__

customer

Florian Merle

@florianm__
Florian-Merle
AKAWAKA

developer

@florianm__
@florianm__

user

customer

admin

customer
$user
@florianm__
$customer
@florianm__
customer

The customer is eligible for a discount

@florianm__

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

@florianm__
@florianm__
$customer->orders
  ->filter(/* placed in 2022 */)
  ->count() > 0

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

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

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

@florianm__

The customer placed an order in 2022, and he is registered to the loyalty program

or he is the boss son

$customer->orders
  ->filter(/* placed in 2022 */)
  ->count() > 0

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

|| $customer->isTheBossSon();

Expression language

@florianm__

Expression language

@florianm__
$services->set('indexBuilder')
  ->arg(
    '$indexName',
    expr("env('index_prefix') ~ '_' ~ parameter('main_index_name')"),
  );
@florianm__
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');
  }
}
@florianm__

Expression language

@florianm__
$el = new ExpressionLanguage();

$sum = $el->evaluate('a + b', [
  'a' => 1,
  'b' => 2,
]);
@florianm__
$el = new ExpressionLanguage();

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

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

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

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

$el = new ExpressionLanguage();

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

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

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

$el = new ExpressionLanguage();

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

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

@florianm__

Syntax tree

@florianm__

Symfony is a great framework

@florianm__

Symfony is a great framework

VERB

NOUN

ADJ

NOUN

@florianm__

DET

NOUN PHRASE

@florianm__

Symfony is a great framework

VERB

NOUN

ADJ

NOUN

DET

VERB

NOUN

VERBAL PHRASE

@florianm__

NOUN PHRASE

Symfony is a great framework

VERB

NOUN

ADJ

NOUN

DET

VERB

NOUN

NOUN

PHRASE

@florianm__

VERBAL PHRASE

NOUN PHRASE

Symfony is a great framework

VERB

NOUN

ADJ

NOUN

DET

VERB

NOUN

NOUN

P
N
VP
V
NP
D
A
N
@florianm__

PHRASE

VERBAL PHRASE

NOUN PHRASE

Symfony is a great framework

VERB

NOUN

ADJ

NOUN

DET

VERB

NOUN

NOUN

@florianm__
(boost + ratings.average) / 2
(boost + ratings.average) / 2

VAR.

OP.

(

VAR.

)

OP.

CST.

.

CST.

@florianm__

VAR.

OP.

(

PROPERTY ACCESSOR

)

OP.

CST.

@florianm__
(boost + ratings.average) / 2

VAR.

OP.

(

VAR.

)

OP.

CST.

.

CST.

ADDITION OPERATOR

OP.

CST.

@florianm__

VAR.

OP.

(

PROPERTY ACCESSOR

)

OP.

CST.

(boost + ratings.average) / 2

VAR.

OP.

(

VAR.

)

OP.

CST.

.

CST.

DIVISION OPERATOR

@florianm__

ADDITION OPERATOR

OP.

CST.

VAR.

OP.

(

PROPERTY ACCESSOR

)

OP.

CST.

(boost + ratings.average) / 2

VAR.

OP.

(

VAR.

)

OP.

CST.

.

CST.

/
+
C
V
P
V
C
@florianm__

DIVISION OPERATOR

ADDITION OPERATOR

OP.

CST.

VAR.

OP.

(

PROPERTY ACCESSOR

)

OP.

CST.

(boost + ratings.average) / 2

VAR.

OP.

(

VAR.

)

OP.

CST.

.

CST.

Lexer

foo === bar.baz
// String
@florianm__

Lexer

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

Lexer

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

VAR.

OP.

VAR.

.

CST.

@florianm__
// Character stream
// String

Parser

foo
===
bar
.
baz

PROPERTY ACCESSOR

VAR.

OP.

VAR.

CST.

.

VAR.

OP.

IDENTICAL OPERATOR

===
V
P
V
C
// Tokens
@florianm__

Expression Language

===
V
P
V
C
// Syntax tree
// Magic
@florianm__
customer in loyaltyProgram.registered
@florianm__

specific syntax

internal structure

@florianm__
customer in loyaltyProgram.registered
.registered
in
is_registred_to_the_loyalty_program(customer)
$loyaltyProgram->registered->contains($customer)
@florianm__
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);
  },

)
@florianm__
final class LoyaltyProgramFunctionProvider implements ExpressionFunctionProviderInterface
{
    public function getFunctions(): array
    {
        return [
            new ExpressionFunction('is_registred_to_the_loyalty_program', ...),
            new ExpressionFunction(...),
            // ...
        ];
    }
}
@florianm__
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);
    }
}
@florianm__
$el = new ECommerceExpressionLanguage();

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

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

Demo'

@florianm__
final class UpdateIsEligibleForDiscountRuleAction extends AbstractController
{
  public function __invoke(ECommerceExpressionLanguage $el, RegleRepository $regles)
  {
    if ($form->isSubmitted() && $form->isValid()) {
      $regles->saveEligibleForDiscount($form->get('expression')->getData());
      
      return $this->redirectToRoute('rules');
    }

    return $this->render('update_is_eligible_for_discount_rule.html.twig', [
      'completion' => [
        ...array_map(
          fn ($f) => ['type' => 'function', 'label' => $f->getName()],
          $el->getFunctions(),
        ),
        ...array_map(
          fn ($v) => ['type' => 'variable', 'label' => $v],
          ['customer', 'loyaltyProgram'],
        ),
      ],
    ]);
  }
}
@florianm__
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;
    }
    
    // ...
  }
}
@florianm__
@florianm__

Thanks !

[Symfony Online June 2023] Expression Language

By Florian David Merle

[Symfony Online June 2023] Expression Language

  • 211