The idea is to be able to evaluate the same expressions client-side (in Javascript with this library) and server-side (in PHP with the Symfony/ExpressionLanguage).
Below is the current parity of this library with Symfony's ExpressionLanguage features. All items default to supported status.
Category | Feature | Supported |
---|---|---|
Literals | Strings (single and double quotes) | ✅ |
Literals | Numbers (integers, decimals, decimals without leading zero) with underscores | ✅ |
Literals | Arrays (JSON-like [ ... ]) | ✅ |
Literals | Hashes/Objects (JSON-like { key: value }) | ✅ |
Literals | Booleans (true/false) | ✅ |
Literals | null | ✅ |
Literals | Exponential/scientific notation | ✅ |
Literals | Block comments /* ... */ inside expressions | ✅ |
Escapes | Backslash escaping in strings and regexes | ✅ |
Escapes | Control characters need escaping (e.g., \n) | ✅ |
Objects | Access public properties with dot syntax (obj.prop) | ✅ |
Objects | Call methods with dot syntax (obj.method(...)) | ✅ |
Objects | Null-safe operator (obj?.prop / obj?.method()) | ✅ |
Nullish | Null-coalescing operator (a ?? b) | ✅ |
Functions | constant() | ✅ |
Functions | enum() | ✅ |
Functions | min() | ✅ |
Functions | max() | ✅ |
Arrays | Access array items with bracket syntax (arr[...]) | ✅ |
Operators: Arithmetic | +, -, *, /, %, ** | ✅ |
Operators: Bitwise | &, | , ^ | ✅ |
Operators: Bitwise | ~ (not), <<, >> | ✅ |
Operators: Comparison | ==, ===, !=, !==, <, >, <=, >= | ✅ |
Operators: Comparison | matches (regex) | ✅ |
Operators: String tests | contains, starts with, ends with | ✅ |
Operators: Logical | not/!, and/&&, or/||, xor | ✅ |
Operators: String | ~ (concatenation) | ✅ |
Operators: Array | in, not in (strict comparison) | ✅ |
Operators: Numeric | .. (range) | ✅ |
Operators: Ternary | a ? b : c, a ?: b, a ? b | ✅ |
Other | Null-safe operator (?.) | ✅ |
Other | Null-coalescing operator (??) | ✅ |
Precedence | Operator precedence as per Symfony docs | ✅ |
fromPhp() | Supported as fromJavascript() | ✅ |
Symfony Built-ins | Security expression variables | ⛔️ |
Symfony Built-ins | Service container expression variables | ⛔️ |
Symfony Built-ins | Routing expression variables | ⛔️ |
Notes: Symfony Built-ins are not supported in the javascript environment
npm install expression-language
# or
yarn add expression-language
You can also use this library directly in the browser by including it via a script tag:
<!-- Unminified version for development -->
<script src="https://unpkg.com/expression-language/dist/expression-language.js"></script>
<!-- or minified version for production -->
<script src="https://unpkg.com/expression-language/dist/expression-language.min.js"></script>
import {ExpressionLanguage} from "expression-language";
let expressionLanguage = new ExpressionLanguage();
<script src="https://unpkg.com/expression-language/dist/expression-language.min.js"></script>
<script>
// The library is available as a global ExpressionLanguage object
const expressionLanguage = new ExpressionLanguage.ExpressionLanguage();
</script>
A complete browser example is available in the examples/browser-usage.html file.
let result = expressionLanguage.evaluate('1 + 1');
// result is 2.
let result = expressionLanguage.evaluate(
'a > 0 && b != a',
{
a: 1,
b: 2
}
);
// result is true
let expression = 'a[2] === "three" and b.myMethod(a[1]) === "bar two"';
let values = {
a: ["one", "two", "three"],
b: {
myProperty: "foo",
myMethod: function (word) {
return "bar " + word;
}
}
};
let result = expressionLanguage.evaluate(expression, values);
// result is true
You can register functions in two main ways. Make sure to register functions before calling evaluate(), compile(), or parse(); otherwise a LogicException will be thrown.
- Using register(name, compiler, evaluator):
import { ExpressionLanguage } from 'expression-language';
const el = new ExpressionLanguage();
// Define how the function should compile to JavaScript and how it should evaluate at runtime.
el.register(
'double',
// compiler: receives the compiled argument strings and must return JS source
(x) => `((+${x}) * 2)`,
// evaluator: receives (values, ...args) and returns the result
(values, x) => Number(x) * 2
);
console.log(el.evaluate('double(21)')); // 42
console.log(el.compile('double(a)', ['a'])); // '((+a) * 2)'
- Using addFunction with an ExpressionFunction instance:
import { ExpressionLanguage, ExpressionFunction } from 'expression-language';
const el = new ExpressionLanguage();
const timesFn = new ExpressionFunction(
'times',
(a, b) => `(${a} * ${b})`,
(values, a, b) => a * b
);
el.addFunction(timesFn);
console.log(el.evaluate('times(6, 7)')); // 42
Providers are a convenient way to bundle and register multiple functions. A provider exposes a getFunctions() method that returns an array of ExpressionFunction instances. You can register providers via the constructor or with registerProvider().
-
Built-in providers you can use out of the box:
- BasicProvider: isset()
- StringProvider: strtolower, strtoupper, explode, strlen, strstr, stristr, substr
- ArrayProvider: implode, count, array_intersect
- DateProvider: date, strtotime
-
Registering built-in providers:
import { ExpressionLanguage, StringProvider, ArrayProvider, DateProvider, BasicProvider } from 'expression-language';
// Pass providers in the constructor (array or any iterable)
const el = new ExpressionLanguage(null, [
new StringProvider(),
new ArrayProvider(),
new DateProvider(),
new BasicProvider(),
]);
console.log(el.evaluate('strtoupper("hello")')); // 'HELLO'
console.log(el.evaluate('count([1,2,3])')); // 3
console.log(el.evaluate('isset(foo.bar)', { foo: { bar: 1 } })); // true
- Creating your own provider:
import { ExpressionLanguage, ExpressionFunction } from 'expression-language';
class MathProvider {
getFunctions() {
return [
new ExpressionFunction(
'clamp',
(x, min, max) => `Math.min(${max}, Math.max(${min}, ${x}))`,
(values, x, min, max) => Math.min(max, Math.max(min, x))
),
new ExpressionFunction(
'pct',
(value, total) => `(((${value}) / (${total})) * 100)`,
(values, value, total) => (value / total) * 100
)
];
}
}
const el = new ExpressionLanguage();
el.registerProvider(new MathProvider());
console.log(el.evaluate('clamp(150, 0, 100)')); // 100
console.log(el.evaluate('pct(2, 8)')); // 25
Use this helper to wrap an existing JavaScript function (resolved from the global object) as an ExpressionFunction.
Rules and tips:
- If you pass a namespaced/dotted path like 'Math.max', you must also provide an explicit expression function name (e.g., 'max').
- For non-namespaced global functions (e.g., 'myFn'), the expression function name defaults to the same name.
- The function must exist on globalThis (window in browsers, global in Node). If it does not exist, an error is thrown.
Examples:
import { ExpressionLanguage, ExpressionFunction } from 'expression-language';
const el = new ExpressionLanguage();
// 1) Non-namespaced global function
globalThis.mySum = (a, b) => a + b; // or window.mySum in browser
const sumFn = ExpressionFunction.fromJavascript('mySum');
el.addFunction(sumFn);
console.log(el.evaluate('mySum(20, 22)')); // 42
// 2) Namespaced (dotted) function requires an explicit expression name
const maxFn = ExpressionFunction.fromJavascript('Math.max', 'max');
el.addFunction(maxFn);
console.log(el.evaluate('max(1, 3, 2)')); // 3
// Note: min/max are already built-in and compile to Math.min/Math.max.
Note: Register functions or providers before calling evaluate(), compile(), or parse(); late registration will throw a LogicException.
These flags let you relax strict validation when parsing expressions via the high-level API. They are useful for linting or building tools where variables/functions may be unknown at parse time.
- IGNORE_UNKNOWN_VARIABLES: allows names that are not provided in the names list.
- IGNORE_UNKNOWN_FUNCTIONS: allows calling functions that are not registered.
- You can combine flags with bitwise OR (|).
Examples:
import { ExpressionLanguage, IGNORE_UNKNOWN_VARIABLES, IGNORE_UNKNOWN_FUNCTIONS } from 'expression-language';
const el = new ExpressionLanguage();
// 1) Allow unknown variables when parsing via ExpressionLanguage
el.parse('foo.bar', [], IGNORE_UNKNOWN_VARIABLES);
// 2) Allow unknown functions when parsing via ExpressionLanguage
el.parse('myFn()', [], IGNORE_UNKNOWN_FUNCTIONS);
// 3) Allow both unknown functions and variables
el.parse('myFn(foo)', [], IGNORE_UNKNOWN_FUNCTIONS | IGNORE_UNKNOWN_VARIABLES);
Linting:
import { ExpressionLanguage, IGNORE_UNKNOWN_VARIABLES, IGNORE_UNKNOWN_FUNCTIONS } from 'expression-language';
const el = new ExpressionLanguage();
// Validate expressions without executing them
el.lint('a > 0 && myFn(foo)', ['a'], IGNORE_UNKNOWN_FUNCTIONS | IGNORE_UNKNOWN_VARIABLES);
// By default (flags = 0), unknowns throw:
try {
el.lint('myFn(foo)');
} catch (e) {
console.warn('Lint failed as expected:', e.message);
}
Notes:
- Passing null for the names parameter is deprecated; use IGNORE_UNKNOWN_VARIABLES instead when you want to allow unknown variables.
This package uses GitHub Actions for automated workflows:
- NPM Publishing: Automatically publishes to npm when the package version changes
- GitHub Releases: Automatically creates GitHub releases with changelogs and distribution files
If you're maintaining this package, you'll need to set up the following:
-
Generate an npm access token with publish permissions:
- Go to npmjs.com and log in
- Click on your profile picture → Access Tokens
- Click "Generate New Token" → Select "Publish"
-
Add the token to your GitHub repository:
- Go to your GitHub repository → Settings → Secrets and variables → Actions
- Click "New repository secret"
- Name:
NPM_TOKEN
- Value: Your npm access token
- Click "Add secret"
The GitHub release workflow automatically:
- Checks if the package version has changed
- Builds the project to generate distribution files
- Creates a GitHub release with the new version tag
- Generates a changelog based on commit messages
- Attaches the distribution files to the release
No additional setup is required for GitHub releases as it uses the default GITHUB_TOKEN
.
Once set up, any push to the main branch will trigger these workflows when the package version changes.