PHP Logosymfony/expression-language

The `symfony/expression-language` component provides a powerful and flexible way to evaluate expressions written in a simple, PHP-like syntax. It allows applications to define and process dynamic logic at runtime, rather than hardcoding all rules.

Purpose and Use Cases:
This component is particularly useful for scenarios where application logic needs to be configurable or dynamic, such as:
* Access Control and Permissions: Defining complex authorization rules (e.g., `is_granted('EDIT', post) or user.isAuthor(post)`).
* Workflow Engines: Specifying conditions for state transitions in a workflow.
* Dynamic Configuration: Allowing administrators to define rules for pricing, discounts, notifications, or form validation.
* Report Generation: Filtering data based on user-defined criteria.
* Business Rules Engines: Implementing custom business logic without code changes.

Key Features:
1. Expression Parsing and Evaluation: It can parse a string expression and evaluate it against a set of variables, returning a result.
2. Rich Syntax: Supports common operators (arithmetic: `+`, `-`, `*`, `/`, `%`; comparison: `==`, `!=`, `<`, `<=`, `>`, `>=`; logical: `and`, `or`, `not`), array and object access (e.g., `user.name`, `items[0]`), and ternary operators (`condition ? value_if_true : value_if_false`).
3. Variables: Expressions can utilize external data passed as variables, making them context-aware.
4. Functions: Built-in functions (like `constant()`, `empty()`) are available, and developers can easily register custom functions to extend its capabilities. Custom functions require both a "compiler" callback (for performance optimization by compiling the expression into native PHP code) and an "evaluator" callback (used when the expression is directly evaluated).
5. Caching: Expressions are parsed into an Abstract Syntax Tree (AST). This AST can be cached (e.g., to a file system or APCu) to avoid repeated parsing, significantly improving performance for frequently evaluated expressions.
6. Extensibility: Custom function providers can be registered to organize and provide a collection of related functions, promoting modularity.
7. Security: While powerful, it's crucial to be mindful of security when evaluating untrusted expressions. Symfony's ExpressionLanguage allows for some sandboxing by controlling available functions and variables, but it's generally best to evaluate expressions only from trusted sources or heavily restrict the available features.

How It Works:
When an expression is evaluated, the component typically goes through two main steps:
1. Parsing: The string expression is tokenized and parsed into an Abstract Syntax Tree (AST).
2. Evaluation/Compilation: The AST is traversed, and the operations are performed using the provided variables and registered functions, ultimately yielding a result (evaluation). Alternatively, for better performance, the AST can be compiled into native PHP code, which is then executed.

Example Code

<?php

require 'vendor/autoload.php';

use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;

// 1. Initialize ExpressionLanguage
$expressionLanguage = new ExpressionLanguage();

echo "--- Basic Evaluation ---\n";
$result1 = $expressionLanguage->evaluate('1 + 2 * (3 - 1)');
echo "Result of '1 + 2 * (3 - 1)': " . $result1 . "\n"; // Output: 5

echo "\n--- Evaluation with Variables ---\n";
$context = [
    'price' => 15.50,
    'quantity' => 3,
    'is_member' => true,
    'discount_rate' => 0.10,
];

// Calculate total with a potential member discount
$expression = 'price * quantity * (is_member ? (1 - discount_rate) : 1)';
$total = $expressionLanguage->evaluate($expression, $context);
echo "Expression: '" . $expression . "'\n";
echo "Context: " . json_encode($context) . "\n";
echo "Total after discount: " . number_format($total, 2) . "\n"; // Output: 41.85 (15.50 * 3 * 0.9)

echo "\n--- Custom Function Registration ---\n";

// Example: Register a custom function 'is_adult'
// The first callable is for the compiler (returns a PHP string for compilation).
// The second callable is for the evaluator (executes the logic directly).
$expressionLanguage->register(
    'is_adult',
    function ($age) { // Compiler: Returns a string representation of the PHP code
        return sprintf('(%s >= 18)', $age);
    },
    function (array $values, $age) { // Evaluator: Executes the logic
        return $age >= 18;
    }
);

$userContext1 = ['user_age' => 25];
$userContext2 = ['user_age' => 16];

$isAdult1 = $expressionLanguage->evaluate('is_adult(user_age)', $userContext1);
echo "Is user with age " . $userContext1['user_age'] . " an adult? " . ($isAdult1 ? 'Yes' : 'No') . "\n"; // Output: Yes

$isAdult2 = $expressionLanguage->evaluate('is_adult(user_age)', $userContext2);
echo "Is user with age " . $userContext2['user_age'] . " an adult? " . ($isAdult2 ? 'Yes' : 'No') . "\n"; // Output: No

echo "\n--- Using 'in' operator and object access (requires an object or array) ---\n";

class User {
    public $roles = [];
    public function __construct(array $roles) {
        $this->roles = $roles;
    }
    public function hasRole(string $role): bool {
        return in_array($role, $this->roles);
    }
}

$adminUser = new User(['ROLE_ADMIN', 'ROLE_EDITOR']);
$guestUser = new User(['ROLE_GUEST']);

$userContext = [
    'user' => $adminUser,
    'allowed_roles' => ['ROLE_ADMIN', 'ROLE_MANAGER']
];

$isAdmin = $expressionLanguage->evaluate('"ROLE_ADMIN" in user.roles', $userContext);
echo "Is admin user in ROLE_ADMIN? " . ($isAdmin ? 'Yes' : 'No') . "\n";

$isAllowed = $expressionLanguage->evaluate('user.hasRole("ROLE_ADMIN") or "ROLE_EDITOR" in allowed_roles', $userContext);
echo "Is admin user allowed? " . ($isAllowed ? 'Yes' : 'No') . "\n";

$isGuestAllowed = $expressionLanguage->evaluate('user.hasRole("ROLE_ADMIN")', ['user' => $guestUser]);
echo "Is guest user allowed (checking ROLE_ADMIN)? " . ($isGuestAllowed ? 'Yes' : 'No') . "\n";