Aureus ERP

Unifying Product Resources with Laravel Dynamic Relationships

Aureus ERP is a modular platform built with Laravel and Filament, where features such as Products, Sales, Purchases, Inventory, Accounting, and Manufacturing are developed as independent plugins. This modular architecture makes the system flexible, scalable, and easy to extend.

As Aureus ERP grew, multiple plugins duplicated product resources, leading to inconsistent code and UI. We unified them into a single extensible Product Resource using Laravel dynamic relationships and a lightweight schema registry.

The Problem

A Product isn’t owned by one plugin, Sales quotes it, Purchases buys it, Inventory stocks it, and Accounting taxes it. So each plugin shipped its own ProductResource (the Filament class defining the product form, table, and relations).

The result: more than ten near-identical ProductResource classes.

Every small change in a new field, a fixed label had to be repeated in all of them. Miss one and the plugins disagree about what a product looks like. The sharp version: Accounting needs to add tax fields to the product form, but it has no business editing the Products plugin’s files.

So the question was: how can each plugin contribute fields and relationships to one shared resource, without editing the base model or duplicating the form?

Why Traditional Approaches Didn’t Work

Each forces plugins to know about each other. We wanted the reverse: let each plugin announce what it adds, and have the shared resource collect those announcements at runtime.

Understanding Laravel Dynamic Relationships

Normally, a relationship is a method in the model. But here the relationship (productTaxes) and the model (Product) live in different plugins, and we won’t edit Product from Accounting.

Laravel’s resolveRelationUsing attaches a relationship at runtime, from anywhere its docs recommend it for exactly this (package development):

After this, $order->customer works without Order ever declaring the method. One rule: always pass explicit key names — there’s no method for Laravel to inspect, so conventions can’t kick in.

How Aureus ERP Solved It

Two parts, both based on contribution, each plugin declares what it adds from its own service provider.

1. Extend the model. Each plugin adds its relationships in boot():

The base Product stays clean, and productTaxes only exists when Accounting is installed. Inventory, Manufacturing, and future plugins extend it in the same way.

2. Extend the form with a schema registry. Instead of each plugin overriding form(), the form is built once, and plugins register small “modifiers” that adjust it like middleware for a form.

Schema Registry Explained

Aureus ERP ships a SchemaRegistry (Support plugin):

A plugin registers its contribution at boot:

The single base resource builds its form, then applies whatever plugins contributed, sorted by priority, so fields land predictably:

public static function form(Schema $schema): Schema

The dependency points one way: Products expose the registry; every other plugin registers into it. Add a new plugin tomorrow, and it slots in with one register() call; the core never changes.

Before vs After

Before  Accounting shipped its own ProductResource with a ~160-line form() override that rebuilt the entire product form just to add a few tax fields. When the base form changed, the copy silently drifted out of sync. 10 plugins, 10 copies.

After  One ProductResource, one form. Accounting deletes its override and keeps only its navigation; its tax fields arrive through a single register() call, and its relations through resolveRelationUsing().

Challenges and Lessons Learned

Dynamic relationships aren’t real methods, so method_exists($product, ‘productTaxes’) returns false even though $product->productTaxes works. Some Filament internals probe relations with method_exists. For those, we declare a real method via a plugin-owned trait instead of the dynamic registration, keeping the plugin decoupled while satisfying the check.

The same “only when installed” rule applies to pages:

Benefits

Final Thoughts

The trap most modular systems fall into is sharing a concept by copying it. By inverting the dependency, letting each plugin contribute to a shared resource instead of duplicating it, Aureus ERP turned ten brittle product screens into one extensible form. resolveRelationUsing handles the data side; a small SchemaRegistry handles the UI side. If you’re building a plugin-based Laravel or Filament app, it’s a pattern worth stealing.

Want to see how Aureus ERP fits your business? Tell us about your company. You can also read the Laravel docs on dynamic relationships or explore PR #1273 on GitHub.

Exit mobile version