PHPackages                             eventjet/ausdruck - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. eventjet/ausdruck

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

eventjet/ausdruck
=================

A small expression engine for PHP

0.3.0(2w ago)053.4k↑187.2%[2 issues](https://github.com/eventjet/ausdruck/issues)[1 PRs](https://github.com/eventjet/ausdruck/pulls)MITPHPPHP &gt;=8.3CI passing

Since Oct 6Pushed 2w ago1 watchersCompare

[ Source](https://github.com/eventjet/ausdruck)[ Packagist](https://packagist.org/packages/eventjet/ausdruck)[ RSS](/packages/eventjet-ausdruck/feed)WikiDiscussions master Synced today

READMEChangelog (10)Dependencies (32)Versions (34)Used By (0)

Ausdruck
========

[](#ausdruck)

A small expression engine for PHP.

Quick start
-----------

[](#quick-start)

```
composer require eventjet/ausdruck

```

```
use Eventjet\Ausdruck\Parser\ExpressionParser;
use Eventjet\Ausdruck\Parser\Types;
use Eventjet\Ausdruck\Type;

$expression = ExpressionParser::parse(
    'joe:MyPersonType.name:string()',
    new Types(['MyPersonType' => Type::listOf(Type::string())]),
);
$scope = new Scope(
    // Passing values to the expression
    ['joe' => ['joe']],
    // Custom function definitions
    ['name' => static fn (array $person): string => $person[0]],
);
$name = $expression->evaluate($scope);
assert($name === 'Joe');
```

Documentation
-------------

[](#documentation)

### Accessing scope variables

[](#accessing-scope-variables)

Syntax: `varName:type`

Scope variables are passed from PHP when it calls `evaluate()` on the expression:

```
use Eventjet\Ausdruck\Parser\ExpressionParser;
use Eventjet\Ausdruck\Scope;

$x = ExpressionParser::parse('foo:int')
    ->evaluate(new Scope(['foo' => 123]));
assert($x === 123);
```

#### Examples

[](#examples)

`foo:int`, `foo:list`

See [Types](#types)

### Literals

[](#literals)

- `123`: Integer
- `"foo"`: String
- `1.23`: Float
- `[1, myInt:int, 3]`: List of integers
- `["foo", myString:string, "bar"]`: List of strings

### Operators

[](#operators)

Most operators go between two operands of the same type:

OperatorDescriptionExampleNote`===`Equality`foo:string === "bar"``!==`Inequality`foo:string !== "bar"``-`Subtraction`foo:int - bar:int`Operands must be of type `int` or `float``+`Addition`foo:int + bar:int`Operands must be of type `int` or `float``*`Multiplication`foo:int * bar:int`Operands must be of type `int` or `float``/`Division`foo:int / bar:int`See [Division and modulo](#division-and-modulo)`%`Modulo`foo:int % bar:int`See [Division and modulo](#division-and-modulo)`>`Greater than`foo:int > bar:int`Operands must be of type `int` or `float``>=`Greater than or equal`foo:int >= bar:int`Operands must be of type `int` or `float`` b:int) === c:bool
(a:int / b:int).unwrap:int()
!(a:bool || b:bool)

```

Parentheses only group; they add no node of their own. Redundant ones — a group the precedence would have produced anyway, like `(a:int - b:int) - c:int` — parse fine and simply disappear, so printing an expression back out puts a pair of parentheses exactly where the precedence would otherwise regroup the tree, and nowhere else.

Anywhere an expression is expected, it can be a whole one, not only at the top level. List items, struct field values, function arguments, and lambda bodies are all full expressions, so operators, calls, and field access are available in all of them:

```
[a:int - 1, 10]
{total: a:int - b:int}
foo:string.substr(a:int - 1, 2)
names:list.contains:bool(user:{ name: string }.name)
{matches: needle:string === haystack:string.substr:string(0, 1) || always:bool}

```

### Types

[](#types)

The following types are supported:

- `int`: Integer
- `string`: String
- `bool`: Boolean
- `float`: Floating point number
- `list`: List of type T
- `map`: Map with key type K and value type V
- `fn(A, B) -> R`: Function taking an A and a B and returning an R
- Any other type will be treated as an alias that you will have to provide when parsing the expression: ```
    use Eventjet\Ausdruck\Parser\ExpressionParser;
    use Eventjet\Ausdruck\Type;

    ExpressionParser::parse('foo:MyType', ['MyType' => Type::alias(Type::listOf(Type::string()))]);
    ```

### Functions

[](#functions)

Syntax: `target.functionName:returnType(arg1, arg2, ...)`

The target can be any expression. It will be passed as the first argument to the function.

#### Example

[](#example)

`haystack:list.contains:bool(needle:string)`

#### Generic Signatures

[](#generic-signatures)

A signature can leave a type open instead of naming it, by binding a type variable in front of the parameter list:

```
fn(list, fn(T) -> U) -> list

```

That is the signature of `map`. `T` and `U` are not types; they are decided per call, from the types of the target and the arguments. So `numbers:list.map(|n| n:int > 2)` is a `list` while `names:list.map(|n| n:string)`is a `list` — the same function, two return types, neither of them written down.

A binder is the whole of a variable's scope, so a variable is a type anywhere below the `fn` that binds it, and a name no binder declares is not a variable: it is an alias, or an error. That's enforced for a signature written as a type string, where only a `fn` binder can introduce a name that resolves to a variable. The binder itself is checked against, not just declared: it has to be exactly the variables that turn out to be used in the parameters and the return type, so a written `fn(int) -> int` where neither `T` nor `U` appears anywhere is rejected -- a name in the binder has to earn its place.

In PHP, a type variable is `Type::var()`, and a function type is always `Type::func()`, whichever position it ends up in -- the outermost signature of a declaration, an alias target, or a fixed parameter nested inside another one's own parameters or return type, like a lambda parameter the way `map`'s own is, or a generic callback taken by a custom function. `Type::func()` itself never commits to a binder: declaring a function (`Declarations`) or aliasing one (`Type::alias()`) is what quantifies it, deriving its binder from wherever `Type::var()` turns out to be used in the return type and the parameters -- the same rule a written `fn` binder is checked against. A function type nested inside another one built with `Type::func()` and no binder of its own shares its variables with whichever declaration or alias goes on to quantify them, rather than claiming any of its own. Nothing stops a nested function type from being a self-contained, rank-1-polymorphic value instead -- one already quantified by its own `fn` binder, the same way a written type string nests one -- and a signature like that is opaque to whatever encloses it: the outer one can pass it around, store it in a list or an `Option`, or return it, but never reach through it for a variable of its own. A nested binder may not reuse a name a binder further out already declares, though: the inner variable would shadow the outer one, and shadowing is not allowed -- `fn(fn(T) -> T) -> T` is rejected, `fn(fn(U) -> U) -> T`is fine. That holds whichever door builds the binder: a written `fn` is rejected as it's parsed, and a binder derived by `Declarations`, `Type::alias()`, or `Signature::quantified()` is rejected as it's derived, rather than minting a value that would print as text the parser reads back as a redeclared variable.

Until it's quantified, a value built with `Type::func()` is open: printing it before declaring or aliasing it prints a variable's bare name, or a `fn` missing the binder that would make it valid syntax again. `Signature::quantified($t)->toType()`promotes one directly, the same way `Declarations` and `Type::alias()` do, for the rarer case where neither fits.

Because the call site decides them, the inline return type is rarely worth writing: `foo:list.head()` is already an `Option`. Writing one anyway is still allowed, and is then checked against the inferred one.

```
use Eventjet\Ausdruck\Parser\Declarations;
use Eventjet\Ausdruck\Type;

// zip: fn(list, list) -> list
$zip = Type::func(
    Type::listOf(Type::struct(['a' => Type::var('T'), 'b' => Type::var('U')])),
    [Type::listOf(Type::var('T')), Type::listOf(Type::var('U'))],
);
$declarations = new Declarations(functions: ['zip' => $zip]);
```

A generic higher-order function -- one that itself takes a generic function as a parameter, the way `map` does -- nests a `Type::var()` from the outer signature inside a `Type::func()` for the inner one, built exactly the same way as the outer one:

```
// myMap: fn(list, fn(T) -> U) -> list
$myMap = Type::func(
    Type::listOf(Type::var('U')),
    [Type::listOf(Type::var('T')), Type::func(Type::var('U'), [Type::var('T')])],
);
$declarations = new Declarations(functions: ['myMap' => $myMap]);
```

#### Built-In Functions

[](#built-in-functions)

FunctionSignatureDescriptionExample`count``fn(list) -> int`Returns the number of elements in a list`foo:list.count()``contains``fn(list, T) -> bool`Returns whether a list contains a value`foo:list.contains("bar")``filter``fn(list, fn(T) -> bool) -> list`Returns a new list of the elements matching a [predicate](#lambdas)`foo:list.filter(|i| i:int > 2)``head``fn(list) -> Option`Returns the first element of a list as an `Option``foo:list.head()``isSome``fn(Option) -> bool`Takes an Option and returns whether it is `Some``foo:Option.isSome()``map``fn(list, fn(T) -> U) -> list`Returns a new list with the results of applying a [function](#lambdas)`foo:list.map(|i| i:int - 2)``some``fn(list, fn(T) -> bool) -> bool`Returns whether any element matches a [predicate](#lambdas)`foo:list.some(|item| item:int > 5)``substr``fn(string, int, int) -> string`Returns a substring of a string`foo:string.substr(0, 5)``tail``fn(list) -> list`Returns all elements of a list except the first`foo:list.tail()``take``fn(list, int) -> list`Returns the first n elements of a list`foo:list.take(5)``unique``fn(list) -> list`Returns a list with duplicate elements removed`foo:list.unique()``unwrap``fn(Option) -> T`Returns the value contained in an `Option``foo:Option.unwrap()`The signatures are the ones the parser checks calls against; the target is the first parameter.

#### Custom Functions

[](#custom-functions)

You can pass custom functions along with the scope variables:

```
use Eventjet\Ausdruck\Parser\ExpressionParser;use Eventjet\Ausdruck\Scope;

$scope = new Scope(
    ['foo' => 'My secret'],
    ['mask' => fn (string $str, string $mask) => str_repeat($mask, strlen($str))]
);
$result = ExpressionParser::parse('foo:string.mask("x")')->evaluate($scope);
assert($result === 'xxxxxxxxx');
```

The target of the function/method call (`foo:string` in the example above) will be passed as the first argument to the function.

### Lambdas

[](#lambdas)

Syntax: `|arg1, arg2, ... | expression`

To access an argument, you must specify its type, just like when accessing scope variables.

#### Example

[](#example-1)

`|item| item:int > 5`

###  Health Score

53

—

FairBetter than 96% of packages

Maintenance97

Actively maintained with recent releases

Popularity30

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity60

Established project with proven stability

 Bus Factor1

Top contributor holds 98.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~73 days

Recently: every ~133 days

Total

15

Last Release

15d ago

PHP version history (3 changes)v0.1.0PHP &gt;=8.2

0.1.1PHP &gt;=8.1

0.2.3PHP &gt;=8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/bf84c6852b03fde91d73c1c04b6dabe5d47167d4994a6cf40c11e5cfd18d07e7?d=identicon)[trinet](/maintainers/trinet)

---

Top Contributors

[![MidnightDesign](https://avatars.githubusercontent.com/u/743172?v=4)](https://github.com/MidnightDesign "MidnightDesign (82 commits)")[![rieschl](https://avatars.githubusercontent.com/u/3321556?v=4)](https://github.com/rieschl "rieschl (1 commits)")

---

Tags

expressionenginehacktoberfestphp

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Psalm

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/eventjet-ausdruck/health.svg)

```
[![Health](https://phpackages.com/badges/eventjet-ausdruck/health.svg)](https://phpackages.com/packages/eventjet-ausdruck)
```

###  Alternatives

[react/zmq

ZeroMQ bindings for React.

2501.7M33](/packages/react-zmq)[tattali/mobile-detect-bundle

Symfony 5.x-7.x bundle to detect mobile devices, manage mobile view and redirect to the mobile and tablet version.

381.2M1](/packages/tattali-mobile-detect-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
