DEV Community

Nick Ciolpan
Nick Ciolpan

Posted on

Enhancing Models with Meta Attributes: A Dive into Business Logic Beyond the Database

A perfect example of this complexity is when certain fields in your models require additional metadata - a set of attributes that aren't stored directly in the database but are instead derived or computed from existing fields. This metadata often encapsulates business rules and logic that can be tailored to individual model instances.

Web applications, especially those built on top of relational databases, often lean heavily on their models to define and structure their data. These models are typically a direct reflection of the database tables they represent. However, in the complex world of modern web development, sometimes the data in the database isn't enough.

Carrying Business Logic with Meta Attributes

Consider an eCommerce application where each product has a price. This price, though a singular value in the database, might carry with it a wealth of additional information such as currency, tax percentage, discount applicability, and more. Rather than altering the database schema to accommodate these attributes, they can be computed on the fly based on business rules, thus preserving database simplicity while providing enriched data to the application.

Let's dive into this with a concrete example.

The HasMetaAttributes Trait

This PHP trait, designed to be used within a Laravel application, empowers any Eloquent model to handle meta attributes seamlessly:

namespace App\Traits;

use Illuminate\Support\Str;

trait HasMetaAttributes
{
    protected static $globalWithMeta = false;
    protected $instanceWithMeta = null;

    public static function toggleMeta(bool $withMeta = true)
    {
        static::$globalWithMeta = $withMeta;
    }

    public function withMeta(bool $withMeta = true)
    {
        $this->instanceWithMeta = $withMeta;
        return $this;
    }

    public function getAttribute($key)
    {
        $value = parent::getAttribute($key);
        $withMeta = $this->instanceWithMeta !== null ? $this->instanceWithMeta : static::$globalWithMeta;

        if ($withMeta && isset($this->meta[$key])) {
            $meta = $this->meta[$key];
            return [
                'value' => $value,
                'meta' => $meta,
            ];
        }

        return $value;
    }

    public function toArray()
    {
        $attributes = parent::toArray();
        $withMeta = $this->instanceWithMeta !== null ? $this->instanceWithMeta : static::$globalWithMeta;

        if ($withMeta) {
            foreach ($this->meta as $key => $metaData) {
                if (array_key_exists($key, $attributes)) {
                    $attributes['meta'][] = [
                        $key => $metaData,
                    ];
                }
            }
        }

        return $attributes;
    }
}
Enter fullscreen mode Exit fullscreen mode

When used in a model, the trait offers the capability to toggle the meta attributes on or off, either globally across all model instances or on a per-instance basis.

Application in an Eloquent Model

For the sake of illustration, we will use a Product model:

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Model;
use App\\Traits\\HasMetaAttributes;

class Product extends Model
{
    use HasMetaAttributes;

    protected $fillable = ['name', 'price'];
    protected $meta = [
        'price' => [
            'currency' => 'USD',
            'tax' => '10%'
        ]
    ];
}

Enter fullscreen mode Exit fullscreen mode
>>> use App\Models\Product;

>>> Product::toggleMeta(true);

>>> $product = Product::find(1);
>>> echo $product->price;
Enter fullscreen mode Exit fullscreen mode

Output

[
    "value" => 100.00,
    "meta" => [
        "currency" => "USD",
        "tax" => "10%"
    ]
]
Enter fullscreen mode Exit fullscreen mode

Toggle Off

>>> Product::toggleMeta(false);

>>> $product = Product::find(1);
>>> echo $product->price;
Enter fullscreen mode Exit fullscreen mode

Output

100.00
Enter fullscreen mode Exit fullscreen mode

Architectural Considerations and Alternative Approaches

The approach detailed above certainly offers a layer of dynamism to your models. However, depending on the application's complexity and performance needs, some might find alternative designs more fitting:

  1. Database Views: Instead of computing meta attributes in the application, create a database view that joins and aggregates data as required. This will offload computational tasks to the database but might make the application logic less clear.
  2. Service Layer: Rather than enriching models directly, introduce a service layer that fetches model data and then augments it with additional attributes. This separates business logic from data representation and is often a favored approach in complex applications.
  3. Decorator Pattern: Use a decorator to wrap around the model, adding additional behavior or data without modifying the model's structure.
  4. Event Listeners: In scenarios where computed attributes don't change frequently, compute them once and store the results. Use event listeners to re-compute whenever the underlying data changes.

Conclusion

Models, while traditionally reflecting database structures, can be enhanced to carry complex business logic rules that go beyond the scope of the database. The HasMetaAttributes trait provides an elegant way of achieving this in a Laravel application, though alternative architectural patterns might offer other advantages based on specific requirements.

As developers, we must remember that there isn't a one-size-fits-all solution. The best approach always depends on the problem at hand, the current architecture, and future scalability needs. Whichever path you choose, make sure it aligns with both the short-term and long-term goals of your application.

Top comments (2)

Collapse
 
maksimepikhin profile image
Maksim N Epikhin (PIKHTA)

The idea is interesting, but it confuses me that the responses are different. Here, it will not always be easy and understandable for the frontend developers or even for themselves to operate with different response formats.

Collapse
 
nickciolpan profile image
Nick Ciolpan

Hey Makim,

Thank you for reading and expressing your thoughts on this. I acknowledge your concern. In my practice, I never make it part of any public contract, a task better delegated to data transfer objects and API resources. The business logic itself can be better handled within strategies too. It helped at some point with organizing sets of strategies, not showcased in my example above:

'price' => [
    'currency' => 'USD',
    'tax' => 'UnitedStatesDividendTaxStrategy::class',
    'accounting' => 'UnitedStatesDividendAccountingStrategy::class',
]
Enter fullscreen mode Exit fullscreen mode