Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid model bloat with eloquent models?

Tags:

php

laravel

I've been trying to make a small game in Laravel in my spare time and I've been running into trouble with how best to structure the application and issues with certain models getting very bloated.

I'm currently using eloquent models and attaching functionality directly to them. So for example, I have a User model, which started out with the below functions

$user->verify()
$user->creditGold()
$user->debitGold()

which seemed reasonable. As I added functionality to the site, the class started getting bigger and unwieldy though, for example:

$user->creditItem()
$user->debitItem()
$user->equipItem()
$user->unequipItem()
$user->moveToTown()
$user->takeQuest()
$user->ban()
$user->unban()
// etc. etc. 

there's a lot of code that feels very unrelated that's been shoved into this one class and it's very messy.

What I've started doing is making helper models that are instantiated and held by the User class. example below

$user->inventory()->creditItem()
$user->inventory()->debitItem()
$user->inventory()->useItem()

It's easy to call and work with but it feels incorrect.

Does anyone have advice for how to best break down a large mass of code that all conceptually belongs to the same entity? I like the idea of functionality being coupled with data because I think that's the most natural way of understanding OO, but would it be better for me to abstract the code out to a Service layer and have service classes that take the user as a parameter and acts on it instead (i.e. $service->giveItemToUser($user, $item) )?

like image 385
fluke Avatar asked Jan 06 '23 20:01

fluke


2 Answers

This is where the principle of SoC (Separation of Concerns) becomes very important. What this means is making sure each piece of your app is only concerned with what it needs to be concerned with.

Separation of Concerns

Lets start by identifying some of the concerns in your User class.

  1. Inventory
  2. Equipment
  3. Quests

The above are the general resources that will be utilized by your user. Each of these also have things they are concerned with:

  1. Inventory
    • Item
  2. Equipment
    • Item
  3. Quests
    • Quest

You can already see we have several separate parts of the user which require the same information.

Separating Logic From State

At this stage, we now need to separate some other concerns. Specifically, the business logic (what we want to do with our data) and the data access layer itself (the ORM/Models). Personally, I like to keep these things separate by using the Repository Pattern. Classes that work on models and are concerned with the overall logic and application process. I feel that models are a representation of state, and should only worry about fetching or persisting that state.

So I split these things out as such:

  1. Models

    • User
    • Item
    • Quest
  2. Repositories (dependencies)

    • UserRepository (User, Item, Inventory, Equipment, Quests)
    • InventoryRepository (Item)
    • EquipmentRepository (Item, Collection)
    • QuestRepository (Quest)

Code Examples

Now this gives me a clear definition of the setup and organization I want. But lets give some example code. This does not concern how the data is persisted (either manually, or via Eloquent relationships, etc).

<?php namespace App\Repositories;

use App\Models\Item;
use Illuminate\Support\Collection;

class Inventory {

    protected $contents;

    public function __construct(Item $item, Collection $contents)
    {
        $this->item = $item;
        $this->contents = $contents;
    }

    public function add(Item $item)
    {
        $this->contents->push($item);
    }

    public function remove(Item $item)
    {
        $this->contents->forget($item->id);
    }

    public function contains(Item $item)
    {
        return $this->contents->has($item->id);
    }
}

The InventoryRepository is only concerned with managing its collection of items. Adding them, removing them and checking if other items are there. To do this it depends on the Collection class and the Item model.

<?php namespace App\Repositories;

use App\Models\Item;

class Equipment {

    protected $slots = [
        'head' => null,
        'body' => null,
        'legs' => null,
        'feet' => null,
        'arms' => null,
    ];

    public function __construct(Item $item)
    {
        $this->item = $item;
    }

    public function get($slot)
    {
        return $this->slots[$slot];
    }

    public function set($slot, Item $item)
    {
        $this->slots[$slot] = $item;
    }

    public function empty($slot)
    {
        $this->slots[$slot] = null;
    }

    public function hasEquipment($slot)
    {
        return !empty($this->get($slot));
    }

    public function isEquipped(Item $item) 
    {
        if ($this->hasEquipment($item->slot))
        {
            return $this->get($item->slot)->id == $item->id;
        }
    }
}

Another class only concerned with the items currently equipped. Equipping, unequipping, etc.

Bringing It All Together

Once you've defined your separate pieces, you can then bring them all into your UserRepository class. By pulling them in as dependencies, the code contained within your UserRepository will be explicitly User management based, while accessing the loaded dependencies gives you all the functionality you require.

<?php App\Repositories;

use App\Models\User;
use App\Repositories\Inventory;
use App\Repositories\Equipment;

class User {

    protected $user;
    protected $quests;
    protected $equipment;
    protected $inventory;

    public function __construct(
        User $user,
        Quests $quests,
        Equipment $equipment,
        Inventory $inventory
    ) {
        $this->user = $user;
        $this->quests = $quests;
        $this->equipment = $equipment;
        $this->inventory = $inventory;
    }

    public function equip(Item $item)
    {
        if ($this->inventory->contains($item))
        {
            $this->equipment->set($item->slot, $item);
        }
    }

    public function unequip(Item $item)
    {
        if ($this->equipment->isEquipped($item)) 
        {
            $this->equipment->empty($item->slot);
        }
    }
}

This is again just a concept for organizing code. How you want to load and persist the data to the DB is up to you within this type of setup. This is also not the only way to organize the code. The takeaway here is how to break your code out into separate parts and concerns to better modularize and isolate functionality into easier to manage and digest bits.

I hope this was helpful, don't hesitate to ask any questions.

like image 82
jardis Avatar answered Jan 09 '23 11:01

jardis


I think that your user has so many responsabilities. Take a look into SOLID principles. Start with Single Responsability Principle. So take out from user inventory actions and put inventory responsabilities into Inventory service for example.

like image 41
gandra404 Avatar answered Jan 09 '23 09:01

gandra404