Enhancing Models with Meta Attributes: A Dive into Business Logic Beyond the Database
Tags: laravel, php, programming
Published: 2023-10-04 09:26:12

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:

```jsx
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;
    }
}
```

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:

```php
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%'
        ]
    ];
}

```

```jsx
>>> use App\Models\Product;

>>> Product::toggleMeta(true);

>>> $product = Product::find(1);
>>> echo $product->price;
```

Output

```jsx
[
    "value" => 100.00,
    "meta" => [
        "currency" => "USD",
        "tax" => "10%"
    ]
]
```

Toggle Off

```jsx
>>> Product::toggleMeta(false);

>>> $product = Product::find(1);
>>> echo $product->price;
```

Output

```jsx
100.00
```

## 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.