Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I want to create a simple PHP ORM myself to use in smaller projects but I'm struggling with some parts of it. The testsetup contains two tables called "log" and "user". A log is adde by a user so the log table contains a userId.

Problems:

1.Lazy Loading

I want to load the user object when I retrieve a log. To achieve this, I created a proxymapper. This proxy accepts a mapper and a condition and passed the data back. Is this a good way to achieve this?

if($entry->userId > 0){
    $proxyMapper = new ProxyMapper($this->userMapper, "id = " . $data->userId);
    $entry->user = $proxyMapper->load();
}

2.Errorhandling When the log doensn't have a user, there will be an error, maybe normal in this situation. But let's say you retrieve an article and its comments, the chance there are no comments is realistic and normal. Should you throw an error? Or how do you handle this?

The project classes can be found here:

The database class

class Database
{
    private $connection;    
    private $host = "";
    private $username = "";
    private $password = "";
    private $dbname = "";

    public function openConnection(){       
        $this->connection = new mysqli($this->host,$this->username,$this->password,$this->dbname);
        if (mysqli_connect_errno()) {
            printf("Database connect error. Errorcode: %s <br />", mysqli_connect_error());
            exit;
        }
        //encoding UTF-8
        $this->connection->set_charset("utf8");
    }

    public function getConnection(){
        return $this->connection;
    }

    public function closeConnection(){
        $this->connection->close();
    }

    public function escape($param){
        return $this->connection->real_escape_string($param);
    }

    public function checkQuotes($value)
    {
        if (!is_numeric($value)) {
            $value = "'" . $this->escape($value) . "'";
        }
        return $value;
    }

    public function insert($table, array $data)
    {
        $fields = implode(',', array_keys($data));
        $values = implode(',', array_map(array($this, 'checkQuotes'), array_values($data)));
        $query = 'INSERT INTO ' . $table . ' (' . $fields . ') ' . ' VALUES (' . $values . ')';     
        $this->connection->query($query);
        return $this->connection->insert_id;
    }

    public function update($table, array $data, $where = ''){
        $set = array();
        foreach ($data as $field => $value) {
            $set[] = $field . '=' . $this->checkQuotes($value);
        }
        $set = implode(',', $set);
        $query = 'UPDATE ' . $table . ' SET ' . $set . (($where) ? ' WHERE ' . $where : '');        
        $this->connection->query($query);       
        return $this->connection->affected_rows;  
    }

    public function delete($table, $where = '')
    {
        $query = 'DELETE FROM ' . $table . (($where) ? ' WHERE ' . $where : '');        
        $this->connection->query($query);
        return $this->connection->affected_rows;
    }

    public function find($table, $where = ''){  
        $query = "select * from " . $table . (($where) ? ' WHERE ' . $where : '');      
        $results = array();
        if ($result = $this->connection->query($query)) {
            if($result->num_rows == 1){
                //single result
                return $result->fetch_object();
            }else{
                //more results in an array
                while ($obj = $result->fetch_object()) {
                    $results[] = $obj;
                }
            }
            $result->close();
        }else{
            throw new Exception("No data: " . $this->connection->error);
        }
        if(empty($results)){
            throw new Exception("No data found.");
        }
        return $results;
    }
}

The abstract mapper class

abstract class DataMapper{
    protected $database;    
    protected $datasource;
    protected $entityClass;
    protected $fields;

    public function __construct(Database $database) 
    { 
        $this->database = $database;        
    }

    function insert($item){
        $itemId = $this->database->insert($this->datasource,(array) $item);
        return $itemId;
    }
    function update($item){         
        $this->database->update($this->datasource,(array) $item,$this->findPk() . " = " . $item->$pk);
    }

    function find($condition){
        $logResult = $this->database->find($this->datasource,$condition);
        if (sizeof($logResult) == 1){
            return $this->toEntity($logResult);
        }
        $results = array();
        foreach($logResult as $log){            
            $results[] = $this->toEntity($log);
        }
        return $results;
    }

    function findById($id){ 
        $logResult = $this->database->find($this->datasource,$this->findPk() . " = " . $id);    
        return $this->toEntity($logResult);
    }

    function findPk(){      
        foreach($this->fields as $field => $value){         
            if(isset($value['primary'])){               
                return $field;
            }
        }
    }

    public function delete($id){
        return $this->database->delete($this->datasource,$this->findPk() . " = " . $id);
    }

    abstract protected function toEntity($data);    
}

The mapper for the log table

class LogMapper extends DataMapper{
//table
protected $datasource = "log";
//class
protected $entityClass = "Log"; 
protected $userMapper;
//fields
protected $fields = array(
    'id' => array('type' => 'int','primary' => true),
    'description' => array('type' => 'string'),
    'urgency' => array('type' => 'int'),
    'userId' => array('type' => 'int'));

public function __construct($database) 
{       
    parent::__construct($database);
    $this->userMapper = new UserMapper($database);
}

protected function toEntity($data)
{
    $entry = new $this->entityClass();      
    foreach($this->fields as $field => $value){     
        $entry->$field = $data->$field;         
    }       
    $entry->setUserProxy(new EntityProxy($this->userMapper, $data->userId));            
    return $entry;
}

}

The mapper for the user table

class UserMapper extends DataMapper{
//table
protected $datasource = "user";
//class
protected $entityClass = "User";
//fields
protected $fields = array(
    'id' => array('type' => 'int','primary' => true),
    'username' => array('type' => 'string'));

protected function toEntity($data)
    {       
     $entry = new $this->entityClass();
     foreach($this->fields as $field => $value){
    $entry->$field = $data->$field;         
     }          
     return $entry;
    }

}

Proxymapper class

class ProxyMapper{
    protected $mapper;
    protected $condition;

    public function __construct($mapper,$condition) 
    { 
        $this->mapper = $mapper;
        $this->condition = $condition;
    }

    public function load(){
        $user = $this->mapper->find($this->condition);
        return $user;
    }   
}

Log class

class Log{
    public $id;
    public $description;
    public $urgency;
    public $userId;
}

User class

class User{
    public $id;
    public $username;
}

Edit 1 based on the answer of @mnhg

1.Lazy Loading Ok in my case when using a public property, there is nothing lazy about it. But in most ORM's I see they use $log->user to display the user object and not $log->getUser(). That is what I want to achieve. I understand your point, when creating a getter, I can call the proxy in the getter method. But how is it done without this so by calling $log->user

2.error handling: So in my Database/find I shouldn't throw an error but just return an empty result? Where should I throw an error then?

Database Ok, I get that. I just want to make sure the Database class is the only class that really implements the database so it can be easilly changed by for example a PDO Database class.

Getters and setters Offcourse, I get that, and that isn't the case now? the user property of the log object is directly loaded in the toEntity function. So you cant call load on the user or where do you mean this? I don't understand exactly what you mean.

Mapping Absolutely true, I was planning to work on that However, I could use my $fields array to get the names in my abstract class toEntity, but how would ik handle $user there because it isn't a column but a collection. Can I just add it to the $fields array?

UPDATE: REMAINING ISSUES:

Lazy Loading How can I implement my lazy loading? I'm still not sure because in my "toEntity" method the user is now directly loaded via the proxy load method. But how can I implement that the user is loaded when I call $log->user ?

share|improve this question
add comment

migrated from stackoverflow.com Apr 17 '13 at 7:30

This question came from our site for professional and enthusiast programmers.

1 Answer

up vote 0 down vote accepted

1. Lazy Loading

A proxy is a common pattern in this scenario, so "Yes this is a good idea". Put keep in mind that when you load a list of logs and call getUser() you don't want to send a query to your database each. So you need some caching and prefetching.

I'm not sure if you don't understand the proxy or I don't understand you ;). If you use '$entry->user = $proxyMapper->load();' then there is nothing lazy about it. You want to fetch the proxy value when the log->getUser() is called the first time and only if it get calls. (Thats why a public property will not work that well here).

Now assume that you want to fetch the last 10 logs and display the message and the username. You will create a foreach over your result and call getUser on every row, which results in a proxy-miss and a request to the database.

Edit - As a draft:

<?php
class Proxy
{
    private $value=null
    public function get()
    {
        if ($this->value==null) $this->load()
        return $this->value;
    }

    ....
}

class Log
{
    /** var Proxy *//
    private $user=null;
    ...
    public function getUser()
    {
         return $user->get();
    }

}

2. Error handling

Why not returning null if there is no user and array() if there are no comments?

As long as your field is not defined as "not null" I would handle the error somewhere else.

Database

Check PDO and don't reinvent the wheel.

*mysql(i)_... is no longer state of the art if you try to develop in an OO-approach, although it will work anyway. This is just a hint, nothing mandatory.*

Public properties

You should think about generating abstract entities with getters and setters. As soon as you use your entities for more than plain value objects the public properties might get an (security) issue.

I want to call $log->getUser() and definitly not $log->user->load();, so you have to hide the proxy at this place

Mapping

Maybe you can generalize your toEntity method if you define a convention for mapping column names to properties? I guess creating this mapping every time manually might be annoying.

share|improve this answer
 
Ok thx, I edited the question below with some remarks and questions. –  randomizer Apr 17 '13 at 12:40
 
Updated in relation to your questions. –  mnhg Apr 17 '13 at 13:07
 
ok thx, updated my questions :) I also added the ProxyMapper class which I forgot to include in my post. –  randomizer Apr 17 '13 at 13:36
 
Still stuck on the Lazy loading part. Is it ok to pass a proxy object to my entity and use magic __get to query it and return a value? –  randomizer Apr 22 '13 at 8:17
 
Don't use this magic stuff. I updated my post with a draft of the Proxy an lazy loading. –  mnhg Apr 22 '13 at 8:50
show 1 more comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.