PHP Domain Model PHP Domain Model php php

PHP Domain Model


Let's say you organize your objects like so:enter image description here

In order to initialize the whole building object (with levels, rooms, items) you have to provide db layer classes to do the job. One way of fetching everything you need for the tree view of the building is:

(zoom the browser for better view)

zoom for better view

Building will initialize itself with appropriate data depending on the mappers provided as arguments to initializeById method. This approach can also work when initializing levels and rooms. (Note: Reusing those initializeById methods when initializing the whole building will result in a lot of db queries, so I used a little results indexing trick and SQL IN opetator)

class RoomMapper implements RoomMapperInterface {    public function fetchByLevelIds(array $levelIds) {        foreach ($levelIds as $levelId) {            $indexedRooms[$levelId] = array();        }        //SELECT FROM room WHERE level_id IN (comma separated $levelIds)        // ...        //$roomsData = fetchAll();        foreach ($roomsData as $roomData) {            $indexedRooms[$roomData['level_id']][] = $roomData;        }        return $indexedRooms;    }}

Now let's say we have this db schema

enter image description here

And finally some code.

Building

class Building implements BuildingInterface {    /**     * @var int     */    private $id;    /**     * @var string     */    private $name;    /**     * @var LevelInterface[]     */    private $levels = array();    private function setData(array $data) {        $this->id = $data['id'];        $this->name = $data['name'];    }    public function __construct(array $data = NULL) {        if (NULL !== $data) {            $this->setData($data);        }    }    public function addLevel(LevelInterface $level) {        $this->levels[$level->getId()] = $level;    }    /**     * Initializes building data from the database.      * If all mappers are provided all data about levels, rooms and items      * will be initialized     *      * @param BuildingMapperInterface $buildingMapper     * @param LevelMapperInterface $levelMapper     * @param RoomMapperInterface $roomMapper     * @param ItemMapperInterface $itemMapper     */    public function initializeById(BuildingMapperInterface $buildingMapper,             LevelMapperInterface $levelMapper = NULL,             RoomMapperInterface $roomMapper = NULL,             ItemMapperInterface $itemMapper = NULL) {        $buildingData = $buildingMapper->fetchById($this->id);        $this->setData($buildingData);        if (NULL !== $levelMapper) {            //level mapper provided, fetching bulding levels data            $levelsData = $levelMapper->fetchByBuildingId($this->id);            //indexing levels by id            foreach ($levelsData as $levelData) {                $levels[$levelData['id']] = new Level($levelData);            }            //fetching room data for each level in the building            if (NULL !== $roomMapper) {                $levelIds = array_keys($levels);                if (!empty($levelIds)) {                    /**                     * mapper will return an array level rooms                      * indexed by levelId                     * array($levelId => array($room1Data, $room2Data, ...))                     */                    $indexedRooms = $roomMapper->fetchByLevelIds($levelIds);                    $rooms = array();                    foreach ($indexedRooms as $levelId => $levelRooms) {                        //looping through rooms, key is level id                        foreach ($levelRooms as $levelRoomData) {                            $newRoom = new Room($levelRoomData);                            //parent level easy to find                            $levels[$levelId]->addRoom($newRoom);                            //keeping track of all the rooms fetched                             //for easier association if item mapper provided                            $rooms[$newRoom->getId()] = $newRoom;                        }                    }                    if (NULL !== $itemMapper) {                        $roomIds = array_keys($rooms);                        $indexedItems = $itemMapper->fetchByRoomIds($roomIds);                        foreach ($indexedItems as $roomId => $roomItems) {                            foreach ($roomItems as $roomItemData) {                                $newItem = new Item($roomItemData);                                $rooms[$roomId]->addItem($newItem);                            }                        }                    }                }            }            $this->levels = $levels;        }    }}

Level

class Level implements LevelInterface {    private $id;    private $buildingId;    private $number;    /**     * @var RoomInterface[]     */    private $rooms;    private function setData(array $data) {        $this->id = $data['id'];        $this->buildingId = $data['building_id'];        $this->number = $data['number'];    }    public function __construct(array $data = NULL) {        if (NULL !== $data) {            $this->setData($data);        }    }    public function getId() {        return $this->id;    }    public function addRoom(RoomInterface $room) {        $this->rooms[$room->getId()] = $room;    }}

Room

class Room implements RoomInterface {    private $id;    private $levelId;    private $number;    /**     * Items in this room     * @var ItemInterface[]     */    private $items;    private function setData(array $roomData) {        $this->id = $roomData['id'];        $this->levelId = $roomData['level_id'];        $this->number = $roomData['number'];    }    private function getData() {        return array(            'level_id' => $this->levelId,            'number' => $this->number        );    }    public function __construct(array $data = NULL) {        if (NULL !== $data) {            $this->setData($data);        }    }    public function getId() {        return $this->id;    }    public function addItem(ItemInterface $item) {        $this->items[$item->getId()] = $item;    }    /**     * Saves room in the databse, will do an update if room has an id     * @param RoomMapperInterface $roomMapper     */    public function save(RoomMapperInterface $roomMapper) {        if (NULL === $this->id) {            //insert            $roomMapper->insert($this->getData());        } else {            //update            $where['id'] = $this->id;            $roomMapper->update($this->getData(), $where);        }    }}

Item

class Item implements ItemInterface {    private $id;    private $roomId;    private $name;    private function setData(array $data) {        $this->id = $data['id'];        $this->roomId = $data['room_id'];        $this->name = $data['name'];    }    public function __construct(array $data = NULL) {        if (NULL !== $data) {            $this->setData($data);        }    }    /**     * Returns room id (needed for indexing)     * @return int     */    public function getId() {        return $this->id;    }}


This is now pushing the limits to my knowledge.....

The building/level/room/item structure you described sounds perfectly fine to me. Domain-driven design is all about understanding your domain and then modeling the concepts as objects -- if you can describe what you want in simple words, you've already accomplished your task. When you're designing your domain, keep everything else (such as persistence) out of the picture and it'll become much simpler to keep track of things.

This seems to tightly couple the different objects albeit in one direction

There's nothing wrong about that. Buildings in the real world do have floors, rooms etc. and you're simply modeling this fact.

and mappers for each class with higher level mappers using the child mappers

In DDD terminology, these "mappers" are called "repositories". Also, your Building object might be considered an "aggregate" if it owns all the floors/rooms/items within it and if it doesn't make sense to load a Room by itself without the building. In that case, you would only need one BuildingRepository that can load the entire building tree. If you use any modern ORM library, it should automatically do all the mapping work for you (including loading child objects).


If I understand your question right , your main problem is that you are not using abstract classes properly. Basically you should have different classes for each of your building, levels, rooms etc. For example you should have an abstract class Building, an abstract class Levels that is extended by Building and so on, depend on what you want to have exactly, and like that you have a tree building->level->room, but it's more like an double-linked list because each building has an array of level objects and each level has parent an building object. You should also use interfaces as many people ignore them and they will help you and your team a lot in the future.

Regarding building models on a more generic way the best way to do it in my opinion is to have a class that implements the same methods for each type of database or other store method you use. For example you have a mongo database and a mysql database, you will have a class for each of these and they will have methods like add, remove, update, push etc. To be sure that you don't do any mistakes and everything will work properly the best way to do this is to have an interface database that will store the methods and you will not end up using a mongo method somewhere where the mysql method is not defined. You can also define an abstract class for the common methods if they have any. Hope this will be helpful, cheers!