Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel cast model to another model when returning from database

Tags:

php

laravel

Background Information

I have a model called Product. The product can have 3 different stock methods: Serialize, Bulk and None.

  • Serialize is chosen when every physical item has a barcode.
  • Bulk is chosen when we care for the changes in quantities
  • None is chosen when we want to include a product in the inventory but we don't care about the quantity.

What I want to do

I want to create those 3 models (Serialize, Bulk, None) and extend them to the Product. Then to be able to call something like Product::all() and get a collection like this:

Products [
   0   => Serialize[...]
   1   => Serialize[...]
   2   => Bulk[...]
   3   => None[...]
   ...
   500 => Bulk[...]
]

My problem

How can I make laravel cast each object to their respective classes automatically?


Database Schema Database Schema


My Model structure

Product model

class Product extends Model
{
    use HasFactory;

    /**
     * The attributes that aren't mass assignable.
     *
     * @var array
     */
    protected $guarded = ['id'];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'purchase_price' => MoneyCast::class,
        'rental_price'   => MoneyCast::class,
        'is_active'      => 'boolean',
    ];

    /**
     * Get active products only
     *
     * @param Builder $builder
     *
     * @return Builder
     */
    public function scopeActive(Builder $builder): Builder
    {
        return $builder->where('is_active', '=', true);
    }

    /**
     * Get inactive products only
     *
     * @param Builder $builder
     *
     * @return Builder
     */
    public function scopeInactive(Builder $builder): Builder
    {
        return $builder->where('is_active', '=', false);
    }

    /**
     * Set the name and slug when saving to database
     *
     * @param $value
     */
    public function setNameAttribute($value): void
    {
        $this->attributes['name'] = $value;
        $this->attributes['slug'] = Str::slug($value);
    }

    /**
     * Get the products full name
     *
     * @return string
     */
    public function getFullNameAttribute(): string
    {
        return $this->details
            ? "{$this->name} {$this->details}"
            : $this->name;
    }

    /**
     * Calculate total stocks
     *
     * @return int
     */
    abstract public function getTotalAttribute(): int;

    /**
     * Calculate total stocks in quarantine
     *
     * @return int
     */
    abstract public function getQuarantinedAttribute(): int;

    /**
     * Calculate available stocks
     *
     * @return int
     */
    public function getAvailableAttribute(): int
    {
        return $this->getTotalAttribute() - $this->getQuarantinedAttribute();
    }

    /**
     * Get brand
     *
     * @return BelongsTo
     */
    public function brand(): BelongsTo
    {
        return $this->belongsTo(Brand::class);
    }

    /**
     * Get category
     *
     * @return BelongsTo
     */
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

Serialize Model

class Serialize extends Product
{
/**
     * Calculate total stocks
     *
     * @return int
     */
    public function getTotalAttribute(): int
    {
        return $this->stocks()->count();
    }

    /**
     * Calculate total stocks in quarantine
     *
     * @return int
     */
    public function getQuarantinedAttribute(): int
    {
        return $this->stocks->sum(function ($stock) {
            return $stock->repairs()
                         ->where('status', '!=', last(Repair::STATUSES))
                         ->where('is_working', '=', false)
                         ->count();
        });
    }

    /**
     * Get associated stocks
     *
     * @return HasMany
     */
    public function stocks(): HasMany
    {
        return $this->hasMany(Stock::class)->orderBy('barcode');
    }
}

Bulk Model

class Bulk extends Product
{
    /**
     * Calculate total stocks
     *
     * @return int
     */
    public function getTotalAttribute(): int
    {
        return $this->transactions()->sum('quantity');
    }

    /**
     * Calculate total stocks in quarantine
     *
     * @return int
     */
    public function getQuarantinedAttribute(): int
    {
        return $this->stocks->sum(function ($stock) {
            return $stock->repairs()
                         ->where('status', '!=', last(Repair::STATUSES))
                         ->where('is_working', '=', false)
                         ->sum('quantity');
        });
    }

    /**
     * Get bulk stock transactions.
     *
     * @return HasMany
     */
    public function transactions(): HasMany
    {
        return $this->hasMany(Transaction::class)->latest();
    }
}
like image 294
Jose Avatar asked Feb 02 '26 01:02

Jose


2 Answers

You can return a new instance of those sub-types based on the stock method:

Product::all()
  ->map(function($product) {
       switch( $product->stock_method ) {
           case 'Serialize':
               return new Serialize((array) $product);
           case 'Bulk':
               return new Bulk((array) $product);
           // and so on..
       }
       return $product;
  });

You can also override the get method to not have to write this piece of code on every all

class Product extends Model {
    // static method 'all' calls 'get' defined in Illuminate\Database\EloquentBuilder
    public function get($columns = ['*']) {
        return parent::get($columns)
            ->map(function($product) {
               switch( $product->stock_method ) {
                 case 'Serialize':
                   return new Serialize((array) $product);
                 case 'Bulk':
                   return new Bulk((array) $product);
                 // and so on..
              }
             return $product;
            });
    }
like image 111
Sumit Wadhwa Avatar answered Feb 04 '26 16:02

Sumit Wadhwa


Programmatically it's very hard, if not impossible, to cast a parent object to a child object.

Since you are selecting all() products, you could also create 3 separate queries for all 3 product types, where each query has the right conditions, like checking for barcode in the case you mention.

Each query could be fast, of indexes are setup correctly.

For each query you get a collection, so you end up with 3 separate collections. A simple concat() gives you the resulting collection you are looking for.

like image 22
Maarten Veerman Avatar answered Feb 04 '26 16:02

Maarten Veerman



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!