├── .gitignore ├── README.markdown ├── autoload.php ├── database ├── tbl_article.sql └── tbl_user.sql ├── phpunit.xml.dist ├── src ├── Repository.php ├── framework │ ├── IdentityMap.php │ ├── MapperException.php │ └── RecursiveClassLoder.php ├── model │ ├── Article.php │ └── User.php └── persistence │ ├── AbstractMapper.php │ ├── ArticleMapper.php │ └── UserMapper.php ├── test-bootstrap.php ├── tests ├── RepositoryTest.php ├── model │ ├── ArticleTest.php │ └── UserTest.php └── persistence │ ├── ArticleMapperTest.php │ ├── UserMapperTest.php │ └── fixture │ ├── article-seed.xml │ └── user-seed.xml └── uml-php-identity-map.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Use wildcards as well 2 | *~ 3 | *.dist 4 | phpunit.xml.dist -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Building an Identity Map in PHP 2 | =============================== 3 | 4 | Sample application used for training. 5 | 6 | This example code is no production code and should be used for training 7 | purposes only. 8 | 9 | This example code requires: 10 | --------------------------- 11 | * PDO a lightweight, consistent interface for accessing databases in PHP. 12 | * PHPUnit a unit testing framework for PHP projects. 13 | 14 | This example code implements: 15 | ----------------------- 16 | * Data-Mapper Pattern 17 | * Identity-Map Pattern 18 | 19 | Why identity mapping? 20 | --------------------- 21 | By using Data-Mapper pattern without an identity map, you can easily run 22 | into problems because you may have more than one object that references 23 | the same domain entity. 24 | 25 | Data-Mapper without identity map 26 | ---------------------------------- 27 | 28 | $userMapper = new UserMapper($pdo); 29 | 30 | $user1 = $userMapper->find(1); // creates new object 31 | $user2 = $userMapper->find(1); // creates new object 32 | 33 | echo $user1->getNickname(); // joe123 34 | echo $user2->getNickname(); // joe123 35 | 36 | $user1->setNickname('bob78'); 37 | 38 | echo $user1->getNickname(); // bob78 39 | echo $user2->getNickname(); // joe123 -> ?!? 40 | 41 | Data-Mapper with identity map 42 | ---------------------------------- 43 | The identity map solves this problem by acting as a registry for all 44 | loaded domain instances. 45 | 46 | $userMapper = new UserMapper($pdo); 47 | 48 | $user1 = $userMapper->find(1); // creates new object 49 | $user2 = $userMapper->find(1); // returns same object 50 | 51 | echo $user1->getNickname(); // joe123 52 | echo $user2->getNickname(); // joe123 53 | 54 | $user1->setNickname('bob78'); 55 | 56 | echo $user1->getNickname(); // bob78 57 | echo $user2->getNickname(); // bob78 -> yes, much better 58 | 59 | By using an identity map you can be confident that your domain entity is 60 | shared throughout your application for the duration of the request. 61 | 62 | Note that using an identity map is not the same as adding a cache layer 63 | to your mappers. Although caching is useful and encouraged, it can still 64 | produce duplicate objects for the same domain entity. 65 | 66 | Load the Data-Mappers with the Repository class 67 | ----------------------------------------------- 68 | 69 | 70 | $repository = new Repository($this->db); 71 | $userMapper = $repository->load('User'); 72 | $insertId = $userMapper->insert(new User('billy', 'gatter')); 73 | 74 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | tests 26 | 27 | 28 | 29 | 30 | 31 | 32 | src 33 | 34 | test-bootstrap.php 35 | autoload.php 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Repository.php: -------------------------------------------------------------------------------- 1 | identityMap->hasId($entity)) { 13 | return $this->identityMap->getObject($entity); 14 | } 15 | 16 | $this->identityMap->set($entity, new $entity($this->db)); 17 | 18 | return $this->identityMap->getObject($entity); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/framework/IdentityMap.php: -------------------------------------------------------------------------------- 1 | objectToId = new SplObjectStorage(); 17 | $this->idToObject = new ArrayObject(); 18 | } 19 | 20 | /** 21 | * @param integer $id 22 | * @param mixed $object 23 | */ 24 | public function set($id, $object) 25 | { 26 | $this->idToObject[$id] = $object; 27 | $this->objectToId[$object] = $id; 28 | } 29 | 30 | /** 31 | * @param mixed $object 32 | * @throws OutOfBoundsException 33 | * @return integer 34 | */ 35 | public function getId($object) 36 | { 37 | if (false === $this->hasObject($object)) { 38 | throw new OutOfBoundsException(); 39 | } 40 | 41 | return $this->objectToId[$object]; 42 | } 43 | 44 | /** 45 | * @param integer $id 46 | * @return boolean 47 | */ 48 | public function hasId($id) 49 | { 50 | return isset($this->idToObject[$id]); 51 | } 52 | 53 | /** 54 | * @param mixed $object 55 | * @return boolean 56 | */ 57 | public function hasObject($object) 58 | { 59 | return isset($this->objectToId[$object]); 60 | } 61 | 62 | /** 63 | * @param integer $id 64 | * @throws OutOfBoundsException 65 | * @return object 66 | */ 67 | public function getObject($id) 68 | { 69 | if (false === $this->hasId($id)) { 70 | throw new OutOfBoundsException(); 71 | } 72 | 73 | return $this->idToObject[$id]; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/framework/MapperException.php: -------------------------------------------------------------------------------- 1 | directoryToBeLoaded = $directoryToBeLoaded; 21 | $this->projectRoot = $projectRoot; 22 | } 23 | 24 | /** 25 | * @param string $className Name of the class, will be invoked by de SPL autoloader. 26 | */ 27 | public function load($className) 28 | { 29 | static $classes; 30 | 31 | if ($classes === null) { 32 | 33 | $regexIterator = new RegexIterator( 34 | new RecursiveIteratorIterator( 35 | new RecursiveDirectoryIterator( 36 | $this->directoryToBeLoaded 37 | ) 38 | ), 39 | '/^.+\.php$/i', 40 | RecursiveRegexIterator::GET_MATCH 41 | ); 42 | 43 | foreach (iterator_to_array($regexIterator, false) as $file) { 44 | 45 | $path = current($file); 46 | $name = explode('/', $path); 47 | $name = str_replace('.php', '', end($name)); 48 | 49 | $classes[$name] = '/'.$path; 50 | } 51 | } 52 | 53 | if (isset($classes[$className])) { 54 | require $this->projectRoot . $classes[$className]; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/model/Article.php: -------------------------------------------------------------------------------- 1 | title = $title; 27 | $this->content = $content; 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function getTitle() 34 | { 35 | return $this->title; 36 | } 37 | 38 | /** 39 | * @param string $title 40 | * @return Article 41 | */ 42 | public function setTitle($title) 43 | { 44 | $this->title = $title; 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function getContent() 53 | { 54 | return $this->content; 55 | } 56 | 57 | /** 58 | * @param string $content 59 | * @return Article 60 | */ 61 | public function setContent($content) 62 | { 63 | $this->content = $content; 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * @return User 70 | */ 71 | public function getUser() 72 | { 73 | return $this->user; 74 | } 75 | 76 | /** 77 | * @param User $user 78 | * @return Article 79 | */ 80 | public function setUser(User $user) 81 | { 82 | $this->user = $user; 83 | 84 | return $this; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/model/User.php: -------------------------------------------------------------------------------- 1 | nickname = $nickname; 32 | $this->password = $password; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getNickname() 39 | { 40 | return $this->nickname; 41 | } 42 | 43 | /** 44 | * @param string $nickname 45 | * @return User 46 | */ 47 | public function setNickname($nickname) 48 | { 49 | $this->nickname = $nickname; 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getPassword() 58 | { 59 | return $this->password; 60 | } 61 | 62 | /** 63 | * @param string $password 64 | * @return User 65 | */ 66 | public function setPassword($password) 67 | { 68 | $this->password = $password; 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * @return integer 75 | */ 76 | public function getId() 77 | { 78 | return $this->id; 79 | } 80 | 81 | /** 82 | * @param string $title 83 | * @param string $content 84 | * @return User 85 | */ 86 | public function addArticle($title, $content) 87 | { 88 | $article = new Article($title, $content); 89 | $this->articles[] = $article->setUser($this); 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * @param array $article List of Article objects. 96 | * @return User 97 | */ 98 | public function setArticles(array $article) 99 | { 100 | $this->articles = $article; 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * @return array A list of Article instances. 107 | */ 108 | public function getArticles() 109 | { 110 | return $this->articles; 111 | } 112 | 113 | /** 114 | * @return boolean 115 | */ 116 | public function hasArticles() 117 | { 118 | return (true === is_array($this->articles) && false === empty($this->articles)); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/persistence/AbstractMapper.php: -------------------------------------------------------------------------------- 1 | db = $db; 26 | $this->identityMap = new IdentityMap(); 27 | } 28 | 29 | public function __destruct() 30 | { 31 | unset($this->identityMap, $this->db); 32 | } 33 | } -------------------------------------------------------------------------------- /src/persistence/ArticleMapper.php: -------------------------------------------------------------------------------- 1 | identityMap->hasId($id)) { 12 | return $this->identityMap->getObject($id); 13 | } 14 | 15 | $sth = $this->db->prepare( 16 | 'SELECT * FROM tbl_article WHERE id = :id' 17 | ); 18 | 19 | $sth->bindValue(':id', $id, PDO::PARAM_INT); 20 | $sth->setFetchMode(PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE, 'Article', array('title', 'content')); 21 | $sth->execute(); 22 | 23 | if ($sth->rowCount() == 0) { 24 | throw new OutOfBoundsException( 25 | sprintf('No article with id #%d exists.', $id) 26 | ); 27 | } 28 | 29 | // let pdo fetch the Article instance for you. 30 | $article = $sth->fetch(); 31 | 32 | $this->identityMap->set($id, $article); 33 | 34 | return $article; 35 | } 36 | 37 | /** 38 | * @param integer $id 39 | * @throws OutOfBoundsException 40 | * @return array A list of Article objects. 41 | */ 42 | public function findByUserId($id) 43 | { 44 | $sth = $this->db->prepare( 45 | "SELECT * FROM tbl_article WHERE userId = :userId" 46 | ); 47 | 48 | $sth->bindValue(':userId', $id, PDO::PARAM_INT); 49 | $sth->setFetchMode(PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE, 'Article', array('title', 'content')); 50 | $sth->execute(); 51 | 52 | if ($sth->rowCount() == 0) { 53 | throw new OutOfBoundsException( 54 | sprintf('No article with userId #%d exists.', $id) 55 | ); 56 | } 57 | 58 | $articles = $sth->fetchAll(); 59 | 60 | return $articles; 61 | } 62 | 63 | /** 64 | * @param Article $article 65 | * @throws MapperException 66 | * @return integer A lastInsertId. 67 | */ 68 | public function insert(Article $article) 69 | { 70 | if (true === $this->identityMap->hasObject($article)) { 71 | throw new MapperException('Object has an ID, cannot insert.'); 72 | } 73 | 74 | $sth = $this->db->prepare( 75 | "INSERT INTO tbl_article (title, content, userId) " . 76 | "VALUES (:title, :content, :userId)" 77 | ); 78 | 79 | $sth->bindValue(':title', $article->getTitle(), PDO::PARAM_STR); 80 | $sth->bindValue(':content', $article->getContent(), PDO::PARAM_STR); 81 | $sth->bindValue(':userId', $article->getUser()->getId(), PDO::PARAM_INT); 82 | $sth->execute(); 83 | 84 | $this->identityMap->set((int)$this->db->lastInsertId(), $article); 85 | 86 | return (int)$this->db->lastInsertId(); 87 | } 88 | 89 | /** 90 | * @param Article $article 91 | * @throws MapperException 92 | * @return boolean 93 | */ 94 | public function update(Article $article) 95 | { 96 | if (false === $this->identityMap->hasObject($article)) { 97 | throw new MapperException('Object has no ID, cannot update.'); 98 | } 99 | 100 | $sth = $this->db->prepare( 101 | "UPDATE tbl_article " . 102 | "SET title = :title, content = :content WHERE id = :id" 103 | ); 104 | 105 | $sth->bindValue(':title', $article->getTitle(), PDO::PARAM_STR); 106 | $sth->bindValue(':content', $article->getContent(), PDO::PARAM_STR); 107 | $sth->bindValue(':id', $this->identityMap->getId($article), PDO::PARAM_INT); 108 | $sth->execute(); 109 | 110 | if ($sth->rowCount() == 1) { 111 | return true; 112 | } 113 | 114 | return false; 115 | } 116 | 117 | /** 118 | * @param Article $article 119 | * @throws MapperException 120 | * @return boolean 121 | */ 122 | public function delete(Article $article) 123 | { 124 | if (false === $this->identityMap->hasObject($article)) { 125 | throw new MapperException('Object has no ID, cannot delete.'); 126 | } 127 | 128 | $sth = $this->db->prepare( 129 | "DELETE FROM tbl_article WHERE id = :id LIMIT 1" 130 | ); 131 | 132 | $sth->bindValue(':id', $this->identityMap->getId($article), PDO::PARAM_INT); 133 | $sth->execute(); 134 | 135 | if ($sth->rowCount() == 0) { 136 | return false; 137 | } 138 | 139 | return true; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/persistence/UserMapper.php: -------------------------------------------------------------------------------- 1 | identityMap->hasId($id)) { 12 | return $this->identityMap->getObject($id); 13 | } 14 | 15 | $sth = $this->db->prepare( 16 | 'SELECT * FROM tbl_user WHERE id = :id' 17 | ); 18 | 19 | $sth->bindValue(':id', $id, PDO::PARAM_INT); 20 | $sth->setFetchMode(PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE, 'User', array('nickname', 'password')); 21 | $sth->execute(); 22 | 23 | if ($sth->rowCount() == 0) { 24 | throw new OutOfBoundsException( 25 | sprintf('No user with id #%d exists.', $id) 26 | ); 27 | } 28 | 29 | // let pdo fetch the User instance for you. 30 | $user = $sth->fetch(); 31 | 32 | // set the protected id of user via reflection. 33 | $attribute = new ReflectionProperty($user, 'id'); 34 | $attribute->setAccessible(true); 35 | $attribute->setValue($user, $id); 36 | 37 | // load all user's articles 38 | $articleMapper = new ArticleMapper($this->db); 39 | 40 | try { 41 | 42 | $user->setArticles($articleMapper->findByUserId($id)); 43 | 44 | } catch (OutOfBoundsException $e) { 45 | // no articles at the database. 46 | } 47 | 48 | $this->identityMap->set($id, $user); 49 | 50 | return $user; 51 | } 52 | 53 | /** 54 | * @param User $user 55 | * @throws MapperException 56 | * @return integer A lastInsertId. 57 | */ 58 | public function insert(User $user) 59 | { 60 | if (true === $this->identityMap->hasObject($user)) { 61 | throw new MapperException('Object has an ID, cannot insert.'); 62 | } 63 | 64 | $sth = $this->db->prepare( 65 | "INSERT INTO tbl_user (nickname, `password`) " . 66 | "VALUES (:nick, :passwd)" 67 | ); 68 | 69 | $sth->bindValue(':nick', $user->getNickname(), PDO::PARAM_STR); 70 | $sth->bindValue(':passwd', $user->getPassword(), PDO::PARAM_STR); 71 | $sth->execute(); 72 | 73 | $id = (int)$this->db->lastInsertId(); 74 | 75 | $attribute = new ReflectionProperty($user, 'id'); 76 | $attribute->setAccessible(true); 77 | $attribute->setValue($user, $id); 78 | 79 | // if user has assosiated articles. 80 | if (true === $user->hasArticles()) { 81 | $articleMapper = new ArticleMapper($this->db); 82 | 83 | // than insert the articles too. 84 | foreach ($user->getArticles() as $article) { 85 | $article->setUser($user); 86 | $articleMapper->insert($article); 87 | } 88 | } 89 | 90 | $this->identityMap->set($id, $user); 91 | 92 | return $id; 93 | } 94 | 95 | /** 96 | * @param User $user 97 | * @throws MapperException 98 | * @return boolean 99 | */ 100 | public function update(User $user) 101 | { 102 | if (false === $this->identityMap->hasObject($user)) { 103 | throw new MapperException('Object has no ID, cannot update.'); 104 | } 105 | 106 | $sth = $this->db->prepare( 107 | "UPDATE tbl_user " . 108 | "SET nickname = :nick, `password` = :passwd WHERE id = :id" 109 | ); 110 | 111 | $sth->bindValue(':nick', $user->getNickname(), PDO::PARAM_STR); 112 | $sth->bindValue(':passwd', $user->getPassword(), PDO::PARAM_STR); 113 | $sth->bindValue(':id', $this->identityMap->getId($user), PDO::PARAM_INT); 114 | 115 | $sth->execute(); 116 | 117 | if ($sth->rowCount() == 1) { 118 | return true; 119 | } 120 | 121 | return false; 122 | } 123 | 124 | /** 125 | * @param User $user 126 | * @throws MapperException 127 | * @return boolean 128 | */ 129 | public function delete(User $user) 130 | { 131 | if (false === $this->identityMap->hasObject($user)) { 132 | throw new MapperException('Object has no ID, cannot delete.'); 133 | } 134 | 135 | $sth = $this->db->prepare( 136 | "DELETE FROM tbl_user WHERE id = :id;" 137 | ); 138 | 139 | $sth->bindValue(':id', $this->identityMap->getId($user), PDO::PARAM_INT); 140 | $sth->execute(); 141 | 142 | if ($sth->rowCount() == 0) { 143 | return false; 144 | } 145 | 146 | return true; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /test-bootstrap.php: -------------------------------------------------------------------------------- 1 | db === null) { 25 | $this->db = new PDO( 26 | $GLOBALS['DB_DSN'], 27 | $GLOBALS['DB_USER'], 28 | $GLOBALS['DB_PASSWD'], 29 | array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8") 30 | ); 31 | } 32 | } 33 | 34 | /** 35 | * @test 36 | */ 37 | public function CreatingNewInstance() 38 | { 39 | new Repository($this->db); 40 | } 41 | 42 | /** 43 | * @test 44 | */ 45 | public function LoadingEntity() 46 | { 47 | $repository = new Repository($this->db); 48 | 49 | $this->assertInstanceOf('UserMapper', $repository->load('user')); 50 | } 51 | 52 | /** 53 | * @test 54 | */ 55 | public function LoadingEntityTwiceTimeExpectingTheSameObject() 56 | { 57 | $repository = new Repository($this->db); 58 | 59 | $this->assertSame($repository->load('user'), $repository->load('user')); 60 | } 61 | 62 | /** 63 | * @test 64 | */ 65 | public function LoadingEntityAnFindOneEntry() 66 | { 67 | $repository = new Repository($this->db); 68 | $user = $repository->load('user'); 69 | 70 | $this->assertInstanceOf('User', $user->find(1)); 71 | } 72 | 73 | /** 74 | * @test 75 | */ 76 | public function InsertingNewUserAndCompareObjectsThanDelete() 77 | { 78 | $user = new User('billy', 'gatter'); 79 | $repository = new Repository($this->db); 80 | $userMapper = $repository->load('User'); 81 | $insertId = $userMapper->insert($user); 82 | $user2 = $userMapper->find($insertId); 83 | 84 | $this->assertTrue($user === $user2); 85 | $this->assertTrue($userMapper->delete($user2)); 86 | } 87 | 88 | /** 89 | * @test 90 | */ 91 | public function CreateUserWithArticlesAndFindArticlesByUserIdThanDeleteUser() 92 | { 93 | $repository = new Repository($this->db); 94 | $userMapper = $repository->load('User'); 95 | 96 | // create data. 97 | $newUser = new User('Conan', 'He rocks!'); 98 | $newUser->addArticle('Conan I', 'Some content about Conan') 99 | ->addArticle('Conan II', 'Some content about Conan') 100 | ->addArticle('Rambo III', 'Some content about Rambo'); 101 | 102 | // insert it. 103 | $lastInsertInd = $userMapper->insert($newUser); 104 | 105 | // retrieve the data from the articles. 106 | $articleMapper = $repository->load('Article'); 107 | $articles1 = $articleMapper->findByUserId($lastInsertInd); 108 | $articles2 = $articleMapper->findByUserId($lastInsertInd); 109 | 110 | $this->assertNotEmpty($articles1); 111 | $this->assertNotEmpty($articles2); 112 | 113 | foreach ($articles1 as $article) { 114 | $this->assertInstanceOf('Article', $article); 115 | } 116 | 117 | foreach ($articles2 as $article) { 118 | $this->assertInstanceOf('Article', $article); 119 | } 120 | 121 | $this->assertTrue($userMapper->delete($newUser)); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/model/ArticleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($reflectedArticle->hasProperty('title')); 14 | $this->assertEquals(null, $article->getTitle()); 15 | 16 | $this->assertTrue($reflectedArticle->hasProperty('content')); 17 | $this->assertEquals(null, $article->getContent()); 18 | 19 | $this->assertTrue($reflectedArticle->hasProperty('user')); 20 | $this->assertEquals(null, $article->getUser()); 21 | } 22 | 23 | /** 24 | * @test 25 | */ 26 | public function InstanceNoException() 27 | { 28 | new Article('the title', 'the content'); 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /tests/model/UserTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($reflectedUser->hasProperty('nickname')); 14 | $this->assertEquals(null, $user->getNickname()); 15 | 16 | $this->assertTrue($reflectedUser->hasProperty('password')); 17 | $this->assertEquals(null, $user->getPassword()); 18 | } 19 | 20 | /** 21 | * @test 22 | */ 23 | public function InstanceNoException() 24 | { 25 | $newUser = new User('maxf', 'love123'); 26 | 27 | $this->assertEquals('love123', $newUser->getPassword()); 28 | } 29 | 30 | /** 31 | * @test 32 | */ 33 | public function AddSomeArticles() 34 | { 35 | $newUser = new User('Conan', 'He rocks!'); 36 | 37 | $this->assertFalse($newUser->hasArticles()); 38 | 39 | $newUser->addArticle('Conan I', 'Some content about conan') 40 | ->addArticle('Conan I', 'Some content about conan'); 41 | 42 | $this->assertTrue($newUser->hasArticles()); 43 | $this->assertInstanceOf('Article', current($newUser->getArticles())); 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /tests/persistence/ArticleMapperTest.php: -------------------------------------------------------------------------------- 1 | db = new PDO( 14 | $GLOBALS['DB_DSN'], 15 | $GLOBALS['DB_USER'], 16 | $GLOBALS['DB_PASSWD'], 17 | array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8") 18 | ); 19 | 20 | $this->db->exec( 21 | file_get_contents( 22 | dirname(dirname(dirname(__FILE__))) . '/database/tbl_article.sql' 23 | ) 24 | ); 25 | 26 | $this->mapper = new ArticleMapper($this->db); 27 | 28 | parent::setUp(); 29 | } 30 | 31 | public function getConnection() 32 | { 33 | return $this->createDefaultDBConnection($this->db, $GLOBALS['DB_DBNAME']); 34 | } 35 | 36 | public function getDataSet() 37 | { 38 | return $this->createFlatXMLDataSet( 39 | __DIR__ . '/fixture/article-seed.xml' 40 | ); 41 | } 42 | 43 | /** 44 | * @test 45 | */ 46 | public function FindByUserId() 47 | { 48 | $articles1 = $this->mapper->findByUserId(1); 49 | $articles2 = $this->mapper->findByUserId(1); 50 | 51 | $this->assertNotEmpty($articles1); 52 | $this->assertNotEmpty($articles2); 53 | 54 | foreach ($articles1 as $article) { 55 | $this->assertInstanceOf('Article', $article); 56 | } 57 | 58 | foreach ($articles2 as $article) { 59 | $this->assertInstanceOf('Article', $article); 60 | } 61 | } 62 | 63 | /** 64 | * @test 65 | * @expectedException OutOfBoundsException 66 | */ 67 | public function deleteUserExpectingDeletingAllArticlesFromUser() 68 | { 69 | // create data. 70 | $newUser = new User('Conan', 'He rocks!'); 71 | $newUser->addArticle('Conan I', 'Some content about Conan') 72 | ->addArticle('Conan II', 'Some content about Conan') 73 | ->addArticle('Rambo III', 'Some content about Rambo'); 74 | 75 | // use user-mapper to insert data. 76 | $userMapper = new UserMapper($this->db); 77 | 78 | $lastUserId = $userMapper->insert($newUser); 79 | 80 | // than delete user with all his articles. 81 | $userMapper->delete($newUser); 82 | 83 | // this one throws the expected OutOfBoundsException. 84 | $this->mapper->findByUserId($lastUserId); 85 | } 86 | 87 | /** 88 | * @test 89 | */ 90 | public function UpdateArticle() 91 | { 92 | $article1 = $this->mapper->find(2); 93 | 94 | $article1->setTitle('conan')->setContent('the barbar'); 95 | 96 | $res = $this->mapper->update($article1); 97 | 98 | $this->assertTrue($res); 99 | } 100 | 101 | /** 102 | * @test 103 | */ 104 | public function DeleteArticle() 105 | { 106 | // create new user and insert. 107 | $user = new User('ZZ', 'TOP'); 108 | $userMapper = new UserMapper($this->db); 109 | $userMapper->insert($user); 110 | 111 | // create an article and assosiate it to the user. 112 | $article = new Article('Make Love', 'Some content about love'); 113 | $article->setUser($user); 114 | $lastArticleId = $this->mapper->insert($article); 115 | 116 | unset($this->mapper); 117 | 118 | $this->mapper = new ArticleMapper($this->db); 119 | 120 | $this->mapper->delete($this->mapper->find($lastArticleId)); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /tests/persistence/UserMapperTest.php: -------------------------------------------------------------------------------- 1 | db = new PDO( 14 | $GLOBALS['DB_DSN'], 15 | $GLOBALS['DB_USER'], 16 | $GLOBALS['DB_PASSWD'], 17 | array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8") 18 | ); 19 | 20 | $this->db->exec( 21 | file_get_contents( 22 | dirname(dirname(dirname(__FILE__))) . '/database/tbl_user.sql' 23 | ) 24 | ); 25 | 26 | $this->mapper = new UserMapper($this->db); 27 | 28 | parent::setUp(); 29 | } 30 | 31 | public function getConnection() 32 | { 33 | return $this->createDefaultDBConnection($this->db, $GLOBALS['DB_DBNAME']); 34 | } 35 | 36 | public function getDataSet() 37 | { 38 | return $this->createFlatXMLDataSet( 39 | __DIR__ . '/fixture/user-seed.xml' 40 | ); 41 | } 42 | 43 | /** 44 | * @test 45 | */ 46 | public function UserCanBeFoundById() 47 | { 48 | $user = $this->mapper->find(1); 49 | 50 | $this->assertEquals('joe123', $user->getNickname()); 51 | } 52 | 53 | /** 54 | * @test 55 | */ 56 | public function InsertingNewUserAndCompareObjectsThanDelete() 57 | { 58 | $user = new User('billy', 'gatter'); 59 | 60 | $insertId = $this->mapper->insert($user); 61 | 62 | $user2 = $this->mapper->find($insertId); 63 | 64 | $this->assertTrue($user === $user2); 65 | $this->assertTrue($this->mapper->delete($user2)); 66 | } 67 | 68 | /** 69 | * @test 70 | * @expectedException OutOfBoundsException 71 | */ 72 | public function UserCanNotBeFoundById() 73 | { 74 | $this->mapper->find(123); 75 | } 76 | 77 | /** 78 | * @test 79 | */ 80 | public function UserCanBeInserted() 81 | { 82 | $newUser = new User('maxf', 'love123'); 83 | 84 | $lastinsertId = $this->mapper->insert($newUser); 85 | 86 | $this->assertEquals(3, $lastinsertId); 87 | 88 | $user = $this->mapper->find($lastinsertId); 89 | 90 | $this->assertEquals('maxf', $user->getNickname()); 91 | } 92 | 93 | /** 94 | * @test 95 | */ 96 | public function IdentityMapInteractionAndConsistency() 97 | { 98 | $user1 = $this->mapper->find(1); 99 | $user2 = $this->mapper->find(1); 100 | 101 | // expects same nickname in each object. 102 | $this->assertEquals($user2->getNickname(), $user1->getNickname()); 103 | 104 | // update the nickname on user1. 105 | $user2->setNickname('tucker'); 106 | 107 | // expects same nickname in each object. 108 | $this->assertEquals($user2->getNickname(), $user1->getNickname()); 109 | 110 | // than update into the database. 111 | $this->mapper->update($user2); 112 | } 113 | 114 | /** 115 | * @test 116 | */ 117 | public function PersistUserWithSomeArticles() 118 | { 119 | $newUser = new User('Conan', 'He rocks!'); 120 | $newUser->addArticle('Conan I', 'Some content about Conan') 121 | ->addArticle('Conan II', 'Some content about Conan') 122 | ->addArticle('Rambo III', 'Some content about Rambo'); 123 | 124 | $lastUserId = $this->mapper->insert($newUser); 125 | 126 | // unset the user-mapper and the identity-map - force db connection. 127 | unset($this->mapper); 128 | 129 | // create new user-mapper with new identity-map. 130 | $this->mapper = new UserMapper($this->db); 131 | 132 | $user = $this->mapper->find($lastUserId); 133 | 134 | foreach ($user->getArticles() as $article) { 135 | $this->assertInstanceOf('Article', $article); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /tests/persistence/fixture/article-seed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 15 | -------------------------------------------------------------------------------- /tests/persistence/fixture/user-seed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /uml-php-identity-map.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gjerokrsteski/php-identity-map/5c22834e4c3ae2e4ab7fa8a565f25ba39878c89a/uml-php-identity-map.gif --------------------------------------------------------------------------------