编写原始 SQL 查询很酷。当您不用数千行代码来管理项目时,这很酷。
当项目增长时,最好自动化构建 sql 查询。
然而,ORM
这是非常复杂和沉重的结构,会减慢您的应用程序的速度,它也为您的应用程序带来了一些额外的功能:您可以添加自动安全检查、用户输入的清理和其他内容。
开始吧
我们需要将表中的行映射到 php 代码中的对象。因此,至少我们需要为代码中使用的每个表提供类:
class Post {
// ... another code
public function save() { // this methods save obejct to databases table
// ... code
}
}
这是引用表的类post
。相同的结构将在类User
或中Comment
。实际上我们应该在每个表类上都有 save 方法。这就是为什么我们为每个实体类创建抽象类:
abstract class Entity {
public function save() {
// this method construct sql using reflection
}
}
save
我们在上节课中已经写好了方法,所以我们只需复制并修改其内容即可:
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;
}
在上面的示例中,我们使用类名作为表名。为了使其更加灵活,我们应该引入
abstract class Entity {
protected $tableName;
// ...
并在save
方法中检查其是否设置为某个值。如果已设置 - 则使用该变量的值作为表名:
public function save() {
$class = new \ReflectionClass($this);
$tableName = '';
if ($this->tableName != '') {
$tableName = $this->tableName;
} else {
$tableName = strtolower($class->getShortName());
}
// ... rest of the code
我们还需要实际的数据库连接。我们将一个PDO
对象放入 protected$db
中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 ');
}
}
PDO
我在 的构造函数中初始化了该对象,Entity
只是为了向您展示这确实是PDO
一个对象。在实际系统中你不应该这样做。在这种情况下,请使用依赖项注入或服务容器。我们将在本课程的后续课程中讨论它们。
将字段映射到表中的列
我们将使用最简单的方法将表的列映射到对象 - 公共属性。有人说在类中使用公共属性破坏了incapsulation
面向对象编程的特性。它们在某些情况下是正确的,但使用公共属性可以降低 orm 的过度复杂性,并给出类的严格描述性视图。
让我们看一下使用ORM
我们推广的功能的类:
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;
}
看起来棒极了,不是吗?
现在是时候将数据库表中的字段映射到该对象中了。这可以通过将数组转换为实体对象的方法来完成morph
。最简单的实现如下:
/**
*
* @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;
}
正如我所说,这是将数组变形为对象的非常简单的实现,并且可以向其中添加许多内容:从数组中转换为正确的类型值、验证等。也许,我们会在下一课中接管类型的铸造。
在 i 的末尾调用实体的morph
方法。initialize
正如我在评论中提到的,它的魔力是:每个对象都可以实现方法initialize
,并且会在创建的每个对象上调用该方法。当您的实体上有一些额外的逻辑时,这将有助于设置所有内容。
创建和保存对象
此时我们已经可以在 php 中创建一个对象并通过面向对象的 api 将其保存到数据库,而无需运行任何行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
这里我们已经在propAUTO_INCREMENT
中拥有了表的 id (因为它是表的主索引):$id
Post
echo "new post id: ".$post->id;
搜索带有 orm 类的表
我们应该实现至少两种方法,这些方法能够在数据库中搜索一个元素并返回实体类的实例以及重新调整大量对象的版本。让我们定义方法 find ,它返回项目数组:
/**
*
* @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;
}
为了简化示例,我仅实现了简单的情况,当您可以传递字段名称和字段应等于的值时。例如,下一个代码将返回标题字段等于的项目Some title
:
$posts = Post::find([
'title' => 'Some title'
]);
我们可以通过向我们的方法添加额外的功能来克服这个限制find
:添加将原始 SQL 查询部分(where 子句)传递给 find 方法的可能性。因此,如果您需要执行一些非标准 WHERE,只需将条件find
作为字符串参数传递给方法:
$posts = Post::find('id IN (1,5,10)');
我们应该更改 find 方法的代码以使其正常工作,如上例所示:
/**
*
* @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;
}
继续前进
实际上,我忘记在方法中实现限制和顺序,find
这样你就可以将其作为家庭任务的一部分:)
给你一个提示:我们应该扩展参数$options
,上面的例子看起来像:
[
'condtions' => [
'title' => 'Some title'
]
]
现在我们可以添加limit
和/或order
和其他选项find
:
$posts = Post::find([
'conditions' => 'id IN (1,2,5)',
'order' => 'id DESC'
]);