MagicFilter
in package
Lazy, immutable chain of predicate operations that resolves against a subject value when `resolve()` is called.
Direct port of upstream magic_filter.magic.MagicFilter
(magic_filter/magic.py:41-291) plus aiogram's local as_() extension
from aiogram/utils/magic_filter.py.
The class exposes three ways to extend a chain:
- PHP
__get($name)—F->message->textresolves to twoGetAttributeOperations on the chain. - PHP
__call($name, $args)—F->message->text->casefold()resolves to[..., GetAttributeOperation('text'), GetAttributeOperation('casefold'), CallOperation([], [])]. We add aGetAttributeOperationfor the name followed by aCallOperationfor the parentheses — same as Python's__getattr__then__call__. - Named instance methods (
equals,gt,func,cast,regexp,in_,contains,as_, …) — terminal-ish operations that append a purpose-built operation directly.
Each operation-append returns a NEW MagicFilter instance — the chain
is immutable so $f = F->text; $g = $f->lower(); doesn't mutate $f.
Logical composition: PHP can't overload operators, so we expose method
forms — $f->and_($g) for AND, $f->or_($g) for OR, $f->not() for
negation. The static Filter::all($f1, $f2) and Filter::any(…) still
work for Filter-level composition; this layer is for MagicFilter-to-
MagicFilter composition mid-chain (the rules that ultimately get bridged
to a Filter via asFilter()).
Resolution: resolve($value) walks the chain operation-by-operation.
Rejections raised by an operation (missing attribute, failed cast, type
mismatch) flip the resolver into a "skip-unless-important" state: the
value becomes null and only important operations (e.g. NOT, OR) run
until the chain ends or an important op rescues the verdict.
Tags
Table of Contents
Constants
- WILDCARD_ALL : string = "\x00magic_filter:wildcard_all\x00"
- Sentinel for `F->items[MagicFilter::WILDCARD_ALL]` — fan-out + ALL semantic. Matches Python's `F[:]` empty-slice case.
- WILDCARD_ANY : string = "\x00magic_filter:wildcard_any\x00"
- Sentinel for `F->items[MagicFilter::WILDCARD_ANY]` — fan-out + ANY semantic. Matches Python's `F[...]` Ellipsis case.
Properties
- $operations : array<int, BaseOperation>
Methods
- __call() : self
- Append a `MethodCallOperation` — PHP doesn't expose bound methods as first-class values the way Python does, so we collapse the upstream `__getattr__` then `__call__` pair into a single op that does `$value->{$name}(...$args)` at resolve time.
- __construct() : mixed
- Direct construction is private-by-convention: the only public seeds are `MagicFilter::root()` (and the global `F` const). Users extend the chain via `__get` / `__call` / named methods rather than building a chain by hand.
- __get() : self
- Append a `GetAttributeOperation` for `$name`. Equivalent of Python's `__getattr__` (`magic.py:99-103`); names starting with an underscore are reserved for PHP internals and raise rather than become chain operations.
- __isset() : bool
- Honour `isset($magic->foo)` — without it PHP emits a warning when `??` / null-checks probe a chain attribute. Always returns true so `__get` runs and produces the chain operation.
- all() : self
- Fan-out + ALL: shorthand for `$f->item(MagicFilter::WILDCARD_ALL)`.
- and_() : self
- AND across chains: `$f->and_($g)` succeeds when both accept. Right- hand operand can be either a `MagicFilter` (resolved against the chain root) or a literal (truthy-AND).
- any() : self
- Fan-out + ANY: shorthand for `$f->item(MagicFilter::WILDCARD_ANY)`.
- as_() : self
- Append the terminal `AsFilterResultOperation` that converts the chain's final value into a kwarg map (`[$name => $value]`) when accepted, or `null` when rejected. Used downstream by `MagicFilterAsFilter` to thread the value into the dispatcher's kwargs bag.
- asBool() : bool
- Truthy check — always `true`. PHP would coerce a `MagicFilter` to `bool` via `is_object($f)` (always truthy) anyway, but we declare the magic method explicitly so `if ($f)` works deterministically and PHPStan can reason about it. Matches upstream's `__bool__` (`magic.py:93-94`).
- asFilter() : Filter
- Wrap this chain in a `Filter` instance so the dispatcher can consume it directly. The bridge's `__invoke` runs the chain against the incoming event and converts the result into a `bool|array` filter verdict — see `MagicFilterAsFilter::__invoke` for the exact rules.
- attr() : self
- Explicit accessor equivalent to `$this->{$name}` — append a `GetAttributeOperation`. The magic-dispatch alias is the more ergonomic form for hand-written DSL chains; this entry point is for codegen and call sites that prefer static type safety.
- casefold() : self
- Alias for `lower` (Python `casefold` is roughly equivalent for ASCII).
- cast() : self
- Apply a unary transformation. Used by users for arbitrary projections: `F->count->cast(intval(...))` to coerce a string to int before a subsequent comparison.
- contains() : self
- `F.text.contains('hello')` — substring (for strings) / containment (for iterables / `ArrayAccess`) predicate on the running value.
- endsWith() : self
- `F.text.endswith('!')` — string-suffix predicate.
- eq() : self
- Alias for `equals` so call sites can read more like Python `eq`.
- equals() : self
- `F.text == 'hi'`.
- extendWith() : self
- Internal-ish: append a generic operation to the chain. Used by the F-DSL builders in `Filters\F\*` and by call sites that need a custom `BaseOperation` subclass beyond the built-ins.
- extract() : self
- Filter an iterable subject by a sub-filter. Mirrors upstream `MagicFilter.extract` (`magic.py:289-290`).
- func() : self
- Apply an arbitrary callable to the running value. Extra args/kwargs are passed BEFORE the value to match upstream `magic.py:283-284`: `function(*args, value, **kwargs)`.
- gt() : self
- gte() : self
- in_() : self
- `F.status.in_({'admin', 'mod'})` → `F->status->in_(['admin', 'mod'])`.
- is() : self
- Strict identity comparison (`===`). PHP equivalent of Python's `is`.
- isNot() : self
- item() : self
- Append a `GetItemOperation`: `$f->item('key')` is the PHP analogue of `F['key']` in Python (PHP doesn't have user-overloadable subscript on instances outside `ArrayAccess`, and we don't want to commit `MagicFilter` to `ArrayAccess` because the dispatcher distinguishes iterables-vs-MagicFilter via instance checks).
- len() : self
- String length / collection size. Mirrors upstream `MagicFilter.len` (`magic.py:250-251`) which wraps `len(value)`. PHP equivalents are `strlen` for strings, `count` for arrays / Countable. The dispatch table picks the right one at resolve time.
- lower() : self
- Lowercase. Equivalent of upstream `F.text.lower()` / `F.text.casefold()`.
- lt() : self
- lte() : self
- ne() : self
- negate() : self
- Alias for `not_()` to read naturally in user code.
- not_() : self
- Negate the chain so far: `$f->not_()` flips the verdict. Implemented via a `NotOperation` (an important function operation wrapping logical NOT) so even a rejected chain (null value) inverts to `true`.
- notContains() : self
- Inverse of `contains`.
- notEquals() : self
- `F.text != 'hi'`.
- notIn() : self
- Inverse of `in_`. Matches upstream `F.not_in(...)` (`magic.py:241-242`).
- or_() : self
- OR across chains. The OR combinator is "important" so a left-hand rejection still gives the right-hand a chance to vote. Mirrors upstream `magic.py:152-155`.
- regexp() : self
- `F.text.regexp('^/(?<cmd>\\w+)')` — anchor / search / fullmatch the pattern against the running value. The resulting chain value is either a match object (a `string[]` for `MATCH`/`SEARCH`/`FULLMATCH`, a `list<string[]>` for `FINDALL`/`FINDITER`) or `null` on no match — which the resolver collapses to `false` at the chain's terminus.
- resolve() : mixed
- Walk the chain against a subject value and return the final value.
- root() : self
- Fresh, empty chain — the equivalent of upstream's bare `F`. Used by typed-builder factories (`MessageF::text()` does `new StringField(MagicFilter::root()->text)`).
- startsWith() : self
- `F.text.startswith('/')` — string-prefix predicate. Rejects when the value isn't a string (the resolver collapses non-string subjects to a chain rejection via `FunctionOperation`'s catch-all).
- upper() : self
- Uppercase. UTF-8 aware via `mb_strtoupper`.
- xor_() : self
- Boolean XOR.
- excludeLast() : self
- Internal: drop the trailing operation. Used by `__invert()` to fold `~~F` back to `F` (matches upstream `magic.py:69-70`).
- extend() : self
- Internal: append an operation and return a new instance. Never mutates `$this`. Mirrors upstream `MagicFilter._extend` (`magic.py:63-64`).
- resolveInner() : mixed
-
splitArgs()
: array{0: list, 1: array
} - Honour `MagicFilter::root()->foo` — without this `__getStatic` PHP still works because `__get` is instance-side; this is documented for completeness only.
Constants
WILDCARD_ALL
Sentinel for `F->items[MagicFilter::WILDCARD_ALL]` — fan-out + ALL semantic. Matches Python's `F[:]` empty-slice case.
public
string
WILDCARD_ALL
= "\x00magic_filter:wildcard_all\x00"
WILDCARD_ANY
Sentinel for `F->items[MagicFilter::WILDCARD_ANY]` — fan-out + ANY semantic. Matches Python's `F[...]` Ellipsis case.
public
string
WILDCARD_ANY
= "\x00magic_filter:wildcard_any\x00"
Properties
$operations
private
array<int, BaseOperation>
$operations
Methods
__call()
Append a `MethodCallOperation` — PHP doesn't expose bound methods as first-class values the way Python does, so we collapse the upstream `__getattr__` then `__call__` pair into a single op that does `$value->{$name}(...$args)` at resolve time.
public
__call(string $name, array<int|string, mixed> $arguments) : self
Mirrors upstream magic_filter.magic.MagicFilter.__call__
(magic.py:141-142) — except the pair of operations becomes one
MethodCallOperation here.
Parameters
- $name : string
- $arguments : array<int|string, mixed>
Return values
self__construct()
Direct construction is private-by-convention: the only public seeds are `MagicFilter::root()` (and the global `F` const). Users extend the chain via `__get` / `__call` / named methods rather than building a chain by hand.
public
__construct([array<int, BaseOperation> $operations = [] ]) : mixed
Parameters
- $operations : array<int, BaseOperation> = []
__get()
Append a `GetAttributeOperation` for `$name`. Equivalent of Python's `__getattr__` (`magic.py:99-103`); names starting with an underscore are reserved for PHP internals and raise rather than become chain operations.
public
__get(string $name) : self
Both $f->message (magic dispatch) and $f->attr('message')
(explicit accessor) reach this code path — call sites that want
to keep PHPStan happy without bespoke property declarations should
prefer attr().
Parameters
- $name : string
Return values
self__isset()
Honour `isset($magic->foo)` — without it PHP emits a warning when `??` / null-checks probe a chain attribute. Always returns true so `__get` runs and produces the chain operation.
public
__isset(string $name) : bool
Parameters
- $name : string
Return values
boolall()
Fan-out + ALL: shorthand for `$f->item(MagicFilter::WILDCARD_ALL)`.
public
all() : self
Equivalent of upstream F[:]. Use mid-chain to apply the rest of
the chain to every element of an iterable.
Return values
selfand_()
AND across chains: `$f->and_($g)` succeeds when both accept. Right- hand operand can be either a `MagicFilter` (resolved against the chain root) or a literal (truthy-AND).
public
and_(mixed $other) : self
and is a reserved word in PHP, hence the trailing underscore.
Parameters
- $other : mixed
Return values
selfany()
Fan-out + ANY: shorthand for `$f->item(MagicFilter::WILDCARD_ANY)`.
public
any() : self
Equivalent of upstream F[...].
Return values
selfas_()
Append the terminal `AsFilterResultOperation` that converts the chain's final value into a kwarg map (`[$name => $value]`) when accepted, or `null` when rejected. Used downstream by `MagicFilterAsFilter` to thread the value into the dispatcher's kwargs bag.
public
as_(string $name) : self
1-for-1 port of aiogram's MagicFilter.as_ (aiogram/utils/magic_filter.py:21-22).
Named as_ (with trailing underscore) because as is a PHP keyword.
Parameters
- $name : string
Return values
selfasBool()
Truthy check — always `true`. PHP would coerce a `MagicFilter` to `bool` via `is_object($f)` (always truthy) anyway, but we declare the magic method explicitly so `if ($f)` works deterministically and PHPStan can reason about it. Matches upstream's `__bool__` (`magic.py:93-94`).
public
asBool() : bool
Return values
boolasFilter()
Wrap this chain in a `Filter` instance so the dispatcher can consume it directly. The bridge's `__invoke` runs the chain against the incoming event and converts the result into a `bool|array` filter verdict — see `MagicFilterAsFilter::__invoke` for the exact rules.
public
asFilter() : Filter
Return values
Filterattr()
Explicit accessor equivalent to `$this->{$name}` — append a `GetAttributeOperation`. The magic-dispatch alias is the more ergonomic form for hand-written DSL chains; this entry point is for codegen and call sites that prefer static type safety.
public
attr(string $name) : self
Upstream exposes this as MagicFilter.attr_ (magic.py:104).
Parameters
- $name : string
Return values
selfcasefold()
Alias for `lower` (Python `casefold` is roughly equivalent for ASCII).
public
casefold() : self
Return values
selfcast()
Apply a unary transformation. Used by users for arbitrary projections: `F->count->cast(intval(...))` to coerce a string to int before a subsequent comparison.
public
cast(callable $func) : self
Mirrors upstream MagicFilter.cast (magic.py:286-287).
Parameters
- $func : callable
Return values
selfcontains()
`F.text.contains('hello')` — substring (for strings) / containment (for iterables / `ArrayAccess`) predicate on the running value.
public
contains(mixed $needle) : self
Mirrors upstream MagicFilter.contains (magic.py:244-245) which
itself wraps the polymorphic contains_op from util.py:18-22.
Parameters
- $needle : mixed
Return values
selfendsWith()
`F.text.endswith('!')` — string-suffix predicate.
public
endsWith(string $suffix) : self
Parameters
- $suffix : string
Return values
selfeq()
Alias for `equals` so call sites can read more like Python `eq`.
public
eq(mixed $other) : self
Parameters
- $other : mixed
Return values
selfequals()
`F.text == 'hi'`.
public
equals(mixed $other) : self
Parameters
- $other : mixed
Return values
selfextendWith()
Internal-ish: append a generic operation to the chain. Used by the F-DSL builders in `Filters\F\*` and by call sites that need a custom `BaseOperation` subclass beyond the built-ins.
public
extendWith(BaseOperation $operation) : self
NOT a substitute for the named methods — equals, func, etc. are
the public surface; this is the trapdoor for advanced users.
Parameters
- $operation : BaseOperation
Return values
selfextract()
Filter an iterable subject by a sub-filter. Mirrors upstream `MagicFilter.extract` (`magic.py:289-290`).
public
extract(self $magic) : self
Parameters
- $magic : self
Return values
selffunc()
Apply an arbitrary callable to the running value. Extra args/kwargs are passed BEFORE the value to match upstream `magic.py:283-284`: `function(*args, value, **kwargs)`.
public
func(callable $function, mixed ...$args) : self
Parameters
- $function : callable
- $args : mixed
-
Positional args resolved against the chain root if any are
MagicFilterinstances.
Return values
selfgt()
public
gt(mixed $other) : self
Parameters
- $other : mixed
Return values
selfgte()
public
gte(mixed $other) : self
Parameters
- $other : mixed
Return values
selfin_()
`F.status.in_({'admin', 'mod'})` → `F->status->in_(['admin', 'mod'])`.
public
in_(iterable<string|int, mixed>|MagicFilter $haystack) : self
Returns true iff the running value is ==-equal to one of $haystack's
entries.
Parameters
- $haystack : iterable<string|int, mixed>|MagicFilter
Return values
selfis()
Strict identity comparison (`===`). PHP equivalent of Python's `is`.
public
is(mixed $other) : self
Not in upstream as a chain method (upstream uses F.is_() with the
underlying operator.is_ callable) — added here for parity with
Python's is operator semantics.
Parameters
- $other : mixed
Return values
selfisNot()
public
isNot(mixed $other) : self
Parameters
- $other : mixed
Return values
selfitem()
Append a `GetItemOperation`: `$f->item('key')` is the PHP analogue of `F['key']` in Python (PHP doesn't have user-overloadable subscript on instances outside `ArrayAccess`, and we don't want to commit `MagicFilter` to `ArrayAccess` because the dispatcher distinguishes iterables-vs-MagicFilter via instance checks).
public
item(mixed $key) : self
When $key is a MagicFilter we treat the call as the "select where
inner accepts" semantic — matches F[F.text == 'hi'] upstream.
Parameters
- $key : mixed
Return values
selflen()
String length / collection size. Mirrors upstream `MagicFilter.len` (`magic.py:250-251`) which wraps `len(value)`. PHP equivalents are `strlen` for strings, `count` for arrays / Countable. The dispatch table picks the right one at resolve time.
public
len() : self
Return values
selflower()
Lowercase. Equivalent of upstream `F.text.lower()` / `F.text.casefold()`.
public
lower() : self
In PHP mb_strtolower handles UTF-8 correctly; we use it instead of
strtolower for the same Unicode-correctness guarantee Python gives.
Return values
selflt()
public
lt(mixed $other) : self
Parameters
- $other : mixed
Return values
selflte()
public
lte(mixed $other) : self
Parameters
- $other : mixed
Return values
selfne()
public
ne(mixed $other) : self
Parameters
- $other : mixed
Return values
selfnegate()
Alias for `not_()` to read naturally in user code.
public
negate() : self
Return values
selfnot_()
Negate the chain so far: `$f->not_()` flips the verdict. Implemented via a `NotOperation` (an important function operation wrapping logical NOT) so even a rejected chain (null value) inverts to `true`.
public
not_() : self
$f->not_()->not_() folds back to $f — mirrors upstream
__invert__ (magic.py:132-139). The fold uses instanceof NotOperation
as a typed sentinel rather than a fragile zero-arg structural check.
Named not_ (with trailing underscore) is the user-visible name; PHP
forbids a method literally named not because of the precedence /
spacing rules around not. We expose not_() and the more readable
alias negate().
Return values
selfnotContains()
Inverse of `contains`.
public
notContains(mixed $needle) : self
Parameters
- $needle : mixed
Return values
selfnotEquals()
`F.text != 'hi'`.
public
notEquals(mixed $other) : self
Parameters
- $other : mixed
Return values
selfnotIn()
Inverse of `in_`. Matches upstream `F.not_in(...)` (`magic.py:241-242`).
public
notIn(iterable<string|int, mixed>|MagicFilter $haystack) : self
Parameters
- $haystack : iterable<string|int, mixed>|MagicFilter
Return values
selfor_()
OR across chains. The OR combinator is "important" so a left-hand rejection still gives the right-hand a chance to vote. Mirrors upstream `magic.py:152-155`.
public
or_(mixed $other) : self
Parameters
- $other : mixed
Return values
selfregexp()
`F.text.regexp('^/(?<cmd>\\w+)')` — anchor / search / fullmatch the pattern against the running value. The resulting chain value is either a match object (a `string[]` for `MATCH`/`SEARCH`/`FULLMATCH`, a `list<string[]>` for `FINDALL`/`FINDITER`) or `null` on no match — which the resolver collapses to `false` at the chain's terminus.
public
regexp(string $pattern[, RegexpMode|null $mode = null ][, bool|null $search = null ]) : self
Port of upstream MagicFilter.regexp (magic.py:253-281). The
deprecated search boolean (kept upstream for backwards compat) is
NOT carried over — modern callers should use mode instead.
Parameters
- $pattern : string
-
A raw PCRE pattern WITHOUT delimiters. The operation builds the final delimited form ('/.../u') internally to handle anchoring per mode.
- $mode : RegexpMode|null = null
- $search : bool|null = null
Return values
selfresolve()
Walk the chain against a subject value and return the final value.
public
resolve(mixed $value) : mixed
null propagates through non-important operations after a rejection.
Mirrors upstream MagicFilter.resolve (magic.py:96-97) which
delegates to _resolve.
Parameters
- $value : mixed
root()
Fresh, empty chain — the equivalent of upstream's bare `F`. Used by typed-builder factories (`MessageF::text()` does `new StringField(MagicFilter::root()->text)`).
public
static root() : self
Return values
selfstartsWith()
`F.text.startswith('/')` — string-prefix predicate. Rejects when the value isn't a string (the resolver collapses non-string subjects to a chain rejection via `FunctionOperation`'s catch-all).
public
startsWith(string $prefix) : self
Parameters
- $prefix : string
Return values
selfupper()
Uppercase. UTF-8 aware via `mb_strtoupper`.
public
upper() : self
Return values
selfxor_()
Boolean XOR.
public
xor_(mixed $other) : self
Parameters
- $other : mixed
Return values
selfexcludeLast()
Internal: drop the trailing operation. Used by `__invert()` to fold `~~F` back to `F` (matches upstream `magic.py:69-70`).
private
excludeLast() : self
Return values
selfextend()
Internal: append an operation and return a new instance. Never mutates `$this`. Mirrors upstream `MagicFilter._extend` (`magic.py:63-64`).
private
extend(BaseOperation $operation) : self
Parameters
- $operation : BaseOperation
Return values
selfresolveInner()
private
resolveInner(mixed $initialValue, array<int, BaseOperation> $operations) : mixed
Parameters
- $initialValue : mixed
- $operations : array<int, BaseOperation>
splitArgs()
Honour `MagicFilter::root()->foo` — without this `__getStatic` PHP still works because `__get` is instance-side; this is documented for completeness only.
private
splitArgs(array<int|string, mixed> $arguments) : array{0: list, 1: array}
Split a flat __call argument bag into positional vs named pieces.
PHP gives us a numerically-indexed array for purely positional calls
and string-keyed entries for any named-argument call. We preserve
insertion order so func($a, b: 1, $c) is reassembled correctly.
Parameters
- $arguments : array<int|string, mixed>