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.
Let's say you organize your objects like so:
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)
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
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!