本系列是讀php data persistence with doctrine2 orm的筆記,本文是第一篇:自己造輪子。
最開始描述下需要構建的系統(tǒng)
一個User可以發(fā)表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需要怎么做。
讀數(shù)據(jù)
先來看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都是從數(shù)據(jù)庫中獲取的,我們來寫一段代碼,測試下從數(shù)據(jù)庫中讀取的方式
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,從數(shù)據(jù)庫中加載數(shù)據(jù),然后將其轉(zhuǎn)換為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() );
}
上面代碼已經(jīng)將數(shù)據(jù)映射的功能進行了封裝,下一步,我們將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,其職責是作為數(shù)據(jù)庫操作的Entry Point,負責所有的具體的數(shù)據(jù)庫操作:
<?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;
}
}
}
此時我們的測試代碼變?yōu)榱耍?/p>
<?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() );
}
}
到目前為止我們做的事情就是將數(shù)據(jù)從數(shù)據(jù)庫中讀取出來,然后根據(jù)數(shù)據(jù)構造出對象,下面我們再進一步,看怎么對對象進行持久化。
保存數(shù)據(jù)
保存操作有兩種:insert、update,先來看準備動作,將數(shù)據(jù)從對象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模式,通過記錄已經(jīng)load的entity,減少從數(shù)據(jù)庫中重新加載數(shù)據(jù)。
關系
用戶有多個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方法的實現(xiàn)
// 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
此時我們已經(jīng)具備了基本的orm框架了,再往下就會越來越復雜了,下一篇讓我們來看下doctrine是怎么來做著一切的。
本文完整的代碼可以查看https://github.com/zhuanxuhit/doctrine-learn