kitano/pel-bundle
A Symfony bundle that integrates the PHP Exif Library (PEL), giving your application a service-based way to read, edit, and write EXIF metadata in JPEG and TIFF images through familiar dependency injection and configuration.
This bundle is a thin Symfony DI wrapper around kitano/pel (PHP Expression Language) — a small library that parses a string expression and either evaluates it against a context array or compiles it to PHP for later execution. The bundle itself adds very little; nearly all behaviour lives in the underlying library. Treat the bundle as "service registration + config tree" and read the library docs for semantics.
Minimal setup:
composer require kitano/pel-bundle
Register the bundle. On Symfony 4+/Flex this is usually automatic via config/bundles.php; on older skeletons add it to AppKernel:
// config/bundles.php
Kitano\PelBundle\KitanoPelBundle::class => ['all' => true],
Where to look first — in this order:
DependencyInjection/Configuration.php — this is the entire public config surface. It tells you what is configurable (typically a cache directory / service for compiled expressions). There is nothing else to learn about the bundle.Resources/config/services.xml (or .yaml) — gives you the service ID and any aliases. That service ID is your entry point.kitano/pel README — this is where evaluate(), parse(), execute(), and function/operator registration are documented.First use case: replacing hardcoded conditionals whose shape changes over time — pricing rules, discount eligibility, feature-gate conditions, notification routing, "should this record be visible" filters — with expressions stored as strings (in config, a DB column, or an admin UI) rather than deployed as code.
use Kitano\Pel\Pel;
class DiscountRule
{
public function __construct(private Pel $pel) {}
public function applies(array $order): bool
{
return (bool) $this->pel->evaluate(
'order.total > 100 and order.country in ["FR", "DE"]',
['order' => $order]
);
}
}
If evaluate() is not on the injected service, run bin/console debug:container pel (or grep the services file for the ID) — the alias name varies by bundle version.
evaluate() parses on every call. For any expression evaluated in a loop (per-row filtering, per-request rule checks), parse once and reuse the node:
$rule = $this->pel->parse($expressionString); // expensive
foreach ($rows as $row) {
if ($rule->execute(['row' => $row])) { ... }
}
Cache the parsed node keyed by a hash of the expression string — an in-request array is usually enough; a PSR-6 pool is worth it only if you have many rules.
Do not spray $pel->evaluate() calls across controllers. Create one small service per expression family so the context shape is defined in one place:
final class EligibilityEvaluator
{
public function __construct(
private Pel $pel,
private array $compiled = [],
) {}
public function isEligible(string $rule, Customer $c): bool
{
$node = $this->compiled[$rule] ??= $this->pel->parse($rule);
return (bool) $node->execute([
'customer' => $c,
'age' => $c->getAge(),
'tier' => $c->getTier(),
'now' => new \DateTimeImmutable(),
]);
}
}
Keeping the context array flat-ish (primitives plus a couple of objects) makes expressions readable for whoever writes them — often a non-developer.
If expressions come from a UI or config file, parse them when they are saved (a Symfony validator or a console command) and store only expressions that parsed successfully. Runtime parse failures on a cron job are far worse than a form error.
try {
$this->pel->parse($submittedExpression);
} catch (\Throwable $e) {
$this->addViolation('Expression is invalid: ' . $e->getMessage());
}
For a fixed, small set of expressions, put them in config/packages/kitano_pel.yaml (or your own parameters) and inject them into the evaluator. This gives you version control over rules without recompiling anything else.
If PEL exposes function registration, the idiomatic Symfony move is a tagged-service compiler pass that collects pel.function tagged services and registers them on the Pel instance at container build time. Check whether the bundle already does this before writing it — with 4 stars, it probably does not.
Security is the headline risk. PEL evaluates expressions — it is not a sandbox. Never pass user-supplied expression strings to it without a hardening layer, and never expose expression editing to untrusted users. Even with only approved context variables, PEL's function set and object access can reach further than you expect. If you need user-authored rules, restrict to a whitelist of functions/operators, or pick a purpose-built rules engine with an explicit sandbox.
This package is effectively dormant. 4 stars, 0 dependents, and an old bundle layout. Practical consequences:
composer.json and commit composer.lock.AppKernel-era structure; Flex auto-registration and Symfony 5/6 config conventions may need manual wiring.Pel service yourself in services.yaml and dropping the bundle entirely is a legitimate, often better, option:services:
Kitano\Pel\Pel:
arguments: ['@?cache.app']
public: true
Distinguish parse errors from execution errors. A typo in an expression and an undefined variable in the context both surface as exceptions but mean very different things operationally. Wrap evaluation in a try/catch that logs the expression string alongside the exception — without it, a failing rule in production is very hard to trace.
Undefined variables are a common foot-gun. Depending on PEL's behaviour, referencing a missing context key either throws or silently evaluates to null, and null propagates quietly through comparisons (null > 5 → falsy), producing rules that "never match" with no error. Validate that your context arrays always carry every key the expression references, or add a compile-time check that extracts variable names from the parsed node and asserts they exist.
Cache invalidation. If the bundle offers a compiled-expression cache directory, it is not keyed to your expression strings unless you key it yourself. Clear it on deploy alongside cache:clear, and be aware that cache:clear alone may not be enough if expressions changed but the cache key did not.
Debugging: dump the parsed node (or generated PHP, if PEL exposes a compile()/__toString() path) rather than the expression string — it is the fastest way to see how precedence and and/or grouping were actually interpreted. Also worth writing a one-liner test per expression; expressions are exactly the kind of thing that silently changes meaning when someone "tidies up" the string.
Extension points: the useful ones live in PEL, not the bundle — custom functions, custom operators, and variable resolution. Confirm the bundle does not overwrite the Pel service in a way that discards your registrations (e.g. by using a fresh instance in a factory), otherwise your registerFunction() calls happen on a service nobody uses.
When to skip it entirely. If your expressions are static and few, plain PHP if statements are clearer, testable, and free. This bundle earns its place only when expressions must change without a deploy — and at that point, weigh it against Symfony's own symfony/expression-language, which is actively maintained, has a Security sandbox, and ships with far more documentation. The main reason to choose kitano/pel is if you specifically want its compiled-PHP execution model for performance; otherwise the default should be the Symfony component.
How can I help you explore Laravel packages today?