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 ?