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;
}
}
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%'
]
];
}
>>> use App\Models\Product;
>>> Product::toggleMeta(true);
>>> $product = Product::find(1);
>>> echo $product->price;
Output
[
"value" => 100.00,
"meta" => [
"currency" => "USD",
"tax" => "10%"
]
]
Toggle Off
>>> Product::toggleMeta(false);
>>> $product = Product::find(1);
>>> echo $product->price;
Output
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:
- 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.
- 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.
- Decorator Pattern: Use a decorator to wrap around the model, adding additional behavior or data without modifying the model's structure.
- 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)
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.
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: