"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
- 247