skip to Main Content

I’m working on a web game project which has the concept of items.

Individual instances of items, which a user could have in their inventory, are stored in the database. Each of those rows has a column called item_base which is used in the code to get details about the item. Example:

unique_item_id player_id item_base
3345346 43455 ITEM_ONE
3856739 97212 ITEM_TWO

My ItemBase class:

class ItemBase {

    public $name;
    public $type;
    public $action;

    public function __construct(string $name, ItemType $type, callable $action) {
        $this->name = $name;
        $this->type = $type->value;
        $this->action = $action();
    }

}

The ItemType enum is just a standard enum that returns a string to prevent typos.

I’m looking to "register" all of my item bases in the codebase rather than its own table in the database since I want to prevent additional queries. Each item base also has a function ($action) to determine which code should be executed when the item is used, and I’m avoiding putting PHP directly into the database.

Creating a new item base would be like this:

new ItemBase("Item One", ItemType::FOOD, function() {
    //Code to execute when the function is called
});

My goal is to be able to reference item bases with something like ItemBase::ITEM_ONE. Then I can use ItemBase::ITEM_ONE->name or the other variables passed through the construct.

What is the best way to approach this? Being able to have the defined type (which IDEs also support type completion on) is preferred over using an $array[‘string’] lookup. Since enums can’t be backed by an object, can I do something with a registry type class?

I’ve tried creating a class with all of my ItemBases in them and then registering each one similar to this:

public static function ITEM_ONE() {
    return new ItemBase("Item One", ItemType::FOOD, function() {
        //Code to execute when the function is called
});

This then allows me to use ItemBase::ITEM_ONE(), but I’m looking for a method that doesn’t require a function for each item base in the game.

2

Answers


  1. You can use a regsitry class and the __callStatic magic method

    class ItemStore {
        protected static $store = [];
    
        public static function registerItem($key, $name, $type, $action) {
            self::$store[$key] = compact("name", "type", "action");
        }
    
        public static function __callStatic($key, $args) {
            if (array_key_exists($key, self::$store)) {
                return new ItemBase(
                    self::$store[$key]["name"],
                    self::$store[$key]["type"],
                    self::$store[$key]["action"]
                );
            }
            throw new InvalidArgumentException("$key is not a valid item");
        }
    }
    
    // demo
    
    try {
        echo ItemStore::ITEM_ONE()->name;
    } catch (InvalidArgumentException $e) {
        echo $e->getMessage();
    }
    echo "n";
    
    ItemStore::registerItem("ITEM_ONE", "Item One", "Food", function () {
        return "foo";
    });
    echo ItemStore::ITEM_ONE()->name;
    echo "n";
    echo ItemStore::ITEM_ONE()->action;
    

    ITEM_ONE is not a valid item

    Item One

    foo

    You could optionally define the registry stuff on the ItemBase class instead of making a separate ItemStore class.

    Login or Signup to reply.
  2. Why not use the name of the enum entry as key in a lookup array? It is a distinct value, so perfectly usable as key in a map.

    enum ItemType {
      case FOOD;
      case DRINK;
    }
    
    readonly class ItemBase {
    
      public function __construct(
        public string   $name,
        // public ItemType $itemType,   // not needed, as the key in $registry is the link
        public Closure  $action
      ) {}
    
    }
    
    $registry = [
      ItemType::FOOD->name  => new ItemBase('Item One', fn() => 'food'),
      ItemType::DRINK->name => new ItemBase('Item One', fn() => 'drink'),
    ];
    
    echo $registry[ItemType::FOOD->name]->name;
    echo "n";
    echo ($registry[ItemType::FOOD->name]->action)();
    

    Output:

    Item One
    food
    

    Note the parentheses around the lookup and property access for action. The parentheses are necessary because the function call () has higher precedence than the property access with ->.

    Or the same with a (type safe) registry class:

    enum ItemType {
      case FOOD;
      case DRINK;
    }
    
    readonly class ItemBase {
    
      public function __construct(
        public string  $name,
        public ItemType $itemType,
        public Closure $action
      ) {}
    
    }
    
    readonly class Registry implements ArrayAccess {
    
      private array $items;
    
      public function __construct(
        ItemBase ...$itemBases
      ) {
        $items = [];
        foreach ($itemBases as $item) {
          $items[$item->itemType->name] = $item;
        }
        $this->items = $items;
      }
    
      final public function offsetExists(mixed $offset): bool {
        return is_string($offset) && array_key_exists($offset, $this->items);
      }
    
      final public function offsetGet(mixed $offset): mixed {
        return $this->offsetExists($offset) ? $this->items[$offset] : null;
      }
    
      final public function offsetSet(mixed $offset, mixed $value): void {
        throw new RuntimeException();
      }
    
      final public function offsetUnset(mixed $offset): void {
        throw new RuntimeException();
      }
    
    }
    
    $registry = new Registry(
      new ItemBase('Item One', ItemType::FOOD, fn() => 'food'),
      new ItemBase('Item One', ItemType::DRINK, fn() => 'drink'),
    );
    
    echo $registry[ItemType::FOOD->name]->name;
    echo "n";
    echo ( $registry[ItemType::FOOD->name]->action )();
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search