3
votes

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();
    }
}
3

3 Answers

4
votes

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;
            });
    }
1
votes

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.

0
votes

Hook into newCollection() on Product model:

class Product extends Model
{
    public function newCollection(array $models = Array())
    {
        // Casting if statements here...
        $casted = $models;

        // then return a collection
        return collect($casted);

        // or maybe even create a custom collection
        return new ProductCollection($casted);

        // Dummy example (run Product::all() to see results)
        return collect([1,2,3]);
    }
}

Great article: https://heera.it/extend-laravel-eloquent-collection-object

Edit: I see now you need unique relationship methods - that will probably not work with this solution.