PHP Domain Model
Asked Answered
T

3

12

I have been programming in PHP for several years and have in the past adopted methods of my own to handle data within my applications.

I have built my own MVC in the past and have a reasonable understanding of OOP within php but I know my implementation needs some serious work.

In the past I have used an is-a relationship between a model and a database table. I now know after doing some research that this is not really the best way forward. As far as I understand it I should create models that don't really care about the underlying database (or whatever storage mechanism is to be used) but only care about their actions and their data.

From this I have established that I can create models of lets say for example a Person an this person object could have some Children (human children) that are also Person objects held in an array (with addPerson and removePerson methods, accepting a Person object).

I could then create a PersonMapper that I could use to get a Person with a specific 'id', or to save a Person.

This could then lookup the relationship data in a lookup table and create the associated child objects for the Person that has been requested (if there are any) and likewise save the data in the lookup table on the save command.

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

What if I wanted to model a building with different levels and different rooms within those levels? What if I wanted to place some items in those rooms?

Would I create a class for building, level, room and item

with the following structure.

building can have 1 or many level objects held in an array level can have 1 or many room objects held in an array room can have 1 or many item objects held in an array

and mappers for each class with higher level mappers using the child mappers to populate the arrays (either on request of the top level object or lazy load on request)

This seems to tightly couple the different objects albeit in one direction (ie. a floor does not need to be in a building but a building can have levels)

Is this the correct way to go about things?

Within the view I am wanting to show a building with an option to select a level and then show the level with an option to select a room etc.. but I may also want to show a tree like structure of items in the building and what level and room they are in.

I hope this makes sense. I am just struggling with the concept of nesting objects within each other when the general concept of oop seems to be to separate things.

If someone can help it would be really useful.

Titos answered 11/9, 2012 at 15:30 Comment(7)
the Nesting sounds right. you might try asking this same question on the Programmers SiteUrochrome
Sorry about signing posts, will not do it in future.Titos
Not too sure how code will help. I am mainly concerned about the principle.Titos
Interfaces are your friend. Your objects needn't be directly coupled together such that a building NEEDS levels and levels NEED rooms and so on. You want to focus on the common aspects of such things that a building can have, then put them into an interface and have your Room/Level classes both implement them. All your Building class will now care about is that it can have objects of this interface (which can also have objects of the same interface etc...). Feel free to use a base abstract class instead if you want to isolate common functionality.Levitation
Didn't get the part: "This seems to tightly couple the different objects albeit in one direction" I don't remember of compositions being a kind of tight coupling... That's the OOP nature when building object graphs.Midweek
"In the past I have used an is-a relationship between a model and a database table. I now know after doing some research that this is not really the best way forward". Sounds like Active Record vs Data Mapper. I think AR is usually the best choice, it's simpler, easier to use, and faster to get a prototype with it. That's what Rails use, for instance (and in PHP, you have Propel I think). But if you prefer Data Mapper, you can use Doctrine 2 (php). It will let you have db-agnostic objects, with nesting and everything.Into
I guess you should try entity attribute value (EAV) model , magento system is using thisMargaretemargaretha
B
8

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;
    }

}
Beuthen answered 11/2, 2013 at 4:18 Comment(0)
A
3

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).

Aglitter answered 12/9, 2012 at 2:34 Comment(1)
One small correction, the Aggregate is the building with the floor and all that. The Building itself is at least for some contexts, the Aggreagate Root (the 'main' object that will act like a facade for the whole aggregate). Also, the Repositories are a bit of mappers but not in the ORM sense. A repository may use one or more ORMs. Btw, the ORM's entities are NOT the domain entities, they are persistence objects.Billbillabong
F
0

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!

Fungal answered 11/9, 2012 at 19:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.