Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Serializer Expression Bundle Laravel Package

jlm/serializer-expression-bundle

Integrates Symfony ExpressionLanguage with JMS Serializer so serialization metadata can use dynamic expressions for conditional exposure and exclusion. Lets API responses adapt to context, user roles, or runtime values without custom serialization code. Easy Symfony bundle setup.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the hard truth: this is not a Laravel package. jlm/serializer-expression-bundle is a Symfony bundle — a DI Extension, a compiler pass, and a JMS Serializer annotation-driver registration. Laravel's package discovery, container, and config publishing consume none of that. composer require jlm/serializer-expression-bundle in a Laravel 10/11 app will fail on Symfony version constraints, and if you force it through, nothing will wire itself up. Do not plan a day around installing it.

What it actually gives you is one primitive: a @excludeIf("expression") annotation that drops a property from serialized output when a Symfony ExpressionLanguage string evaluates truthy. Everything that follows is about reproducing that primitive in Laravel terms, because that is what day-to-day leverage looks like here.

Where to look first, in priority order:

  1. App\Http\Resources\* — your existing API Resources. Laravel has covered this capability natively for years: $this->when($cond, $value), whenHas(), whenNotNull(), whenLoaded(), whenCounted(), and mergeWhen(). If your question is "should this field be in the response for this caller?", start and probably finish here.
  2. composer show jms/serializer. If the answer is "not installed," close the tab. The package has nothing to attach to.
  3. Only if JMS Serializer is already mandated by an existing codebase (common in DTO-heavy or legacy-migrated apps): the bundle's ~100–300 LOC of glue — an exclusion strategy that reads an annotation, builds an ExpressionLanguage, evaluates it against the object — is the reference you port, not a dependency you install.

First use case to implement (do this one, in Laravel):

Hide internal_notes and cost_basis from the payload unless the authenticated caller is staff.

// app/Http/Resources/ProductResource.php
public function toArray($request): array
{
    return [
        'id'          => $this->id,
        'name'        => $this->name,
        'price'       => $this->price,
        'internal_notes' => $this->when(
            $request->user()?->isStaff(),
            fn () => $this->internal_notes
        ),
        'cost_basis'  => $this->when(
            $request->user()?->can('viewCost'),
            $this->cost_basis
        ),
        'supplier'    => $this->whenLoaded('supplier'),
    ];
}

That is one file, no dependency, no eval surface, fully typed, and testable with $this->actingAs($staff)->getJson(...). It is the same declarative, context-aware field exclusion the bundle exists to provide — with when() the closure is your expression language, and PHP's type system is your parser.

If you genuinely need the JMS path, budget 1–2 dev-days and read JMS\Serializer\Metadata\PropertyMetadata plus ExclusionStrategyInterface before writing anything. Do not port the docblock annotation syntax; JMS 3.x expects PHP attributes, and 2016-era annotation code will not run.

Implementation Patterns

Pattern 1 — Laravel API Resources (the default). Map the four real-world excludeIf cases to the four native helpers:

Product need Laravel idiom
Hide unless caller has role/permission $this->when($user->can('viewPii'), $this->ssn)
Omit null/empty values $this->whenNotNull($this->middle_name)
Include relation only when eager-loaded $this->whenLoaded('orders')
Include count only if it was computed $this->whenCounted('comments')
Group of fields under one condition $this->mergeWhen($cond, ['a' => ..., 'b' => ...])

The key discipline: keep predicates as small named methods on the user/permission layer (isStaff(), can('viewCost')), not inline boolean soup. That keeps the rule auditable — which is the entire compliance argument for the pattern — and lets you unit-test visibility without booting the serializer.

Pattern 2 — Context object + conditional groups, when one DTO serves many callers. If the same resource is rendered under different "contexts" (admin console, public API, internal service), don't branch on $request->user() in twenty places. Pass an explicit context and let the resource read it:

class SerializationContext
{
    public function __construct(
        public readonly bool $isAdmin = false,
        public readonly bool $isInternal = false,
        public readonly string $apiVersion = 'v2',
    ) {}
}

Resolve it once in a middleware or controller and inject it into resources. This mirrors what the bundle did with its runtime serialization context, and it gives you a single seam to swap later.

Pattern 3 — Porting the bundle's logic onto JMS (only if JMS is already in the app). Implement an EventSubscriberInterface on serializer.pre_serialize, or better, a custom ExclusionStrategyInterface. Define your own PHP 8 attribute so metadata is explicit:

#[Attribute(Attribute::TARGET_PROPERTY)]
final class ExcludeIf
{
    public function __construct(public string $expression) {}
}

Then in the subscriber, for each property carrying the attribute, evaluate via symfony/expression-language. Compile and cache expressions — ExpressionLanguage::compile() returns PHP source you can eval-once or write into a CacheItemPool, keyed by the expression string. Without this you re-parse every expression on every property of every object on every request, which is the package's most likely performance defect and your opportunity to do better.

Expose only a safe variable set to the expression (object, context, and nothing else). Resist the urge to register arbitrary application services as expression functions — that is how you rebuild an eval surface by accident.

Pattern 4 — Golden-file tests as the contract. Whether you use Resources or a JMS subscriber, write response tests that assert the exact JSON for a staff caller and a guest caller. This is the only reliable guard against the failure mode where an exclusion silently stops applying. Diff the whole payload; don't assert single keys, or a leaked field elsewhere in the tree goes unnoticed.

Integration tips.

  • If a codebase has some resources on JMS and some on API Resources, standardize before inventing a third path. Mixing serializers in one response tree is a debugging tarpit.
  • Keep visibility rules out of controllers. The moment you write if ($user->isAdmin()) $payload->forget('ssn'), you have re-created the problem the declarative approach solves.
  • Treat $hidden/$visible on Eloquent models as a coarse, static safety net — never as the dynamic mechanism. Model-level hiding applies everywhere including internal jobs and logs, which is usually wrong.

Gotchas and Tips

Fail-open is the defect that matters. Every version of this pattern has a dangerous default: if an expression errors, references an unknown property, or the annotation is simply not read (version drift, wrong serializer cache), the field is included. That's a data-exposure bug, not a cosmetic one. Always test the negative case — assert that a guest response does not contain the sensitive key, and add a test where the expression deliberately throws and assert the property is still absent. With API Resources this is less of a risk because a throwing closure fails the request rather than leaking, but verify it.

Lazy-loading blowups inside the serializer. An expression that touches a relation (object.getCustomer().isVip()) can trigger a query per object. In Laravel with Model::preventLazyLoading() enabled outside production, this fails loudly; without it, you get a silent N+1 at scale. If a visibility predicate needs relational state, load it via with()/withCount() and read the loaded attribute, never traverse the relation inside the predicate.

Don't install this bundle, full stop. It is archived: no PHP 8.3/8.4 work, no Symfony 6/7 compatibility, no security response, no maintainer to escalate to. Four stars and zero dependents means no community signal when it breaks at 2 a.m. Adoption transfers the CVE process to you on day one. If someone insists, vendor it under a named owner with a pinned version and a dated exit plan — treat it as a temporary local patch.

Docblock annotations vs. attributes. Any code sample you find for this bundle will use docblock @excludeIf annotations. JMS Serializer 3.x moved to PHP attributes. A straight copy-paste port silently ignores your annotations and — per fail-open — returns the fields you thought you'd excluded. Tests on stale fixtures will pass.

ExpressionLanguage is an eval-like surface. It is not arbitrary PHP, but it is still a string parsed at runtime and reviewed by humans who will skim it. If your requirement is genuinely complex visibility logic, prefer a closed set of typed predicate objects or explicit read-model DTO classes over annotations on entities. Annotations couple persistence to presentation, which is fine for two fields and miserable for twenty.

Cache the compiled expressions and invalidate on deploy. Cache key should include the expression source plus a build/version stamp. A stale compiled expression after a deploy is a visibility bug that reproduces in production and not locally.

HTTP caching gets harder. Context-dependent payloads are not safely cacheable at the CDN unless you set Vary on the right headers (Authorization, or a custom role header). If your app uses full-response caching, audit any endpoint with conditional fields before turning it on.

Prefer whenLoaded over when(fn () => $this->relation). The former encodes the eager-load contract; the latter will lazily load and quietly undo your query tuning.

Escape hatch for mixed-context DTOs. If one DTO must render differently for five callers, that's a signal to split into per-caller read models rather than accumulate five predicates. The bundle's whole premise — one entity, many payload shapes via annotations — degrades exactly here, and so does the Laravel-native version. Splitting is cheaper than either.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core