本系列是读 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,部分代码如下:
classUser{ const GENDER_MALE = 0; const GENDER_FEMALE = 1; const GENDER_MALE_DISPLAY_VALUE = "Mr."; const GENDER_FEMALE_DISPLAY_VALUE = "Mrs."; /** * @return string */ public functionassembleDisplayName() { $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; } } classUserTestextendsPHPUnit_Frame work_TestCase{ public functiontestAssembleDisplayName() { $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 functiontestLoadFromDataBase() { $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; classUser{ private $mapping = [ 'id' => 'id', 'firstName' => 'first_name', 'lastName' => 'last_name', 'gender' => 'gender', 'namePrefix' => 'name_prefix', ]; public functionpopulate( $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 functiontestPopulate() { $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; classUser{ /**@var\EntityManager */ private $em; private $mapper; public function__construct( $em ) { $this->mapper = new UserMapper; $this->em = $em; } public functionfindOneById( $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; classEntityManager{ 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 functionquery( $stmt ) { return $this->connection->query( $stmt ); } public functiongetUserRepository() { if ( !is_null( $this->userRepository ) ) { return $this->userRepository; } else { $this->userRepository = new UserRepository( $this ); return $this->userRepository; } } }
此时我们的测试代码变为了:
<?php classUserRepositoryTestextends\PHPUnit_Frame work_TestCase{ public functiontestPopulate() { $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 functionextract( $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 functionsaveUser( $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_Frame work_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