本系列是读php data persistence with doctrine2 orm的笔记,本文是第一篇:自己造轮子。
最开始描述下需要构建的系统
一个User可以发表Post,一个Post只有一个作者,User和Post之间彼此引用
一个User可以有多个Roles,User有Roles的引用,但是不能通过Role找到Users
一个User有一个UserInfo,UserInfo中包含了用户的注册信息等,User和UserInfo彼此引用
一个User有一个ContactData,包含email、电话等信息,User单向引用ContactData
一个User可能会有一个life partner,彼此之间互相引用
一个User会有多个friends,关系是单向的
一个Post会有多个标签Tag,Post到Tag是双向关系
一个Post有一个Category,Post到Category时单向关系
一个Category会有subcategories,并且会有parent Category
一个User会有多个Categories,User到Categories是单向关系
在起初这个阶段我们不会直接就是用Doctrine,而是会自己来打造一个ORM,让我们更清楚的了解一个好的ORM需要怎么做。
读数据
先来看Model:User,部分代码如下:
class User {
const GENDER_MALE = 0;
const GENDER_FEMALE = 1;
const GENDER_MALE_DISPLAY_VALUE = "Mr.";
const GENDER_FEMALE_DISPLAY_VALUE = "Mrs.";
/**
* @return string
*/
public function assembleDisplayName()
{
$displayName = '';
if ( $this->gender == self::GENDER_MALE ) {
$displayName .= self::GENDER_MALE_DISPLAY_VALUE;
} elseif ( $this->gender == self::GENDER_FEMALE ) {
$displayName .= self::GENDER_FEMALE_DISPLAY_VALUE;
}
if ( $this->namePrefix ) {
$displayName .= ' ' . $this->namePrefix;
}
$displayName .= ' ' . $this->firstName . ' ' . $this->lastName;
return $displayName;
}
}
class UserTest extends PHPUnit_Framework_TestCase {
public function testAssembleDisplayName()
{
$user = new User();
$user->setFirstName( 'Max' );
$user->setLastName( 'Mustermann' );
$user->setGender( 0 );
$user->setNamePrefix( 'Prof. Dr' );
$this->assertEquals("Mr. Prof. Dr Max Mustermann",$user->assembleDisplayName());
}
}
上面测试了User的一个功能,一般来说User都是从数据库中获取的,我们来写一段代码,测试下从数据库中读取的方式
public function testLoadFromDataBase()
{
$db = new \PDO( 'mysql:host=127.0.0.1;dbname=app;port=33060', 'root', 'root' );
$userData = $db->query( 'SELECT * FROM users WHERE id = 1' )->fetch();
$user = new Entity\User();
$user->setId( $userData['id'] );
$user->setFirstName( $userData['first_name'] );
$user->setLastName( $userData['last_name'] );
$user->setGender( $userData['gender'] );
$user->setNamePrefix( $userData['name_prefix'] );
$this->assertEquals( "Mr. Prof. Dr. Max Mustermann", $user->assembleDisplayName() );
}
上面代码就是一个简易的ORM,从数据库中加载数据,然后将其转换为Object,让我们更进一步,将这些“data mapping”功能单独抽取出来,叫做Mapper:
<?php
namespace Mapper;
class User {
private $mapping = [
'id' => 'id',
'firstName' => 'first_name',
'lastName' => 'last_name',
'gender' => 'gender',
'namePrefix' => 'name_prefix',
];
public function populate( $data, $user )
{
$mappingsFlipped = array_flip( $this->mapping );
foreach ( $data as $key => $value ) {
if ( isset( $mappingsFlipped[ $key ] ) ) {
call_user_func_array(
[ $user, 'set' . ucfirst( $mappingsFlipped[ $key ] ) ],
[ $value ]
);
}
}
return $user;
}
}
此处我们再来看测试代码:
public function testPopulate()
{
$db = new \PDO( 'mysql:host=127.0.0.1;dbname=app;port=33060', 'root', 'root' );
$userData = $db->query( 'SELECT * FROM users WHERE id = 1' )->fetch();
$user = new Entity\User();
$userMapper = new Mapper\User();
$user = $userMapper->populate( $userData, $user );
$this->assertEquals( "Mr. Prof. Dr. Max Mustermann", $user->assembleDisplayName() );
}
上面代码已经将数据映射的功能进行了封装,下一步,我们将sql语句抽离出来,封装到Repository中:
<?php namespace Repository;
use Mapper\User as UserMapper;
use Entity\User as UserEntity;
class User {
/** @var \EntityManager */
private $em;
private $mapper;
public function __construct( $em )
{
$this->mapper = new UserMapper;
$this->em = $em;
}
public function findOneById( $id )
{
$userData = $this->em
->query( 'SELECT * FROM users WHERE id = ' . $id )
->fetch();
return $this->mapper->populate( $userData, new UserEntity() );
}
}
此处有个类叫EntityManager,其职责是作为数据库操作的Entry Point,负责所有的具体的数据库操作:
<?php
use Repository\User as UserRepository;
use Repository\Post as PostRepository;
use Mapper\User as UserMapper;
class EntityManager {
private $host;
private $db;
private $user;
private $pwd;
private $port;
private $connection;
private $userRepository;
private $postRepository;
private $identityMap;
public function __construct( $host, $db, $port, $user, $pwd )
{
$this->host = $host;
$this->user = $user;
$this->pwd = $pwd;
$this->connection = new \PDO( "mysql:host=$host;port=$port;dbname=$db", $user, $pwd );
$this->userRepository = null;
$this->postRepository = null;
$this->db = $db;
$this->identityMap = [ 'users' => [] ];
$this->port = $port;
}
public function query( $stmt )
{
return $this->connection->query( $stmt );
}
public function getUserRepository()
{
if ( !is_null( $this->userRepository ) ) {
return $this->userRepository;
} else {
$this->userRepository = new UserRepository( $this );
return $this->userRepository;
}
}
}
此时我们的测试代码变为了:
<?php
class UserRepositoryTest extends \PHPUnit_Framework_TestCase {
public function testPopulate()
{
$em = new \EntityManager('127.0.0.1','app',33060,'root','root');
$repository = new Repository\User($em);
$user = $repository->findOneById(1);
$this->assertEquals( "Mr. Prof. Dr. Max Mustermann", $user->assembleDisplayName() );
}
}
到目前为止我们做的事情就是将数据从数据库中读取出来,然后根据数据构造出对象,下面我们再进一步,看怎么对对象进行持久化。
保存数据
保存操作有两种:insert、update,先来看准备动作,将数据从对象Entity中取出来:
// class Mapper\User
public function extract( $user )
{
$data = [];
foreach ( $this->mapping as $keyObject => $keyColumn ) {
if ( $keyColumn != $this->getIdColumn() ) {
$data[ $keyColumn ] = call_user_func(
[ $user, 'get' . ucfirst( $keyObject ) ]
);
}
}
return $data;
}
在EntityManager中新增saveUser方法:
public function saveUser( $user )
{
$userMapper = new UserMapper();
$data = $userMapper->extract( $user );
$userId = call_user_func(
[ $user, 'get' . ucfirst( $userMapper->getIdColumn() ) ]
);
if ( array_key_exists( $userId, $this->identityMap['users'] ) ) {
$setString = '';
foreach ( $data as $key => $value ) {
$setString .= $key . "='$value',";
}
return $this->query(
"UPDATE users SET " . substr( $setString, 0, -1 ) .
" WHERE " . $userMapper->getIdColumn() . "=" . $userId
);
} else {
$columnsString = implode( ", ", array_keys( $data ) );
$valuesString = implode( "', '", $data );
return $this->query(
"INSERT INTO users ($columnsString) VALUES('$valuesString')"
);
}
}
此时新增一个User的方法如下:
<?php
class EntityManagerTest extends PHPUnit_Framework_TestCase {
public function testSaveUser()
{
$em = new \EntityManager( '127.0.0.1', 'app', 33060, 'root', 'root' );
$newUser = new Entity\User();
$newUser->setFirstName( 'Ute' );
$newUser->setLastName( 'Musermann' );
$newUser->setGender( 1 );
$em->saveUser( $newUser );
$this->assertEquals("Mrs. Ute Musermann",$newUser->assembleDisplayName());
}
}
此处在saveUser中使用了identity map模式,通过记录已经load的entity,减少从数据库中重新加载数据。
关系
用户有多个Posts,通过User的getPosts方法可以获取posts,因此有下面的代码:
// class Entity\User
public function getPosts()
{
if ( is_null( $this->posts ) ) {
$this->posts = $this->postRepository->findByUser( $this );
}
return $this->posts;
}
此时为了能够获取posts,需要初始化postRepository,最好的初始化地方就是Repository\User中的findOneById,看代码:
public function findOneById( $id )
{
$userData = $this->em->query('SELECT * FROM users WHERE id = ' . $id)->fetch();
$newUser = new UserEntity();
$newUser->setPostRepository($this->em->getPostRepository());
return $this->em->registerUserEntity(
$id,
$this->mapper->populate($userData, $newUser)
);
}
最后要配套的Post的Entity,Mapper,Repository,然后是findByUser方法的实现
// class Repository\Post
public function findByUser( UserEntity $user )
{
$postsData = $this->em
->query( 'SELECT * FROM posts WHERE user_id = ' . $user->getId() )->fetchAll();
$posts = [];
foreach ( $postsData as $postData ) {
$newPost = new PostEntity();
$posts[] = $this->mapper->populate( $postData, $newPost );
}
return $posts;
}
此时让我们回过头来看下项目结构:
src
├── Entity
│ ├── Post.php
│ └── User.php
├── EntityManager.php
├── Mapper
│ ├── Post.php
│ └── User.php
└── Repository
├── Post.php
└── User.php
此时我们已经具备了基本的orm框架了,再往下就会越来越复杂了,下一篇让我们来看下doctrine是怎么来做着一切的。
本文完整的代码可以查看https://github.com/zhuanxuhit/doctrine-learn