Silverstripe add/save object to has_one relation - php

I cannot find a clue on how to correctly save a has_one relation in Silverstripe.
class Car extends DataObject {
$has_one = array(
'garage'=>'Garage';
);
}
class Garage extends DataObject {
$has_many = array(
'cars'=>'Car';
);
}
// let's say I have these records in the DB
$g = Garage::get()->ByID(111);
$c = Car::get()->ByID(222);
// I want to do sth like this to define the relation
$c->Garage = $g;
$c->write();
But this code does nothing, no error, but also the relation is not created in the DB.
What I could to is this:
$c->GarageID = $g->ID;
$c->write();
But this does not seem very ORM like...

there doesn't seem to be an extra method for adding has_one relations, but if you want to stick with the ORM, you could do it the other way around:
$g->cars()->add($c);

This question is especially relevant if you have no corresponding has_many relationship, but want to establish an unsaved relationship between two objects.
What worked for me was creating a property, under the initial class, and assigning the unsaved relating object against that. The major limitations are:
Your reference to the most current instance of the object needs to always be the property, otherwise you'll get concurency issues.
Large objects being asigned will weigh down your available memory.
Fortunately, my case was a very simple object.
Example:
Car.php:
. . .
private static $has_one = array(
'Garage' => 'Garage'
);
private $unsaved_relation_garage;
protected function onBeforeWrite() {
parent::onBeforeWrite();
// Save the unsaved relation too
$garage = $this->unsaved_relation_garage;
// Check for unsaved relation
// NOTE: Unsaved relation will override existing
if($garage) {
// Check if garage already exists in db
if(!$garage->exists()) {
// If not, write garage
$garage->write();
}
$this->GarageID = $garage->ID;
}
}
/**
* setGarage() will assign a written garage to this object's has_one 'Garage',
* or an unwritten garage to $this->unsaved_relation_garage. Will not write.
*
* #param Garage $garage
* #return Car
*/
public function setGarage($garage) {
if($garage->exists()) {
$this->GarageID = $garage->ID;
return $this;
}
$this->unsaved_relation_garage = $garage;
return $this;
}
/**
* getGarage() takes advantage of the variation in method names for has_one relationships,
* and will return $this->unsaved_relation_garage or $this->Garage() dependingly.
*
* #return Garage
*/
public function getGarage() {
$unsaved = $this->unsaved_relation_garage;
if($unsaved) {
return $unsaved;
}
if($this->Garage()->exists()) {
return $this->Garage();
}
return null;
}
. . .

Related

Cleaning up my Application Structure... Domain Objects and Data Mappers

I've been developing an application utilizing ZendFramework 1.1 for the better part of two years now, and as-so it has seen a few different stages of refactoring from me learning or trying something new. At its current state, I feel that my structure is pretty good in that I can get stuff done quickly, but could certainly use some improvements in certain areas- where I feel like there is a lot of bloat and awkward dependencies.
Bear with me here as I lay down some example code from my application. I will use an example of an Order object which has OrderItem instances which also must be saved. I will explain all necessary parts of instantiation and saving.
As far as my understanding goes, what I've got going on here is more-so in line with the ActiveRecord design pattern than with Domain Models, though I think I have practices from both...
class Order extends BaseObject {
/** #var OrderItem array of items on the order */
public $items = array();
public function __construct($data = array()){
// Define the attributes for this model
$schema = array(
"id" => "int", // primary key
"order_number" => "string", // user defined
"order_total" => "float", // computed
// etc...
);
// Get datamapper and validator classes
$mf = MapperFactory::getInstance();
$mapper = $mf->get("Order");
$validator = new Order_Validator();
$table = new Application_DbTable_Order();
// Construct parent
parent::__construct($schema, $mapper, $validator, $table);
// If data was provided then parse it
if(count($data)){
$this->parseData($data);
}
// return the instance
return $this;
}
// Runs before a new instance is saved, does some checks
public function addPrehook(){
$orderNumber = $this->getOrderNumber();
if($this->mapper->lookupByOrderNumber($orderNumber)){
// This order number already exists!
$this->addError("An order with the number $orderNumber already exists!");
return false;
}
// all good!
return true;
}
// Runs after the primary data is saved, saves any other associated objects e.g., items
public function addPosthook(){
// save order items
if($this->commitItems() === false){
return false;
}
// all good!
return true;
}
// saves items on the order
private function commitItems($editing = false){
if($editing === true){
// delete any items that have been removed from the order
$existingOrder = Order::getById($this->getId());
$this->deleteRemovedItems($existingOrder);
}
// Iterate over items
foreach($this->items as $idx => $orderItem){
// Ensure the item's order_id is set!
$orderItem->setOrderId($this->getId());
// save the order item
$saved = $orderItem->save();
if($saved === false){
// add errors from the order item to this instance
$this->addError($orderItem->getErrors());
// return false
return false;
}
// update the order item on this instance
$this->items[$idx] = $saved;
}
// done saving items!
return true;
}
/** #return Order|boolean The order matching provided ID or FALSE if not found */
public static function getById($id){
// Get the Order Datamapper
$mf = MapperFactory::getInstance();
$mapper = $mf->get("Order");
// Look for the primary key in the order table
if($mapper->lookup($id)){
return new self($mapper->fetchObjectData($id)->toArray());
}else{
// no order exists with this id
return false;
}
}
}
The parsing of data, saving, and pretty much anything else that applies to all models (a more appropriate term may be Entity) exists in the BaseObject, as so:
class BaseObject {
/** #var array Array of parsed data */
public $data;
public $schema; // valid properties names and types
public $mapper; // datamapper instance
public $validator; // validator instance
public $table; // table gateway instance
public function __construct($schema, $mapper, $validator, $table){
// raise an error if any of the properties of this method are missing
$this->schema = $schema;
$this->mapper = $mapper;
$this->validator = $validator;
$this->table = $table;
}
// parses and validates $data to the instance
public function parseData($data){
foreach($data as $key => $value){
// If this property isn't in schema then skip it
if(!array_key_exists($key, $this->schema)){
continue;
}
// Get the data type of this
switch($this->schema[$key]){
case "int": $setValue = (int)$value; break;
case "string": $setValue = (string)$value; break;
// etc...
default: throw new InvalidException("Invalid data type provided ...");
}
// Does our validator have a handler for this property?
if($this->validator->hasProperty($key) && !$this->validator->isValid($key, $setValue)){
$this->addError($this->validator->getErrors());
return false;
}
// Finally, set property on model
$this->data[$key] = $setValue;
}
}
/**
* Save the instance - Inserts or Updates based on presence of ID
* #return BaseObject|boolean The saved object or FALSE if save fails
*/
public function save(){
// Are we editing an existing instance, or adding a new one?
$action = ($this->getId()) ? "edit" : "add";
$prehook = $action . "Prehook";
$posthook = $action . "Posthook";
// Execute prehook if its there
if(is_callable(array($this, $prehook), true) && $this->$prehook() === FALSE){
// some failure occured and errors are already on the object
return false;
}
// do the actual save
try{
// mapper returns a saved instance with ID if creating
$saved = $this->mapper->save($this);
}catch(Exception $e){
// error occured saving
$this->addError($e->getMessage());
return false;
}
// run the posthook if necessary
if(is_callable(array($this, $posthook), true) && $this->$posthook() === FALSE){
// some failure occured and errors are already on the object
return false;
}
// Save complete!
return $saved;
}
}
The base DataMapper class has very simple implementations for save, insert and update, which are never overloaded because of the $schema being defined per-object. I feel like this is a bit wonky, but it works I guess? Child classes of BaseMapper essentially just provide domain-specific finder functions e.g., lookupOrderByNumber or findUsersWithLastName and other stuff like that.
class BaseMapper {
public function save(BaseObject $obj){
if($obj->getId()){
return $this->update($obj);
}else{
return $this->insert($obj);
}
}
private function insert(BaseObject $obj){
// Get the table where the object should be saved
$table = $obj->getTable();
// Get data to save
$saveData = $obj->getData();
// Do the insert
$table->insert($saveData);
// Set the object's ID
$obj->setId($table->getAdapter()->getLastInsertId());
// Return the object
return $obj;
}
}
I feel like what I have isn't necessarily horrible, but I also feel like there are some not-so-great designs in place here. My concerns are primarily:
Models have a very rigid structure which is tightly coupled to the database table schema, making adding/removing properties from the model or database table a total pain in the butt! I feel like giving all of my objects which save to the database a $table and $mapper in the constructor is a bad idea... How can I avoid this? What can I do to avoid defining $schema?
Validation seems a bit quirky as it is tied very tightly to the property names on the model which also correspond to column names in the database. This further complicates making any database or model changes! Is there a more appropriate place for validation?
DataMappers don't really do much besides provide some complicated finder functions. Saving complex objects is handled entirely by the object class itself (e.g., Order class in my example. Also is there an appropriate term for this type of object, other than 'complex object'? I say that my Order object is "complex" because it has OrderItem objects that it must also save. Should a DataMapper handle the saving logic that currently exists in the Order class?
Many thanks for your time and input!
It's a good practice to separate the concerns between objects as much as possible. Have one responsible for Input Validation, other to perform the business logic, DB operations, etc. In order to keep the 2 objects loosely coupled they should not know anything about each other’s implementation only what they can do. This is defined thru an interface.
I recommend reading this article http://www.javaworld.com/article/2072302/core-java/more-on-getters-and-setters.html and other ones from this guy. He's got a book as well worth reading http://www.amazon.com/Holub-Patterns-Learning-Looking-Professionals/dp/159059388X.
I would separate if possible order and items, I don’t know much about your app but if you need to show a list of 20 orders only with their order numbers then those DB calls and processing regarding order items would be a waste if not separated. This is of course not the only way.
So first you need to know what the order attributes are and encapsulate a way to feed those into an order and also have an order expose that data to other objects.
interface OrderImporter {
public function getId();
public function getOrderNumber();
public function getTotal();
}
interface OrderExporter {
public function setData($id, $number, $total);
}
In order to keep the business logic separate from the database we need to encapsulate that behavior as well like so
interface Mapper {
public function insert();
public function update();
public function delete();
}
Also I would define a specific mapper whose duty is to handle DB operations regarding orders.
interface OrderMapper extends Mapper {
/**
* Returns an object that captures data from an order
* #return OrderExporter
*/
public function getExporter();
/**
* #param string $id
* #return OrderImporter
*/
public function findById($id);
}
Finally an order needs to be able to communicate with all those objects through some messages.
interface Order {
public function __construct(OrderImporter $importer);
public function export(OrderExporter $exporter);
public function save(OrderMapper $orderRow);
}
So far we have a way to provide data to the Order, a way to extract data from the order and a way to interact with the db.
Below I've provided a pretty simple example implementation which is far from perfect.
class OrderController extends Zend_Controller_Action {
public function addAction() {
$requestData = $this->getRequest()->getParams();
$orderForm = new OrderForm();
if ($orderForm->isValid($requestData)) {
$orderForm->populate($requestData);
$order = new ConcreteOrder($orderForm);
$mapper = new ZendOrderMapper(new Zend_Db_Table(array('name' => 'order')));
$order->save($mapper);
}
}
public function readAction() {
//if we need to read an order by id
$mapper = new ZendOrderMapper(new Zend_Db_Table(array('name' => 'order')));
$order = new ConcreteOrder($mapper->findById($this->getRequest()->getParam('orderId')));
}
}
/**
* Order form can be used to perform validation and as a data provider
*/
class OrderForm extends Zend_Form implements OrderImporter {
public function init() {
//TODO setup order input validators
}
public function getId() {
return $this->getElement('orderID')->getValue();
}
public function getOrderNumber() {
return $this->getElement('orderNo')->getValue();
}
public function getTotal() {
return $this->getElement('orderTotal')->getValue();
}
}
/**
* This mapper also serves as an importer and an exporter
* but clients don't know that :)
*/
class ZendOrderMapper implements OrderMapper, OrderImporter, OrderExporter {
/**
* #var Zend_Db_Table_Abstract
*/
private $table;
private $data;
public function __construct(Zend_Db_Table_Abstract $table) {
$this->table = $table;
}
public function setData($id, $number, $total) {
$this->data['idColumn'] = $id;
$this->data['numberColumn'] = $number;
$this->data['total'] = $total;
}
public function delete() {
return $this->table->delete(array('id' => $this->data['id']));
}
public function insert() {
return $this->table->insert($this->data);
}
public function update() {
return $this->table->update($this->data, array('id' => $this->data['id']));
}
public function findById($id) {
$this->data = $this->table->fetchRow(array('id' => $id));
return $this;
}
public function getId() {
return $this->data['idColumn'];
}
public function getOrderNumber() {
return $this->data['numberColumn'];
}
public function getTotal() {
return $this->data['total'];
}
public function getExporter() {
return $this;
}
}
class ConcreteOrder implements Order {
private $id;
private $number;
private $total;
public function __construct(OrderImporter $importer) {
//initialize this object
$this->id = $importer->getId();
$this->number = $importer->getOrderNumber();
$this->total = $importer->getTotal();
}
public function export(\OrderExporter $exporter) {
$exporter->setData($this->id, $this->number, $this->total);
}
public function save(\OrderMapper $mapper) {
$this->export($mapper->getExporter());
if ($this->id === null) {
$this->id = $mapper->insert();
} else {
$mapper->update();
}
}
}

Yii one model for multiple tables

I have Yii application and two tables with same structure tbl and tbl_history:
Now want to create model so it will select table by parameter I send when calling model. For example:
MyModel::model('tbl')->find();
//and
MyModel::model('tbl_history')->find();
Find related article with solution in Yii forum. Made same changes and finally got this in MyModel:
private $tableName = 'tbl'; // <=default value
private static $_models=array();
private $_md;
public static function model($tableName = false, $className=__CLASS__)
{
if($tableName === null) $className=null; // this string will save internal CActiveRecord functionality
if(!$tableName)
return parent::model($className);
if(isset(self::$_models[$tableName.$className]))
return self::$_models[$tableName.$className];
else
{
$model=self::$_models[$tableName.$className]=new $className(null);
$model->tableName = $tableName;
$model->_md=new CActiveRecordMetaData($model);
$model->attachBehaviors($model->behaviors());
return $model;
}
}
Now when I make:
echo MyModel::model('tbl_history')->tableName(); // Output: tbl_history
It returns right value, but:
MyModel::model('tbl_history')->find();
still returns value for tbl.
Added:
public function __construct($id=null,$scenario=null){
var_dump($id);
echo '<br/>';
parent::__construct($scenario);
}
and got:
string(tbl_history)
string(tbl_history)
NULL
It means Yii makes call to model from other place but don't know from where and how to prevent it.
Also It makes 2 calls to model, is it too bad for performance?
It looks like the CActiveRecord::getMetaData() method needs to be overridden to achieve what you are looking for.
<?php
class TestActiveRecord extends CActiveRecord
{
private $tableName = 'tbl'; // <=default value
private static $_models=array();
private $_md;
public function __construct($scenario='insert', $tableName = null)
{
if($this->tableName === 'tbl' && $tableName !== null)
$this->tableName = $tableName;
parent::__construct($scenario);
}
public static function model($tableName = false, $className=__CLASS__)
{
if($tableName === null) $className=null; // this string will save internal CActiveRecord functionality
if(!$tableName)
return parent::model($className);
if(isset(self::$_models[$tableName.$className]))
return self::$_models[$tableName.$className];
else
{
$model=self::$_models[$tableName.$className]=new $className(null);
$model->tableName = $tableName;
$model->_md=new CActiveRecordMetaData($model);
$model->attachBehaviors($model->behaviors());
return $model;
}
}
public function tableName()
{
return $this->tableName;
}
/**
* Returns the meta-data for this AR
* #return CActiveRecordMetaData the meta for this AR class.
*/
public function getMetaData()
{
if($this->_md!==null)
return $this->_md;
else
return $this->_md=static::model($this->tableName())->_md;
}
public function refreshMetaData()
{
$finder=static::model($this->tableName());
$finder->_md=new CActiveRecordMetaData($finder);
if($this!==$finder)
$this->_md=$finder->_md;
}
}
Maybe it's easier to make MyModelHistory which extends MyModel and overrides only one method - tableName().
I recommend implementing single table inheritance. In order to do this you will need to combine your tables with a flag or type column that states whether or not this is a history record. I've pasted a few links at the bottom so you can see how this is implemented in Yii and listed some of the benefits below.
Benefits:
You won't need to duplicate code commonly used between the models
Changes to this table will only need to be executed once.
Changes to the parent model will only need to be made once.
Code becomes generally more maintainable and readable.
You seperate the code that belongs specifically to tbl and tbl_history
http://www.yiiframework.com/wiki/198/single-table-inheritance/
http://en.wikipedia.org/wiki/Single_Table_Inheritance
I created a solution for performing this exact action a couple of months ago. This is a completely dynamic solution, you just pass the table name like you are looking for to the model. That solution was originally designed to work with the same database structure across multiple databases, but it was trivial to adapt it to work in the same database. The documentation for that is here. I'd recommend reading over it as it has more details about CDynamicRecord
It's easy to adapt to work with multiple tables. You can download the adaptation as a gist from github.
Usage
1) Download the Gist and drop it into ext, save as CDynamicRecordSDB.php
2) Create Your model in Model, and setup up as follows:
Basically, you want to extend CDynamicRecord, and override your model() and tableName() so they are compliant with CDyanmicRecord.
<?php
Yii::import('ext.CDynamicRecordSDB');
class Test extends CDynamicRecordSDB
{
public static function model($dbConnectionString = 0, $className=__CLASS__)
{
return parent::model($dbConnectionString, $className);
}
public function tableName()
{
return $this->dbConnectionString;
}
[... Do everything else after this ...]
}
3) Setup your model as you normally would.
Usage
The usage is identical to CActiveRecord, and you can perform all actions. No surprises. Just a couple examples below.
$data = Test::model('tbl')->findAll();
$data2 = new Test('tbl');
$data2->findAll();
foreach ($data as $row)
print_r($row->attributes);
$data = Test::model('tbl_history')->findAll();
foreach ($data as $row)
print_r($row->attributes);
Limitations
The only limitation with doing this is you have to modify how relations work. IF you plan on accessing a related model (Bar), and you have no intention on calling Bar by itself. Then Bar should extend CActiveRecord, and in Foo you can define normal relations. Yii magically carries over the CDbConnectionString across the instances for you.
OTHERWISE, if you intend to access models in the same database, but also want to retain the ability to call them by themselves, then Bar should extend CDynamicModel, and Foo should have a getter defined as follows.
public function getBar()
{
return Bar::model($this->$dbConnectionString);
}
A small way but work for me for any number of table
public static $dynamic_table_name="main_table";
public static function setDynamicTable($param)
{
self::$dynamic_table_name=self::$dynamic_table_name.$param;
}
/**
* #return string the associated database table name
*/
public function tableName($param='')
{
self::setDynamicTable($param);
return self::$dynamic_table_name;
}
// to use it like
ModelName::model()->tableName('_one');
ModelName::model()->tableName('_two');
ModelName::model()->tableName('_three');

Iterator on MySQL-using objects

I was thinking about such problem... Let's say we have a class Person:
class Person {
private $iPersonId;
private $sName;
private $sLastName;
private $rConn;
public function __construct($rConn, $iPersonId) {
$this->rConn = $rConn;
$this->iPersonId = $iPersonId;
}
public function load() {
// load name and last name using the $rConn object and $iPersonId
}
}
And now we want to perform some actions on many people so we write a new class:
class People {
private $aPeople = array();
public function addPerson(Person $oPerson) {
// ...
}
public function loadPeople() {
// PROBLEM HERE //
}
}
And now there are two problems:
1. Person and People have the same interface for loading (function load()) but if I wanted to iterate through $aPeople in People to load their data then this would result in maaaaany queries like:
SELECT * FROM people WHERE id = 1
SELECT * FROM people WHERE id = 2
SELECT ......
.....
....
And if wanted to load 1000 then something would go boom :) .
How do I design this code for loading all the users in one query? (IN)
I have to keep using Dependency Injection in every Person object I add into People. It's against the DRY rule and just doesn't look well.
So dear users, what is the better way to design this code?
I'd suggest a static method within People to load a bulk of people.
This would also require you to rewrite the constructor, or add another method to initialize the other data.
class Person {
protected $_data
protected $rConn;
public function __construct($rConn, $iPersonId) {
$this->rConn = $rConn;
$this->_data = array();
$this->_data['id'] = $iPersonId;
}
public function load() {
// load name and last name using the $rConn object and $iPersonId
}
// under the assumption, that $rConn is a mysqli connection
// if not rewrite the specific section
// also there is no injection protection or error handling in here
// this is just a workflow example, not good code!
public static function loadPeople($ids) {
$res = $rConn->query("select * from people where id in (" . implode(',', $ids) . ")");
$people = array();
while ($row = $res->fetch_assoc()) {
$p = new People($rConn, $row['id']);
$p->setData($row);
$people[] = $p;
}
$res->free();
return $people;
}
public function setData($data) {
foreach ($data as $key => $value {
$this->_data[key] = $value;
}
}
}
If you build a service as in Symfony2 (http://symfony.com/doc/2.0/book/service_container.html), you can just add methods. It doesn't sound right to have a "load()" on a "person". What does it load, Itself? It's also a bad practice to give your Object or Entity access to the database, this causes unwanted dependencies.
Your Entity or Object should never have a function to load itself, bad practice. Let something else manage the Entities or Objects.
Don't make dependencies that cause confusion, keep an object to its own purpose. A PersonEntity should never know anything about a Database Connection or EntityManager
Build your code so that you can move it into another project without things breaking Composer. http://getcomposer.org/
example as how I would do it in symfony2
class PeopleService
{
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* #param int $id
* #return Person
*/
public function loadPerson($id)
{
// do something and return 1 person
return $this->em->find('MyBundleNamspace:Person', $id);
}
/**
* #return array of Person objects
*/
public function loadPeople()
{
// do something and return an array with persons
}
}

PHP Domain Model

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!

PHP return self as array of objects from within the class?

I'm using CodeIgniter to build a php web application, and I'm trying to use good OO practices - of which there appears to be many schools of thought. I specifically have a class biography_model to interact with a MySQL table. This data model has some class properties representing the columns in the table, but it also has some properties not in the table such as $image_url. The class constructor function accepts an optional record ID parameter which then fetches that record from the table and sets all object properties by calling the get_biography() method, including the $image_url property not in the table. This way I can instantiate a new biography_model object in the controller with all useful properties ready to go: $bio = new biography_model($id);
But, what is the best approach when we are returning a multi-row result set of records from the table? For each record I need to also set the $image_url. I could do this in the controller, by querying the list of records in the table and then passing each id into the new biography_model($id) object. But then I would have a situation where the controller is directly querying the database bypassing the model.
Instead, I choose to return an array of biography_model objects from within the biography_model.
Example:
class Biography_model extends Model
{
/**
* This model manages biography information in the 'biography_content' table.
* If a biography ID is passed in when instantiating a new object,
* then all class properties are set.
*/
protected $id;
protected $person_name;
protected $title;
protected $image_file_name;
protected $image_url;
protected $biography_text;
protected $active;
/**
* Constructor
*
* If an id is supplied when instantiating a new object, then
* all class variables are set for the record.
*/
public function __construct($person_id = NULL)
{
parent::Model();
if(isset($person_id))
{
$this->set_property('id',$person_id);
$this->get_biography();
}
}
/**
* Sets supplied property with supplied value.
*/
public function set_property($property, $value)
{
// Set image path if $value is the file name
if($property == 'image_file_name')
{
$this->set_property('image_url',$this->get_bio_img_url($value));
}
$this->$property = $value;
}
/**
* Gets requested property value.
*/
public function get_property($property)
{
return $this->$property;
}
/**
* Returns the biography thumbnail image URL
*/
public function get_bio_img_url($image_name)
{
return $this->config->item('parent_url').'assets/img/biography/'.$image_name;
}
/**
* Get one or more biography entries
*/
public function get_biography()
{
// If the ID is set then set model properties.
if($this->get_property('id'))
{
$this->db->where('id',$this->get_property('id'));
$query = $this->db->get('biography_content');
if($query->num_rows() == 1)
{
foreach($query->row() as $key => $value)
{
$this->set_property($key, $value);
}
}
}
// Otherwise return result set of all biographies
else
{
// Get the list of record ID's
$this->db->select('id');
$query = $this->db->get('biography_content');
if ($query->num_rows() > 0)
{
// New array to return result set
$biography_list = array();
// For each record, return a new biography_model object
foreach($query->result() as $value)
{
$biography_list[] = new biography_model($value->id);
}
}
return $biography_list;
}
}
}
// End of Biography_model Class
It works. But is it a reasonable approach? Are there other more accepted methods? I'm keenly aware that I am querying the database twice, but I was not sure of a better way to handle this. All suggestions are welcome!
Thanks, Wolf
Usually it's better for functions to have one job. Your get_biography() function has 2: get one biography and get all biographies. Consider splitting them up into 2 functions. Also there's no need for the multiple db access.
public function get_biography($id=null)
{
$this->db->where('id', $this->get_property($id))
$query = $this->db->get('biography_content');
foreach($query->row() as $key => $value)
{
$this->set_property($key, $value);
}
}
public function get_biographies()
{
$biography_list = array();
// don't limit this query to just id's - get everything
$query = $this->db->get('biography_content');
// For each record, return a new biography_model object
foreach($query->result() as $row)
{
$model = new biography_model();
// set the properties you already have straight onto the new model
// instead of querying again with just the id
foreach($row as $key => $value)
{
$model->set_property($key, $value);
}
$biography_list[] = $model;
}
return $biography_list;
}
Also you might want to take advantage of php's __get and __set magic methods:
public function __get($property)
{
if(!isset($this->$property))
return null;
return $this->$property;
}
public function __set($property, $value)
{
if(!property_exists($this, $property))
return;
if($property == 'image_file_name')
{
$this->image_url = $this->get_bio_img_url($value);
}
else
$this->$property = $value;
}
This will let you get properties on your model like this: $bio->title instead of $bio->get_property('title') while at the same time provide a place you can introduce new logic later.
Using an array to represent a set of records is a perfectly valid approach.
However the property image_url directly depends on the value of another property, so it doesn't make sense to store it as a separate field. Just calculate it on the fly, in your case you'd have to do that in the get_property method.
On the other hand should the model really be responsible for dealing with URLs? I don't think so. There should be a method outside the model that takes the Biography_model object and generates the URL of the image based on its image_file_name. If you already have some routing module responsible for mapping controllers to URLs, this code should probably land there.

Categories