How to create an ORM framework in pure php. ORM Creation Tutorial
lang_of_article_differ
want_proper_trans
Writing raw sql queries is cool. Its cool when you don't manage project with thouthands of lines of code.
When the project grow its better to automatize building sql queries.
Nevertheless ORM
is very complex and heavy structure that slows down your application, it brings some extra ability to your application too: you can add auto security checks, sanitization of user input and other stuff.
Let's start
We need to get rows in table mapped to objects in php code. So, at least we need class for each table we going to use in our code:
class Post {
// ... another code
public function save() { // this methods save obejct to databases table
// ... code
}
}
This is the class which refers to post
table. The same structure would be in class User
or Comment
. Actually we should have save method on every table-class. Thats why we create abstract class for every Entity class:
abstract class Entity {
public function save() {
// this method construct sql using reflection
}
}
We already have written method save
in previous lesson of the course, so we can just copy and modify its content:
public function save() {
$class = new \ReflectionClass($this);
$tableName = strtolower($class->getShortName());
$propsToImplode = [];
foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { // consider only public properties of the providen
$propertyName = $property->getName();
$propsToImplode[] = '`'.$propertyName.'` = "'.$this->{$propertyName}.'"';
}
$setClause = implode(',',$propsToImplode); // glue all key value pairs together
$sqlQuery = '';
if ($this->id > 0) {
$sqlQuery = 'UPDATE `'.$tableName.'` SET '.$setClause.' WHERE id = '.$this->id;
} else {
$sqlQuery = 'INSERT INTO `'.$tableName.'` SET '.$setClause.', id = '.$this->id;
}
$result = self::$db->exec($sqlQuery);
if (self::$db->errorCode()) {
throw new \Exception(self::$db->errorInfo()[2]);
}
return $result;
}
In example above we use classname as table name. To make it more flexible we should introduce
abstract class Entity {
protected $tableName;
// ...
and in save
method check if its set to some value. If its set - then use value of this variable as the table name:
public function save() {
$class = new \ReflectionClass($this);
$tableName = '';
if ($this->tableName != '') {
$tableName = $this->tableName;
} else {
$tableName = strtolower($class->getShortName());
}
// ... rest of the code
We also need the actually database connection. We would put a PDO
object into protected $db
into Entity
:
/**
*
* @var PDO
*/
protected $db;
public function __construct() {
try {
$this->db = new \PDO('mysql:host=localhost;dbname=blog','root', '');
} catch (\Exception $e) {
throw new \Exception('Error creating a database connection ');
}
}
I've initialized the PDO
object in the constructor of Entity
only to show you that this is really PDO
object. In real systems you shouldn't do that. Use a dependency injection or service container in such case. We would talk about them in the further lessons of this course.
Mapping fields to columns in table
We would use the easiest way to map columns of the table to object - public properties. Some people say that using public properties in classes breaks incapsulation
feature of Object Orientated Proprgamming. They're right in some case, but using public properties lowers overcomplexity of orm and gives strict descriptive view of classes.
Let's have a look at class which uses ORM
features we promoted:
class Post extends Entity {
protected $tableName = 'posts'; // usually tables are named in plural when the object should be named singular
public $id;
public $title;
public $body;
public $author_id;
public $date;
public $views;
public $finished;
}
Looks awesome, no ?
Now its time to map the fields from the database's table into this object. This would be done by the method morph
which converts array into Entity object. The simplest implementation is the following:
/**
*
* @return Entity
*/
public static function morph(array $object) {
$class = new \ReflectionClass(get_called_class()); // this is static method that's why i use get_called_class
$entity = $class->newInstance();
foreach($class->getProperties(\ReflectionProperty::PUBLIC) as $prop) {
if (isset($object[$prop->getName()])) {
$prop->setValue($entity,$object[$prop->getName()]);
}
}
$entity->initialize(); // soft magic
return $entity;
}
This is very simple implementation of the morphing array to object as i say and many things can be added to it: casting to proper types values from the array, validation and other. Probably, we would taking over the casting of types in next lessons.
At the end of the morph
i invoke a method initialize
of the
entity. Its kind of magic as i mentioned in comment: every object could implement method initialize
and it would be invoked on each object created. This would help to set up all things up when you have some extra logic on your entity.
Creating and saving objects
At this moment we can already create an object in php and save it to the database via object orientated api without running any line of SQL
:
$post = new Post(); // this creates post object
$post->title = 'How to cook pizza';
$post->date = time();
$post->finished = false;
$post->save(); // here we construct sql and execute it on the database
here we already have an AUTO_INCREMENT
id of the table in $id
prop of the Post
(because its a primary index of the table):
echo "new post id: ".$post->id;
Searching over tables with orm classes
we should implement at least two methods which would give ability to search for one element in database and return instance of the entity class and the version which retun bulk of the objects. Let's define method find which returns array of items:
/**
*
* @return Entity[]
*/
public static function find ($options = []) {
$result = [];
$query = '';
$whereClause = '';
$whereConditions = [];
if (!empty($options)) {
foreach ($options as $key => $value) {
$whereConditions[] = '`'.$key.'` = "'.$value.'"';
}
$whereClause = " WHERE ".implode(' AND ',$whereConditions);
}
$raw = self::$db->query($query);
if (self::$db->errorCode()) {
throw new \Exception(self::$db->errorInfo()[2]);
}
foreach ($raw as $rawRow) {
$result[] = self::morph($rawRow);
}
return $result;
}
To simplify the example i'm only implemented simple situation when you can pass the fields names and values fields should equal to. For example next code will return items which have title field equal to Some title
:
$posts = Post::find([
'title' => 'Some title'
]);
We can overcome this limit by adding extra feature to our find
method: add possibility to pass raw SQL query part (where clause) to find method. So, if you need to execute some non standart WHERE just pass the conditions to the find
method as a string parameter:
$posts = Post::find('id IN (1,5,10)');
We should alter code of the find method to get it work as in example above:
/**
*
* @return Entity[]
*/
public static function find ($options = []) {
$result = [];
$query = '';
if (is_array($options)) {
// ... old code
} elseif (is_stirng($options)) {
$query = 'WHERE '.$options;
} else {
throw new \Exception('Wrong parameter type of options');
}
$raw = $this->database->execute($query);
foreach ($raw as $rawRow) {
$result[] = self::morph($rawRow);
}
return $result;
}
Going ahead
Actually, i forget to implement limit and order in find
method so you can make it as a part of the hometask :)
Give you a hint: we should extends the $options
parameter, the example from above would look like:
[
'condtions' => [
'title' => 'Some title'
]
]
Now we can add the limit
and/or order
and other options to find
:
$posts = Post::find([
'conditions' => 'id IN (1,2,5)',
'order' => 'id DESC'
]);