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

Vom Laravel Package

zolex/vom

zolex/vom generates Vuex ORM models from your Laravel Eloquent models. It inspects models, relationships, and casts to scaffold JavaScript model classes, helping keep frontend and backend schemas aligned while reducing manual duplication.

View on GitHub
Deep Wiki
Context7

Getting Started

VOM (Value Object Mapper) turns raw arrays — JSON decoded payloads, XML-converted structures, legacy API responses — into typed PHP objects and back again, driven entirely by PHP attributes. The mapping logic lives on your DTO, not in a transformer class.

composer require zolex/vom

Requirements are PHP 8.1+ and symfony/expression-language (pulled in automatically). There is no service provider or config file to publish — it is a framework-agnostic library that happens to compose well with Laravel.

Where to look first

  1. src/Mapping/Attribute/ — the full attribute surface (#[Property], #[Normalizer], #[Denormalizer], #[Factory], #[PostDenormalize], …). This is the real documentation; the README only shows the happy path.
  2. The Expression Language docs for Symfony — VOM expressions are plain ExpressionLanguage syntax over the values being mapped.
  3. Any existing Laravel app/DataTransferObjects folder — that is where VOM will pay for itself.

First use case — map a third-party JSON payload that does not match your naming:

use Zolex\VOM\Mapping as VOM;

#[VOM\Model]
class Address
{
    #[VOM\Property('address_line_1')]
    public string $street;

    #[VOM\Property('address_line_2')]
    public ?string $street2 = null;

    #[VOM\Property('zip_code')]
    #[VOM\Normalizer('value')]
    public string $postalCode;
}

#[VOM\Model]
class Customer
{
    #[VOM\Property('id')]
    public int $id;

    #[VOM\Property('address')]
    public Address $address;

    /** @var Address[] */
    #[VOM\Property('addresses')]
    public array $addresses = [];
}

$customer = Vom::denormalize($payload, Customer::class);
$out      = Vom::normalize($customer);   // back to an array for a request body

If your mapper is registered as a singleton in a service provider, you get the 2.2.1 expression cache for free across the whole request/worker:

$this->app->singleton(\Zolex\VOM\Vom::class, fn () => new \Zolex\VOM\Vom());

Implementation Patterns

DTO-as-mapper. Keep models dumb; put all renaming, casting, and shaping on the attribute. Anything that needs branching should be an expression or a named method, never a wrapper service.

Use standard PHP functions in expressions (new in 2.2.1). This removes the most common reason people wrote custom normalizer methods. You can now do string/array cleanup inline:

#[VOM\Property('phone')]
#[VOM\Normalizer("str_replace([' ', '-', '(', ')'], '', value)")]
public string $phone;

#[VOM\Property('tags')]
#[VOM\Normalizer("implode(',', value)")]
public string $tags;              // on the way out

#[VOM\Property('tags')]
#[VOM\Denormalizer("array_filter(array_map('trim', explode(',', value)))")]
public array $tagsIn;             // on the way in

Normalizers run when serialising the object, denormalizers when hydrating it — mixing them up is the #1 source of "why is my field empty" bugs.

Asymmetric attributes. Real payloads rarely round-trip. Declare #[VOM\Normalizer(...)] and #[VOM\Denormalizer(...)] with different expressions on the same property, and use #[VOM\Property(..., normalizer: false)]-style flags (or simply omit the attribute) for write-only/read-only fields like password hashes and computed totals.

Factories for polymorphic payloads. When a nested key can be one of several classes (a type discriminator in a webhook body), reach for #[VOM\Factory] on a static method that inspects the raw data and returns the right class. Do not model this with a conditional expression — expressions cannot instantiate objects.

Post-denormalize for invariants. #[VOM\PostDenormalize] methods are the right place for derived values and lightweight guards. Keep them free of I/O; validate with Laravel Validator::make($vom->normalize($dto), $rules) afterwards so error messages stay in the framework you already know.

Composition in the service layer. Inject the mapper where you already do HTTP calls:

class CrmGateway
{
    public function __construct(private \Zolex\VOM\Vom $vom, private Http $http) {}

    public function customer(int $id): Customer
    {
        $json = $this->http->get("/customers/{$id}")->throw()->json();
        return $this->vom->denormalize($json['data'], Customer::class);
    }
}

Pass a context array through denormalize($data, Class::class, context: ['locale' => app()->getLocale()]) when an expression or factory needs ambient state — this is cleaner than reading app() inside the DTO.

Collections. Map a list with array_map(fn ($row) => $vom->denormalize($row, Customer::class), $rows) rather than a bespoke wrapper; combined with the 2.2.1 cache the per-item cost is dominated by hydration, not by parsing expressions.

Gotchas and Tips

  • Register the mapper as a singleton. 2.2.1 adds expression caching and pre-compilation, but the cache lives on the Vom/ExpressionLanguage instance. Constructing new Vom() per call throws the whole performance win away — and in a loop over thousands of rows that is the difference you were promised.
  • Compilation errors now surface earlier. Because expressions are pre-compiled rather than lazily evaluated, a typo in an expression (bad function name, unbalanced bracket) fails on first use of the DTO instead of on the one record that hits that branch. That is good, but it means a deploy can break a code path your tests only exercised with null data. After deploying, run php artisan optimize:clear and, under Octane, php artisan octane:reload so workers pick up recompiled metadata and a clean cache.
  • Only standard PHP functions are exposed. The 2.2.1 addition registers a curated set (string, array, sprintf-style helpers) — arbitrary userland functions, closures, and arrow functions are not callable from an expression. For anything bespoke, use a static method via a normalizer/denormalizer callable or register your own ExpressionLanguage function provider if you construct the mapper yourself.
  • Expressions run once per value. array_map('trim', …) over a 50-element list is fine; the same call over a 50k-row import is not. If an expression is more than a couple of chained calls, move it into a #[VOM\PostDenormalize] method where you can profile it.
  • null short-circuits are not automatic. Expressions are evaluated on whatever the key held, including null, and str_replace on null is a deprecation in 8.1+. Guard with value is null ? null : str_replace(...) or make the property nullable and let the attribute skip it.
  • Nested vs. flat path syntax. #[VOM\Property('[customer][address][city]')]-style deep paths work, but a single missing intermediate key silently yields null rather than erroring. When a field is required, assert it in a #[VOM\PostDenormalize] method and throw there, so failures are loud and located.
  • Debugging. var_dump($vom->getContext()) inside a post-denormalize method shows you exactly what VOM saw for the current object, including the raw data and any context you passed. For expression problems, temporarily replace the expression with the bare variable ('value') to confirm the input is what you think it is.
  • Don't use it everywhere. For a flat array-to-array reshape, collect()->map() is faster and has no attribute indirection. VOM earns its keep on deep nesting, renamed keys, polymorphic payloads, and round-tripping the same shape in both directions. Introducing it for one three-field DTO will make reviewers ask why.
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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource