├── .gitignore ├── .php_cs ├── .scrutinizer.yml ├── .styleci.yml ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Driver.php ├── Driver │ ├── DynamoDb │ │ ├── Driver.php │ │ └── Style.php │ ├── Exception.php │ └── MongoDb │ │ ├── AbstractDriver.php │ │ ├── Driver.php │ │ ├── MongoDbDriver.php │ │ ├── MongoDriver.php │ │ └── Style.php └── Mapper.php └── tests ├── Driver ├── DynamoDb │ ├── DriverTest.php │ └── StyleTest.php ├── MongoDb │ ├── DriverTest.php │ ├── MongoDbDriverTest.php │ ├── MongoDriverTest.php │ └── StyleTest.php └── TestCase.php └── MapperTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.7z 3 | *.bzip 4 | *.deb 5 | *.dmg 6 | *.egg 7 | *.gem 8 | *.gz 9 | *.iso 10 | *.jar 11 | *.lock 12 | *.lzma 13 | *.phar 14 | *.rar 15 | *.rpm 16 | *.swo 17 | *.swp 18 | *.tar 19 | *.tgz 20 | *.xpi 21 | *.xz 22 | *.zip 23 | /pirum 24 | /nbproject 25 | /build/coverage 26 | /vendor 27 | /phpunit.xml 28 | /composer.lock 29 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | files() 5 | ->name('*.php') 6 | ->name('*.phpt') 7 | ->in('src') 8 | ->in('tests'); 9 | 10 | return Symfony\CS\Config\Config::create() 11 | ->level(\Symfony\CS\FixerInterface::PSR2_LEVEL) 12 | ->fixers(array( 13 | '-psr0', 14 | // All items of the @param, @throws, @return, @var, and @type phpdoc 15 | // tags must be aligned vertically. 16 | 'phpdoc_params', 17 | // Convert double quotes to single quotes for simple strings. 18 | 'single_quote', 19 | // Group and seperate @phpdocs with empty lines. 20 | 'phpdoc_separation', 21 | // An empty line feed should precede a return statement. 22 | 'return', 23 | // PHP arrays should use the PHP 5.4 short-syntax. 24 | 'short_array_syntax', 25 | // Remove trailing whitespace at the end of blank lines. 26 | 'whitespacy_lines', 27 | // Removes extra empty lines. 28 | 'extra_empty_lines', 29 | // Unused use statements must be removed. 30 | 'unused_use', 31 | // Ordering use statements. 32 | 'ordered_use', 33 | )) 34 | ->finder($finder); 35 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: 3 | - 'vendor/*' 4 | - 'tests/*' 5 | paths: 6 | - 'src/*' 7 | tools: 8 | php_analyzer: true 9 | php_changetracking: true 10 | php_code_sniffer: 11 | enabled: true 12 | config: 13 | standard: PSR2 14 | php_cpd: true 15 | php_mess_detector: true 16 | php_pdepend: true 17 | php_sim: true 18 | 19 | build: 20 | dependencies: 21 | before: 22 | - "yes '' | pecl -q install -f mongo-stable" 23 | - "yes '' | pecl -q install -f mongodb-stable" 24 | - "sudo composer self-update" 25 | override: 26 | - composer install --no-interaction --prefer-dist 27 | tests: 28 | override: 29 | - 30 | command: 'composer coverage' 31 | coverage: 32 | file: 'clover.xml' 33 | format: 'php-clover' 34 | environment: 35 | php: 36 | version: 5.6 37 | mysql: false 38 | postgresql: false 39 | redis: false 40 | elasticsearch: false 41 | rabbitmq: false 42 | mongodb: false 43 | neo4j: false 44 | memcached: false 45 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: PSR2 2 | 3 | risky: false 4 | 5 | linting: true 6 | 7 | enabled: 8 | - no_extra_consecutive_blank_lines 9 | - no_unused_imports 10 | - no_whitespace_in_blank_lines 11 | - ordered_imports 12 | - phpdoc_align 13 | - phpdoc_separation 14 | - return 15 | - short_array_syntax 16 | - single_quote 17 | - unalign_double_arrow 18 | 19 | finder: 20 | exclude: 21 | - "*.yml" 22 | - "*.json" 23 | - "*.lock" 24 | - "*.xml" 25 | - "*.md" 26 | name: 27 | - "*.php" 28 | path: 29 | - "src" 30 | - "tests" -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: 2 | php 3 | 4 | php: 5 | - 5.6 6 | - 7.0 7 | 8 | install: 9 | - pecl channel-update pecl.php.net 10 | - if [ $TRAVIS_PHP_VERSION = "5.6" ]; then yes '' | pecl -q install -f mongo-stable && php --ri mongo; fi; 11 | - yes '' | pecl -q install -f mongodb-stable && php --ri mongodb || exit 0 12 | 13 | before_script: 14 | - composer self-update 15 | - composer install --no-interaction --prefer-dist 16 | 17 | script: 18 | - composer tests 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2016, Alexandre Gomes Gaigalas. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Alexandre Gomes Gaigalas nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Respect/Structural 2 | ================== 3 | 4 | [![Latest Stable Version](https://poser.pugx.org/respect/structural/v/stable)](https://packagist.org/packages/respect/structural) 5 | [![Total Downloads](https://poser.pugx.org/respect/structural/downloads)](https://packagist.org/packages/respect/structural) 6 | [![Latest Unstable Version](https://poser.pugx.org/respect/structural/v/unstable)](https://packagist.org/packages/respect/structural) 7 | [![License](https://poser.pugx.org/respect/structural/license)](https://packagist.org/packages/respect/structural) 8 | 9 | [![Build Status](https://travis-ci.org/Respect/Structural.svg?branch=master)](https://travis-ci.org/Respect/Structural) 10 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Respect/Structural/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Respect/Structural/?branch=master) 11 | [![Code Coverage](https://scrutinizer-ci.com/g/Respect/Structural/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/Respect/Structural/?branch=master) 12 | [![StyleCI](https://styleci.io/repos/2753337/shield)](https://styleci.io/repos/2753337) 13 | 14 | ### The Near-zero Part 15 | 16 | ```php 17 | // bootstrap.php 18 | require_once __DIR__ . '/vendor/autoload.php'; 19 | 20 | use Respect\Structural\Mapper; 21 | use Respect\Structural\Driver\MongoDb\Style as MongoDbStyle; 22 | use Respect\Structural\Driver\MongoDb\Driver as MongoDbDriver; 23 | 24 | $driver = MongoDbDriver::factory('respect'); 25 | 26 | $mapper = new Mapper($driver); 27 | $mapper->setStyle(new MongoDbStyle()); 28 | ``` 29 | 30 | ### Persisting 31 | ```php 32 | $author = new \stdClass(); 33 | $author->firstName = 'Antonio'; 34 | $mapper->authors->persist($author); 35 | $mapper->flush(); 36 | 37 | echo "'{$author->firstName}' was created with id({$author->_id})".PHP_EOL; 38 | ``` 39 | 40 | ### Updating 41 | ```php 42 | $author->lastName = 'Spinelli'; 43 | $mapper->authors->persist($author); 44 | $mapper->flush(); 45 | 46 | echo "last name was updated to '{$author->lastName}' from id({$author->_id})".PHP_EOL; 47 | ``` 48 | 49 | ### Fetching 50 | ```php 51 | $authors = $mapper->authors->fetchAll(); 52 | 53 | echo "Fetching all authors:" . PHP_EOL; 54 | foreach ($authors as $index => $author) { 55 | echo "{$index} {$author->firstName} {$author->lastName}" . PHP_EOL; 56 | } 57 | ``` 58 | 59 | ### Condition 60 | ```php 61 | // find author by ID 62 | $foundAuthor = $mapper->authors[(string)$author->_id]->fetch(); 63 | echo "find by id('{$author->_id}') {$foundAuthor->firstName} {$foundAuthor->lastName}".PHP_EOL; 64 | ``` 65 | 66 | ### Removing 67 | ```php 68 | $mapper->authors->remove($author); 69 | $mapper->flush(); 70 | 71 | $author = $mapper->authors(['lastName' => 'Spinelli'])->fetch(); 72 | echo ($author ? "'Spinelli' was found" : "'Spinelli' removed."); 73 | ``` 74 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "respect/structural", 3 | "description": "Fluent NoSQL Toolkit", 4 | "keywords": ["nosql", "mongo", "mongodb", "aws", "dynamodb", "data", "odm", "respect", "aws dynamo", "dynamo", "aws dynamodb"], 5 | "type": "library", 6 | "time": "2012-05-05 19:11:23", 7 | "homepage": "http://respect.li", 8 | "license": "BSD Style", 9 | "support": { 10 | "issues": "https://github.com/Respect/Structural/issues" 11 | }, 12 | "authors": [ 13 | { 14 | "name": "Alexandre Gomes Gaigalas", 15 | "email": "alexandre@gaigalas.net" 16 | }, 17 | { 18 | "name": "Antonio Spinelli", 19 | "email": "tonicospinelli85@gmail.com" 20 | }, 21 | { 22 | "name": "Claudson Oliveira", 23 | "email": "claudsonweb@gmail.com" 24 | }, 25 | { 26 | "name": "Fa\u0301bio da Silva Ribeiro", 27 | "email": "fabiorphp@gmail.com" 28 | }, 29 | { 30 | "name": "Henrique Moody", 31 | "email": "henriquemoody@gmail.com" 32 | }, 33 | { 34 | "name": "Kinn Coelho Julia\u0303o", 35 | "email": "kinncj@gmail.com" 36 | }, 37 | { 38 | "name": "Rogerio Prado de Jesus", 39 | "email": "rogeriopradoj@gmail.com" 40 | }, 41 | { 42 | "name": "Thiago Rigo", 43 | "email": "thiagophx@gmail.com" 44 | } 45 | ], 46 | "require": { 47 | "php": "^5.6 || ^7.0", 48 | "respect/data": "^0.2", 49 | "ramsey/uuid": "^3.2" 50 | }, 51 | "require-dev": { 52 | "phpunit/phpunit": "^5.2", 53 | "phpmd/phpmd": "^2.3", 54 | "squizlabs/php_codesniffer": "^2.5", 55 | "fabpot/php-cs-fixer": "^1.11", 56 | "mongodb/mongodb": "^1.0.0", 57 | "aws/aws-sdk-php": "^3.15" 58 | }, 59 | "suggest": { 60 | "ext-mongo": "@stable", 61 | "ext-mongodb": "@stable", 62 | "mongodb/mongodb": "^1.0.0", 63 | "aws/aws-sdk-php": "^3.15" 64 | }, 65 | "scripts": { 66 | "tests": "phpunit --colors", 67 | "coverage-html": "phpunit --coverage-html build/coverage tests", 68 | "coverage": "phpunit -c phpunit.xml.dist --coverage-clover=clover.xml", 69 | "md": "phpmd src,tests text cleancode,codesize,controversial,design,naming,unusedcode", 70 | "csfix": "php-cs-fixer fix" 71 | }, 72 | "autoload": { 73 | "psr-4": { 74 | "Respect\\Structural\\": "src/" 75 | } 76 | }, 77 | "autoload-dev": { 78 | "psr-4": { 79 | "Respect\\Structural\\Tests\\": "tests/" 80 | } 81 | }, 82 | "extra": { 83 | "branch-alias": { 84 | "dev-master": "0.1.x-dev" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | tests 19 | 20 | 21 | 22 | 23 | src 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Driver.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 38 | $this->marshaler = new Marshaler(); 39 | $this->uuid = $uuid; 40 | } 41 | 42 | /** 43 | * @return \Aws\DynamoDb\DynamoDbClient 44 | */ 45 | public function getConnection() 46 | { 47 | return $this->connection; 48 | } 49 | 50 | /** 51 | * @param \Iterator $cursor 52 | * 53 | * @return array 54 | */ 55 | public function fetch(\Iterator $cursor) 56 | { 57 | $data = []; 58 | 59 | if ($cursor->valid()) { 60 | $data = $cursor->current(); 61 | $cursor->next(); 62 | } 63 | 64 | return $data; 65 | } 66 | 67 | /** 68 | * @param array $collection 69 | * @param array $query 70 | * 71 | * @return \Iterator 72 | */ 73 | public function find($collection, array $query = []) 74 | { 75 | $expression = [ 76 | 'TableName' => $collection, 77 | ]; 78 | 79 | if (!empty($query)) { 80 | $expression['ScanFilter'] = $query; 81 | } 82 | 83 | $result = $this->getConnection()->scan($expression); 84 | 85 | return $this->formatResults($result); 86 | } 87 | 88 | /** 89 | * @param Collection $collection 90 | * 91 | * @return array 92 | */ 93 | public function generateQuery(Collection $collection) 94 | { 95 | return $this->parseConditions($collection); 96 | } 97 | 98 | /** 99 | * @param Collection $collection 100 | * 101 | * @return array 102 | */ 103 | protected function parseConditions(Collection $collection) 104 | { 105 | $collections = iterator_to_array( 106 | CollectionIterator::recursive($collection) 107 | ); 108 | 109 | $collections = array_slice($collections, 1); 110 | $condition = $this->getConditionArray($collection); 111 | 112 | foreach ($collections as $name => $coll) { 113 | $condition += $this->getConditionArray($coll); 114 | } 115 | 116 | return $condition; 117 | } 118 | 119 | /** 120 | * @param Collection $collection 121 | * 122 | * @return array 123 | */ 124 | protected function getConditionArray(Collection $collection) 125 | { 126 | $condition = $collection->getCondition(); 127 | 128 | if (!is_array($condition)) { 129 | $condition = ['_id' => $condition]; 130 | } 131 | 132 | $conditions = []; 133 | 134 | foreach ($condition as $key => $value) { 135 | $conditions = [ 136 | $key => [ 137 | 'AttributeValueList' => [ 138 | $this->marshaler->marshalValue($value), 139 | ], 140 | 'ComparisonOperator' => 'EQ', 141 | ], 142 | ]; 143 | } 144 | 145 | return $conditions; 146 | } 147 | 148 | /** 149 | * @param Collection $collection 150 | * @param $document 151 | */ 152 | public function insert($collection, $document) 153 | { 154 | $document->_id = $this->uuid->uuid4()->toString(); 155 | 156 | $args = [ 157 | 'TableName' => $collection, 158 | 'Item' => $this->marshaler->marshalItem($document), 159 | ]; 160 | 161 | $this->getConnection()->putItem($args); 162 | } 163 | 164 | /** 165 | * @param Collection $collection 166 | * @param $criteria 167 | * @param $document 168 | */ 169 | public function update($collection, $criteria, $document) 170 | { 171 | $args = [ 172 | 'TableName' => $collection, 173 | 'Key' => $this->marshaler->marshalItem($criteria), 174 | 'AttributeUpdates' => $this->formatAttributes($document), 175 | ]; 176 | 177 | $this->getConnection()->updateItem($args); 178 | } 179 | 180 | /** 181 | * @param Collection $collection 182 | * @param $criteria 183 | */ 184 | public function remove($collection, $criteria) 185 | { 186 | $args = [ 187 | 'TableName' => $collection, 188 | 'Key' => $this->marshaler->marshalItem($criteria), 189 | ]; 190 | 191 | $this->getConnection()->deleteItem($args); 192 | } 193 | 194 | /** 195 | * @param $values 196 | * 197 | * @return array 198 | */ 199 | protected function formatAttributes($values) 200 | { 201 | $attributes = []; 202 | 203 | foreach ($values as $key => $value) { 204 | $attributes[$key] = [ 205 | 'Value' => $this->marshaler->marshalValue($value), 206 | ]; 207 | } 208 | 209 | return $attributes; 210 | } 211 | 212 | protected function formatResults(\Aws\Result $result) 213 | { 214 | $items = new \ArrayIterator(); 215 | 216 | if ($result['Count'] === 0) { 217 | return $items; 218 | } 219 | 220 | foreach ($result['Items'] as $item) { 221 | $items[] = $this->marshaler->unmarshalItem($item); 222 | } 223 | 224 | return $items; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/Driver/DynamoDb/Style.php: -------------------------------------------------------------------------------- 1 | parseConditions($collection); 31 | } 32 | 33 | /** 34 | * @param Collection $collection 35 | * 36 | * @return array 37 | */ 38 | protected function parseConditions(Collection $collection) 39 | { 40 | $allCollections = CollectionIterator::recursive($collection); 41 | $allCollections = iterator_to_array($allCollections); 42 | $allCollections = array_slice($allCollections, 1); 43 | 44 | $condition = $this->getConditionArray($collection); 45 | 46 | foreach ($allCollections as $coll) { 47 | $condition += $this->getConditionArray($coll, true); 48 | } 49 | 50 | return $condition; 51 | } 52 | 53 | /** 54 | * @param Collection $collection 55 | * @param bool|false $prefix 56 | * 57 | * @return array 58 | */ 59 | protected function getConditionArray(Collection $collection, $prefix = false) 60 | { 61 | $condition = $collection->getCondition(); 62 | 63 | if (!is_array($condition)) { 64 | $condition = ['_id' => $this->createObjectId($condition)]; 65 | } 66 | 67 | if ($prefix) { 68 | $condition = static::prefixArrayKeys($condition, $collection->getName() . '.'); 69 | } 70 | 71 | return $condition; 72 | } 73 | 74 | /** 75 | * @param array $array 76 | * @param string $prefix 77 | * 78 | * @return array 79 | */ 80 | protected static function prefixArrayKeys(array $array, $prefix) 81 | { 82 | return array_combine( 83 | array_map( 84 | function ($key) use ($prefix) { 85 | return "{$prefix}{$key}"; 86 | }, array_keys($array)), 87 | $array 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Driver/MongoDb/Driver.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 19 | } 20 | 21 | /** 22 | * @param string $database 23 | * @param string $server 24 | * @param array $options 25 | * @param array $driverOptions 26 | * 27 | * @return Driver 28 | * 29 | * @throws DriverException 30 | */ 31 | public static function factoryLegacy( 32 | $database, 33 | $server = 'mongodb://localhost:27017', 34 | array $options = ['connect' => false], 35 | array $driverOptions = [] 36 | ) { 37 | if (!extension_loaded('mongo')) { 38 | throw DriverException::extensionNotLoaded('mongo'); 39 | } 40 | 41 | $client = new \MongoClient($server, $options, $driverOptions); 42 | $driver = new MongoDriver($client, $database); 43 | 44 | return new self($driver); 45 | } 46 | 47 | /** 48 | * @param string $database 49 | * @param string $uri 50 | * @param array $uriOptions 51 | * @param array $driverOptions 52 | * 53 | * @return Driver 54 | * 55 | * @throws DriverException 56 | */ 57 | public static function factory( 58 | $database, 59 | $uri = 'mongodb://localhost:27017', 60 | array $uriOptions = [], 61 | array $driverOptions = [] 62 | ) { 63 | if (!extension_loaded('mongodb')) { 64 | throw DriverException::extensionNotLoaded('mongodb'); 65 | } 66 | $client = new \MongoDB\Client($uri, $uriOptions, $driverOptions); 67 | $driver = new MongoDbDriver($client, $database); 68 | 69 | return new self($driver); 70 | } 71 | 72 | /** 73 | * @return Driver 74 | */ 75 | public function getConnection() 76 | { 77 | return $this->connection; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function fetch(\Iterator $cursor) 84 | { 85 | return $this->getConnection()->fetch($cursor); 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function find($collection, array $query = []) 92 | { 93 | return $this->getConnection()->find($collection, $query); 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function generateQuery(Collection $collection) 100 | { 101 | return $this->getConnection()->generateQuery($collection); 102 | } 103 | 104 | /** 105 | * @param Collection $collection 106 | * @param $document 107 | * 108 | * @return void 109 | */ 110 | public function insert($collection, $document) 111 | { 112 | $this->getConnection()->insert($collection, $document); 113 | } 114 | 115 | /** 116 | * {@inheritdoc} 117 | */ 118 | public function update($collection, $criteria, $document) 119 | { 120 | $this->getConnection()->update($collection, $criteria, $document); 121 | } 122 | 123 | /** 124 | * {@inheritdoc} 125 | */ 126 | public function remove($collection, $criteria) 127 | { 128 | $this->getConnection()->remove($collection, $criteria); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Driver/MongoDb/MongoDbDriver.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 30 | $this->database = $connection->selectDatabase($database); 31 | } 32 | 33 | /** 34 | * @return \MongoDB\Database 35 | */ 36 | public function getDatabase() 37 | { 38 | return $this->database; 39 | } 40 | 41 | /** 42 | * @param int|string $id 43 | * 44 | * @return ObjectID 45 | */ 46 | public function createObjectId($id) 47 | { 48 | return new ObjectID($id); 49 | } 50 | 51 | /** 52 | * @return MongoDBClient 53 | */ 54 | public function getConnection() 55 | { 56 | return $this->connection; 57 | } 58 | 59 | /** 60 | * @param \Iterator $cursor 61 | * 62 | * @return array 63 | */ 64 | public function fetch(\Iterator $cursor) 65 | { 66 | $data = null; 67 | if ($cursor->valid()) { 68 | $data = $cursor->current(); 69 | $cursor->next(); 70 | } 71 | 72 | return $data; 73 | } 74 | 75 | /** 76 | * @param string $collection 77 | * @param array $query 78 | * 79 | * @return \Iterator 80 | */ 81 | public function find($collection, array $query = []) 82 | { 83 | $cursor = $this->getDatabase()->selectCollection($collection)->find($query); 84 | $iterator = new \ArrayIterator($cursor); 85 | $iterator->rewind(); 86 | 87 | return $iterator; 88 | } 89 | 90 | /** 91 | * @param string $collection 92 | * @param $document 93 | * 94 | * @return void 95 | */ 96 | public function insert($collection, $document) 97 | { 98 | $result = $this->getDatabase()->selectCollection($collection)->insertOne($document); 99 | $document->_id = $result->getInsertedId(); 100 | } 101 | 102 | public function update($collection, $criteria, $document) 103 | { 104 | $this->getDatabase()->selectCollection($collection)->updateOne($criteria, ['$set' => $document]); 105 | } 106 | 107 | /** 108 | * @param string $collection 109 | * @param array $criteria 110 | */ 111 | public function remove($collection, $criteria) 112 | { 113 | $this->getDatabase()->selectCollection($collection)->deleteOne($criteria); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Driver/MongoDb/MongoDriver.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 28 | $this->database = $connection->selectDB($database); 29 | } 30 | 31 | /** 32 | * @return \MongoDB 33 | */ 34 | public function getDatabase() 35 | { 36 | return $this->database; 37 | } 38 | 39 | public function getConnection() 40 | { 41 | return $this->connection; 42 | } 43 | 44 | /** 45 | * @param \Iterator $cursor 46 | * 47 | * @return array 48 | */ 49 | public function fetch(\Iterator $cursor) 50 | { 51 | $data = $cursor->current(); 52 | $cursor->next(); 53 | 54 | return $data; 55 | } 56 | 57 | /** 58 | * @param array $collection 59 | * @param array $query 60 | * 61 | * @return \Iterator 62 | */ 63 | public function find($collection, array $query = []) 64 | { 65 | $cursor = $this->getDatabase()->selectCollection($collection)->find($query); 66 | $cursor->rewind(); 67 | 68 | return $cursor; 69 | } 70 | 71 | /** 72 | * @param int|string $id 73 | * 74 | * @return \MongoId|\MongoInt32 75 | */ 76 | public function createObjectId($id) 77 | { 78 | if (is_int($id)) { 79 | return new \MongoInt32($id); 80 | } 81 | 82 | return new \MongoId($id); 83 | } 84 | 85 | /** 86 | * @param Collection $collection 87 | * @param $document 88 | * 89 | * @return void 90 | */ 91 | public function insert($collection, $document) 92 | { 93 | $this->getDatabase()->selectCollection($collection)->insert($document); 94 | } 95 | 96 | public function update($collection, $criteria, $document) 97 | { 98 | $this->getDatabase()->selectCollection($collection)->update($criteria, $document); 99 | } 100 | 101 | public function remove($collection, $criteria) 102 | { 103 | $this->getDatabase()->selectCollection($collection)->remove($criteria); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Driver/MongoDb/Style.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 32 | } 33 | 34 | /** 35 | * @return Mapper 36 | */ 37 | public function __get($name) 38 | { 39 | return parent::__get($name); 40 | } 41 | 42 | /** 43 | * Flushes a single instance into the database. This method supports 44 | * mixing, so flushing a mixed instance will flush distinct tables on the 45 | * database 46 | * 47 | * @param object $entity Entity instance to be flushed 48 | * 49 | * @return null 50 | */ 51 | protected function flushSingle($entity) 52 | { 53 | $coll = $this->tracked[$entity]; 54 | $cols = $this->extractColumns($entity, $coll); 55 | 56 | if ($this->removed->contains($entity)) { 57 | $this->rawDelete($coll, $entity); 58 | } elseif ($this->new->contains($entity)) { 59 | $this->rawInsert($coll, $entity); 60 | } else { 61 | $this->rawUpdate($cols, $coll); 62 | } 63 | } 64 | 65 | public function persist($object, Collection $onCollection) 66 | { 67 | $next = $onCollection->getNext(); 68 | 69 | if ($this->filterable($onCollection)) { 70 | $next->setMapper($this); 71 | $next->persist($object); 72 | 73 | return; 74 | } 75 | 76 | if ($next) { 77 | $remote = $this->getStyle()->remoteIdentifier($next->getName()); 78 | $next->setMapper($this); 79 | $next->persist($object->$remote); 80 | } 81 | 82 | foreach ($onCollection->getChildren() as $child) { 83 | $remote = $this->getStyle()->remoteIdentifier($child->getName()); 84 | $child->persist($object->$remote); 85 | } 86 | 87 | return parent::persist($object, $onCollection); 88 | } 89 | 90 | /** 91 | * Receives columns from an entity and her collection. Returns the columns 92 | * that belong only to the main entity. This method supports mixing, so 93 | * extracting mixins will also persist them on their respective 94 | * tables 95 | * 96 | * @param \Respect\Data\Collections\Collection $collection Target collection 97 | * @param array $cols Entity columns 98 | * 99 | * @return array Columns left for the main collection 100 | */ 101 | protected function extractAndOperateMixins(Collection $collection, $cols) 102 | { 103 | if (!$this->mixable($collection)) { 104 | return $cols; 105 | } 106 | 107 | foreach ($this->getMixins($collection) as $mix => $spec) { 108 | //Extract from $cols only the columns from the mixin 109 | $mixCols = array_intersect_key( 110 | $cols, 111 | array_combine(//create array with keys only 112 | $spec, 113 | array_fill(0, count($spec), '') 114 | ) 115 | ); 116 | if (isset($cols["{$mix}_id"])) { 117 | $mixCols['id'] = $cols["{$mix}_id"]; 118 | $cols = array_diff($cols, $mixCols); //Remove mixin columns 119 | $this->rawUpdate($mixCols, $this->__get($mix)); 120 | } else { 121 | $mixCols['id'] = null; 122 | $cols = array_diff($cols, $mixCols); //Remove mixin columns 123 | $this->rawinsert($mixCols, $this->__get($mix)); 124 | } 125 | } 126 | 127 | return $cols; 128 | } 129 | 130 | protected function guessCondition(&$columns, Collection $collection) 131 | { 132 | $primaryName = $this->getStyle()->identifier($collection->getName()); 133 | $condition = [$primaryName => $columns[$primaryName]]; 134 | unset($columns[$primaryName]); 135 | 136 | return $condition; 137 | } 138 | 139 | protected function rawDelete(Collection $collection, $entity) 140 | { 141 | $name = $collection->getName(); 142 | $columns = $this->extractColumns($entity, $collection); 143 | $condition = $this->guessCondition($columns, $collection); 144 | 145 | return $this->driver->remove($name, $condition); 146 | } 147 | 148 | protected function rawUpdate(array $columns, Collection $collection) 149 | { 150 | $columns = $this->extractAndOperateMixins($collection, $columns); 151 | $name = $collection->getName(); 152 | $condition = $this->guessCondition($columns, $collection); 153 | 154 | $this->driver->update($name, $condition, $columns); 155 | } 156 | 157 | protected function rawInsert(Collection $collection, $entity = null) 158 | { 159 | $name = $collection->getName(); 160 | $this->driver->insert($name, $entity); 161 | } 162 | 163 | public function flush() 164 | { 165 | try { 166 | foreach ($this->changed as $entity) { 167 | $this->flushSingle($entity); 168 | } 169 | } catch (Exception $e) { 170 | throw $e; 171 | } 172 | 173 | $this->reset(); 174 | } 175 | 176 | protected function createStatement(Collection $collection, $withExtra = null) 177 | { 178 | $query = $this->generateQuery($collection); 179 | 180 | $withExtraList = (array)$withExtra; 181 | 182 | $withExtraList = array_merge($withExtraList, $query); 183 | 184 | return $this->driver->find($collection->getName(), $withExtraList); 185 | } 186 | 187 | protected function generateQuery(Collection $collection) 188 | { 189 | return $this->driver->generateQuery($collection); 190 | } 191 | 192 | protected function extractColumns($entity, Collection $collection) 193 | { 194 | $cols = get_object_vars($entity); 195 | 196 | return $cols; 197 | } 198 | 199 | protected function hasComposition($entity, $next, $parent) 200 | { 201 | $style = $this->getStyle(); 202 | 203 | return $entity === $style->composed($parent, $next) || $entity === $style->composed($next, $parent); 204 | } 205 | 206 | protected function fetchSingle(Collection $collection, $statement) 207 | { 208 | $name = $collection->getName(); 209 | $entityName = $name; 210 | $row = $this->driver->fetch($statement); 211 | 212 | if (!$row) { 213 | return false; 214 | } 215 | 216 | if ($this->typable($collection)) { 217 | $entityName = $row->{$this->getType($collection)}; 218 | } 219 | 220 | $entities = new SplObjectStorage(); 221 | $entities[$this->transformSingleRow($row, $entityName)] = $collection; 222 | 223 | return $entities; 224 | } 225 | 226 | protected function getNewEntityByName($entityName) 227 | { 228 | $entityName = $this->getStyle()->styledName($entityName); 229 | $entityClass = $this->entityNamespace . $entityName; 230 | $entityClass = class_exists($entityClass) ? $entityClass : '\stdClass'; 231 | 232 | return new $entityClass; 233 | } 234 | 235 | protected function transformSingleRow($row, $entityName) 236 | { 237 | $newRow = $this->getNewEntityByName($entityName); 238 | 239 | foreach ($row as $prop => $value) { 240 | $this->inferSet($newRow, $prop, $value); 241 | } 242 | 243 | return $newRow; 244 | } 245 | 246 | protected function inferSet(&$entity, $prop, $value) 247 | { 248 | try { 249 | $mirror = new \ReflectionProperty($entity, $prop); 250 | $mirror->setAccessible(true); 251 | $mirror->setValue($entity, $value); 252 | } catch (\ReflectionException $e) { 253 | $entity->$prop = $value; 254 | } 255 | } 256 | 257 | protected function fetchMulti(Collection $collection, $statement) 258 | { 259 | $row = $this->driver->fetch($statement); 260 | 261 | if (!$row) { 262 | return false; 263 | } 264 | 265 | $this->postHydrate( 266 | $entities = $this->createEntities($row, $statement, $collection) 267 | ); 268 | 269 | return $entities; 270 | } 271 | 272 | protected function createEntities($row, $statement, Collection $collection) 273 | { 274 | $entities = new SplObjectStorage(); 275 | $entitiesInstances = $this->buildEntitiesInstances($collection, $entities); 276 | $entityInstance = array_pop($entitiesInstances); 277 | 278 | //Reversely traverses the columns to avoid conflicting foreign key names 279 | foreach (array_reverse($row, true) as $col => $value) { 280 | $columnMeta = $statement->getColumnMeta($col); 281 | $columnName = $columnMeta['name']; 282 | $primaryName = $this->getStyle()->identifier( 283 | $entities[$entityInstance]->getName() 284 | ); 285 | 286 | $this->inferSet($entityInstance, $columnName, $value); 287 | 288 | if ($primaryName == $columnName) { 289 | $entityInstance = array_pop($entitiesInstances); 290 | } 291 | } 292 | 293 | return $entities; 294 | } 295 | 296 | protected function buildEntitiesInstances(Collection $collection, SplObjectStorage $entities) 297 | { 298 | $entitiesInstances = []; 299 | 300 | foreach (CollectionIterator::recursive($collection) as $c) { 301 | if ($this->filterable($c) && !$this->getFilters($c)) { 302 | continue; 303 | } 304 | 305 | $entityInstance = $this->getNewEntityByName($c->getName()); 306 | 307 | if ($this->mixable($c)) { 308 | $mixins = $this->getMixins($c); 309 | foreach ($mixins as $mix) { 310 | $entitiesInstances[] = $entityInstance; 311 | } 312 | } 313 | 314 | $entities[$entityInstance] = $c; 315 | $entitiesInstances[] = $entityInstance; 316 | } 317 | 318 | return $entitiesInstances; 319 | } 320 | 321 | protected function postHydrate(SplObjectStorage $entities) 322 | { 323 | $entitiesClone = clone $entities; 324 | 325 | foreach ($entities as $instance) { 326 | foreach ($instance as $field => &$value) { 327 | if ($this->getStyle()->isRemoteIdentifier($field)) { 328 | foreach ($entitiesClone as $sub) { 329 | $this->tryHydration($entities, $sub, $field, $value); 330 | } 331 | } 332 | } 333 | } 334 | } 335 | 336 | /** 337 | * @param \SplObjectStorage $entities 338 | * @param $sub 339 | * @param $field 340 | * @param $value 341 | */ 342 | protected function tryHydration($entities, $sub, $field, &$value) 343 | { 344 | $tableName = $entities[$sub]->getName(); 345 | $primaryName = $this->getStyle()->identifier($tableName); 346 | 347 | if ($tableName === $this->getStyle()->remoteFromIdentifier($field) 348 | && $sub->{$primaryName} === $value 349 | ) { 350 | $value = $sub; 351 | } 352 | } 353 | 354 | protected function getSetterStyle($name) 355 | { 356 | $name = str_replace('_', '', $this->getStyle()->styledProperty($name)); 357 | 358 | return "set{$name}"; 359 | } 360 | 361 | public function getFilters(Collection $collection) 362 | { 363 | return $collection->getExtra('filters'); 364 | } 365 | 366 | public function getMixins(Collection $collection) 367 | { 368 | return $collection->getExtra('mixins'); 369 | } 370 | 371 | public function getType(Collection $collection) 372 | { 373 | return $collection->getExtra('type'); 374 | } 375 | 376 | public function mixable(Collection $collection) 377 | { 378 | return $collection->have('mixins'); 379 | } 380 | 381 | public function typable(Collection $collection) 382 | { 383 | return $collection->have('type'); 384 | } 385 | 386 | public function filterable(Collection $collection) 387 | { 388 | return $collection->have('filters'); 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /tests/Driver/DynamoDb/DriverTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 19 | } 20 | 21 | $uuid4 = $this->getMockForAbstractClass(UuidInterface::class, ['toString']); 22 | $uuid4->expects($this->any())->method('toString')->willReturn(uniqid()); 23 | 24 | $uuid = $this->getMockForAbstractClass(UuidFactoryInterface::class, ['uuid4']); 25 | $uuid->expects($this->any())->method('uuid4')->willReturn($uuid4); 26 | 27 | return new Driver($connection, $uuid); 28 | } 29 | 30 | public function getMockConnectionRetrieveEmptyResult() 31 | { 32 | return $this->createConnection('scan', new \Aws\Result(['Count' => 0])); 33 | } 34 | 35 | public function getMockConnectionRetrieveFilledResult() 36 | { 37 | $result = new \Aws\Result([ 38 | 'Items' => [ 39 | [ 40 | '_id' => ['N' => '1'], 41 | 'name' => ['S' => 'Test'] 42 | ] 43 | ], 44 | 'Count' => 1 45 | ]); 46 | 47 | return $this->createConnection('scan', $result); 48 | } 49 | 50 | public function getMockConnectionInsertOne() 51 | { 52 | $result = new Result([ 53 | 'Attributes' => [ 54 | '_id' => ['N', 1] 55 | ] 56 | ]); 57 | 58 | return $this->createConnection('putItem', $result); 59 | } 60 | 61 | public function getMockConnectionUpdateOne() 62 | { 63 | $result = new Result([]); 64 | 65 | return $this->createConnection('updateItem', $result); 66 | } 67 | 68 | public function getMockConnectionRemoveOne() 69 | { 70 | $result = new Result([]); 71 | 72 | return $this->createConnection('deleteItem', $result); 73 | } 74 | 75 | public function getConnectionInterface() 76 | { 77 | return DynamoDbClient::class; 78 | } 79 | 80 | public function provideGenerateQueryShouldReturnSimpleFindById() 81 | { 82 | return [ 83 | 'simple return' => [ 84 | Collection::my_coll(42), 85 | [ 86 | '_id' => [ 87 | 'AttributeValueList' => [['N' => 42]], 88 | 'ComparisonOperator' => 'EQ' 89 | ] 90 | ] 91 | ] 92 | ]; 93 | } 94 | 95 | public function provideCollectionAndSearchShouldRetrieveEmptyResult() 96 | { 97 | return [ 98 | 'empty result' => ['authors', ['_id' => 1]], 99 | ]; 100 | } 101 | 102 | public function provideCollectionAndSearchShouldRetrieveFilledResult() 103 | { 104 | return [ 105 | 'simple result' => [ 106 | 'authors', 107 | ['_id' => 1], 108 | new \ArrayIterator([ 109 | [ 110 | '_id' => 1, 111 | 'name' => 'Test' 112 | ] 113 | ]) 114 | ], 115 | ]; 116 | } 117 | 118 | public function provideGenerateQueryShouldUsePartialResultSets() 119 | { 120 | return [ 121 | 'simple' => [ 122 | Collection::article()->author[42], 123 | [ 124 | '_id' => [ 125 | 'AttributeValueList' => [['N' => 42]], 126 | 'ComparisonOperator' => 'EQ' 127 | ] 128 | ] 129 | ] 130 | ]; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/Driver/DynamoDb/StyleTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('_id', (new Style())->identifier('id')); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/Driver/MongoDb/DriverTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('missing mongo extension'); 17 | } 18 | 19 | $driverWrapper = Driver::factoryLegacy('database'); 20 | $this->assertInstanceOf(MongoDriver::class, $driverWrapper->getConnection()); 21 | } 22 | 23 | public function testDriverShouldAnInstanceOfMongoDbDriver() 24 | { 25 | if (!extension_loaded('mongodb')) { 26 | $this->markTestSkipped('missing mongo extension'); 27 | } 28 | 29 | $driverWrapper = Driver::factory('database'); 30 | $this->assertInstanceOf(MongoDbDriver::class, $driverWrapper->getConnection()); 31 | } 32 | 33 | /** 34 | * @param string $method 35 | * @param array $arguments 36 | * @dataProvider provideActionMethodsAndRespectiveArguments 37 | */ 38 | public function testDriverShouldCallMethodFromConnection($method, $arguments) 39 | { 40 | $mockDriver = $this->getMockForAbstractClass(\Respect\Structural\Driver::class); 41 | $mockDriver->expects($this->once())->method($method); 42 | 43 | $driver = $this 44 | ->getMockBuilder(Driver::class) 45 | ->disableOriginalConstructor() 46 | ->setMethods(['getConnection']) 47 | ->getMock(); 48 | $driver->expects($this->once())->method('getConnection')->willReturn($mockDriver); 49 | call_user_func_array([$driver, $method], $arguments); 50 | } 51 | 52 | public function provideActionMethodsAndRespectiveArguments() 53 | { 54 | return [ 55 | 'insert' => ['insert', ['collection', []]], 56 | 'update' => ['update', ['collection', [], []]], 57 | 'remove' => ['remove', ['collection', []]], 58 | 'fetch' => ['fetch', [new \IteratorIterator(new \ArrayObject())]], 59 | 'find' => ['find', ['collection', []]], 60 | 'generateQuery' => ['generateQuery', [Collection::coll()]], 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Driver/MongoDb/MongoDbDriverTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('missing mongo or mongodb extension'); 19 | } 20 | 21 | if (extension_loaded('mongodb') && !class_exists('\MongoDB\Client')) { 22 | $this->markTestSkipped('missing mongodb library'); 23 | } 24 | 25 | parent::setUp(); 26 | } 27 | 28 | public function createDriver($connection = null) 29 | { 30 | if (is_null($connection)) { 31 | $connection = $this->createConnection(); 32 | } 33 | 34 | return new MongoDbDriver($connection, 'database'); 35 | } 36 | 37 | public function getConnectionInterface() 38 | { 39 | return Client::class; 40 | } 41 | 42 | public function getMockConnectionRetrieveEmptyResult() 43 | { 44 | $collection = $this->getMockBuilder(\MongoDB\Collection::class) 45 | ->disableOriginalConstructor() 46 | ->setMethods(['find']) 47 | ->getMock(); 48 | $collection->expects($this->once())->method('find')->willReturn(new \ArrayIterator()); 49 | 50 | $database = $this->getMockBuilder(Database::class) 51 | ->disableOriginalConstructor() 52 | ->setMethods(['selectCollection']) 53 | ->getMock(); 54 | $database->expects($this->once())->method('selectCollection')->willReturn($collection); 55 | 56 | return $this->createConnection('selectDatabase', $database); 57 | } 58 | 59 | public function getMockConnectionRetrieveFilledResult() 60 | { 61 | $result = new \ArrayIterator([ 62 | [ 63 | '_id' => 1, 64 | 'name' => 'Test', 65 | ], 66 | ]); 67 | $collection = $this->getMockBuilder(\MongoDB\Collection::class) 68 | ->disableOriginalConstructor() 69 | ->setMethods(['find']) 70 | ->getMock(); 71 | $collection->expects($this->once())->method('find')->willReturn($result); 72 | 73 | $database = $this->getMockBuilder(Database::class) 74 | ->disableOriginalConstructor() 75 | ->setMethods(['selectCollection']) 76 | ->getMock(); 77 | $database->expects($this->once())->method('selectCollection')->willReturn($collection); 78 | 79 | return $this->createConnection('selectDatabase', $database); 80 | } 81 | 82 | public function getMockConnectionInsertOne() 83 | { 84 | $insertResult = $this->getMockBuilder(InsertOneResult::class) 85 | ->disableOriginalConstructor() 86 | ->setMethods(['getInsertedId']) 87 | ->getMock(); 88 | $insertResult->expects($this->once())->method('getInsertedId')->willReturn(new ObjectID('56d6fb233f90a8231f0041a9')); 89 | 90 | $collection = $this->getMockBuilder(\MongoDB\Collection::class) 91 | ->disableOriginalConstructor() 92 | ->setMethods(['insertOne']) 93 | ->getMock(); 94 | $collection->expects($this->once())->method('insertOne')->willReturn($insertResult); 95 | 96 | $database = $this->getMockBuilder(Database::class) 97 | ->disableOriginalConstructor() 98 | ->setMethods(['selectCollection']) 99 | ->getMock(); 100 | $database->expects($this->once())->method('selectCollection')->willReturn($collection); 101 | 102 | return $this->createConnection('selectDatabase', $database); 103 | } 104 | 105 | public function getMockConnectionUpdateOne() 106 | { 107 | $collection = $this->getMockBuilder(\MongoDB\Collection::class) 108 | ->disableOriginalConstructor() 109 | ->setMethods(['updateOne']) 110 | ->getMock(); 111 | $collection->expects($this->once())->method('updateOne')->willReturn(null); 112 | 113 | $database = $this->getMockBuilder(Database::class) 114 | ->disableOriginalConstructor() 115 | ->setMethods(['selectCollection']) 116 | ->getMock(); 117 | $database->expects($this->once())->method('selectCollection')->willReturn($collection); 118 | 119 | return $this->createConnection('selectDatabase', $database); 120 | } 121 | 122 | public function getMockConnectionRemoveOne() 123 | { 124 | $collection = $this->getMockBuilder(\MongoDB\Collection::class) 125 | ->disableOriginalConstructor() 126 | ->setMethods(['deleteOne']) 127 | ->getMock(); 128 | $collection->expects($this->once())->method('deleteOne')->willReturn(null); 129 | 130 | $database = $this->getMockBuilder(Database::class) 131 | ->disableOriginalConstructor() 132 | ->setMethods(['selectCollection']) 133 | ->getMock(); 134 | $database->expects($this->once())->method('selectCollection')->willReturn($collection); 135 | 136 | return $this->createConnection('selectDatabase', $database); 137 | } 138 | 139 | public function provideGenerateQueryShouldReturnSimpleFindById() 140 | { 141 | return [ 142 | 'simple return' => [ 143 | Collection::my_coll('56d6fb233f90a8231f0041a9'), 144 | [ 145 | '_id' => '56d6fb233f90a8231f0041a9' 146 | ] 147 | ] 148 | ]; 149 | } 150 | 151 | public function provideCollectionAndSearchShouldRetrieveEmptyResult() 152 | { 153 | return [ 154 | ['collection', ['_id' => 1]] 155 | ]; 156 | } 157 | 158 | public function provideGenerateQueryShouldUsePartialResultSets() 159 | { 160 | return [ 161 | 'simple' => [ 162 | Collection::article()->author['56d6fb233f90a8231f0041a9'], 163 | [ 164 | 'author._id' => new ObjectID('56d6fb233f90a8231f0041a9'), 165 | ] 166 | ] 167 | ]; 168 | } 169 | 170 | public function provideCollectionAndSearchShouldRetrieveFilledResult() 171 | { 172 | return [ 173 | 'simple result' => [ 174 | 'authors', 175 | ['_id' => 1], 176 | new \ArrayIterator([ 177 | [ 178 | '_id' => 1, 179 | 'name' => 'Test' 180 | ] 181 | ]) 182 | ], 183 | ]; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /tests/Driver/MongoDb/MongoDriverTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('missing mongo extension'); 15 | } 16 | 17 | parent::setUp(); 18 | } 19 | 20 | public function createDriver($connection = null) 21 | { 22 | if (is_null($connection)) { 23 | $connection = $this->createConnection(); 24 | } 25 | 26 | return new MongoDriver($connection, 'database'); 27 | } 28 | 29 | public function getConnectionInterface() 30 | { 31 | return \MongoClient::class; 32 | } 33 | 34 | public function getMockConnectionRetrieveEmptyResult() 35 | { 36 | $collection = $this->getMockBuilder(\MongoCollection::class) 37 | ->disableOriginalConstructor() 38 | ->setMethods(['find']) 39 | ->getMock(); 40 | $collection->expects($this->once())->method('find')->willReturn(new \ArrayIterator()); 41 | 42 | $database = $this->getMockBuilder(\MongoDB::class) 43 | ->disableOriginalConstructor() 44 | ->setMethods(['selectCollection']) 45 | ->getMock(); 46 | $database->expects($this->once())->method('selectCollection')->willReturn($collection); 47 | 48 | return $this->createConnection('selectDB', $database); 49 | } 50 | 51 | public function getMockConnectionRetrieveFilledResult() 52 | { 53 | $result = new \ArrayIterator([ 54 | [ 55 | '_id' => 1, 56 | 'name' => 'Test', 57 | ], 58 | ]); 59 | $collection = $this->getMockBuilder(\MongoCollection::class) 60 | ->disableOriginalConstructor() 61 | ->setMethods(['find']) 62 | ->getMock(); 63 | $collection->expects($this->once())->method('find')->willReturn($result); 64 | 65 | $database = $this->getMockBuilder(\MongoDB::class) 66 | ->disableOriginalConstructor() 67 | ->setMethods(['selectCollection']) 68 | ->getMock(); 69 | $database->expects($this->once())->method('selectCollection')->willReturn($collection); 70 | 71 | return $this->createConnection('selectDB', $database); 72 | } 73 | 74 | public function getMockConnectionInsertOne() 75 | { 76 | $collection = $this->getMockBuilder(\MongoCollection::class) 77 | ->disableOriginalConstructor() 78 | ->setMethods(['insert']) 79 | ->getMock(); 80 | $collection->expects($this->once())->method('insert')->willReturnCallback(function ($document) { 81 | $document->_id = new \MongoId('56d6fb233f90a8231f0041a9'); 82 | }); 83 | 84 | $database = $this->getMockBuilder(\MongoDB::class) 85 | ->disableOriginalConstructor() 86 | ->setMethods(['selectCollection']) 87 | ->getMock(); 88 | $database->expects($this->once())->method('selectCollection')->willReturn($collection); 89 | 90 | return $this->createConnection('selectDB', $database); 91 | } 92 | 93 | public function getMockConnectionUpdateOne() 94 | { 95 | $collection = $this->getMockBuilder(\MongoCollection::class) 96 | ->disableOriginalConstructor() 97 | ->setMethods(['update']) 98 | ->getMock(); 99 | $collection->expects($this->once())->method('update')->willReturn(null); 100 | 101 | $database = $this->getMockBuilder(\MongoDB::class) 102 | ->disableOriginalConstructor() 103 | ->setMethods(['selectCollection']) 104 | ->getMock(); 105 | $database->expects($this->once())->method('selectCollection')->willReturn($collection); 106 | 107 | return $this->createConnection('selectDB', $database); 108 | } 109 | 110 | public function getMockConnectionRemoveOne() 111 | { 112 | $collection = $this->getMockBuilder(\MongoCollection::class) 113 | ->disableOriginalConstructor() 114 | ->setMethods(['remove']) 115 | ->getMock(); 116 | $collection->expects($this->once())->method('remove')->willReturn(null); 117 | 118 | $database = $this->getMockBuilder(\MongoDB::class) 119 | ->disableOriginalConstructor() 120 | ->setMethods(['selectCollection']) 121 | ->getMock(); 122 | $database->expects($this->once())->method('selectCollection')->willReturn($collection); 123 | 124 | return $this->createConnection('selectDB', $database); 125 | } 126 | 127 | public function provideGenerateQueryShouldReturnSimpleFindById() 128 | { 129 | return [ 130 | 'simple return' => [ 131 | Collection::my_coll('56d6fb233f90a8231f0041a9'), 132 | [ 133 | '_id' => '56d6fb233f90a8231f0041a9' 134 | ] 135 | ] 136 | ]; 137 | } 138 | 139 | public function provideCollectionAndSearchShouldRetrieveEmptyResult() 140 | { 141 | return [ 142 | ['collection', ['_id' => 1]] 143 | ]; 144 | } 145 | 146 | public function provideGenerateQueryShouldUsePartialResultSets() 147 | { 148 | return [ 149 | 'simple' => [ 150 | Collection::article()->author['56d6fb233f90a8231f0041a9'], 151 | [ 152 | 'author._id' => new \MongoId('56d6fb233f90a8231f0041a9'), 153 | ] 154 | ] 155 | ]; 156 | } 157 | 158 | public function provideCollectionAndSearchShouldRetrieveFilledResult() 159 | { 160 | return [ 161 | 'simple result' => [ 162 | 'authors', 163 | ['_id' => 1], 164 | new \ArrayIterator([ 165 | [ 166 | '_id' => 1, 167 | 'name' => 'Test' 168 | ] 169 | ]) 170 | ], 171 | ]; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /tests/Driver/MongoDb/StyleTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('_id', $style->identifier('id')); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/Driver/TestCase.php: -------------------------------------------------------------------------------- 1 | driver = $this->createDriver(); 20 | } 21 | 22 | /** 23 | * @param object $connection 24 | * 25 | * @return Driver 26 | */ 27 | abstract public function createDriver($connection = null); 28 | 29 | /** 30 | * @return string 31 | */ 32 | abstract public function getConnectionInterface(); 33 | 34 | /** 35 | * @return \PHPUnit_Framework_MockObject_MockObject 36 | */ 37 | abstract public function getMockConnectionRetrieveEmptyResult(); 38 | 39 | /** 40 | * @return \PHPUnit_Framework_MockObject_MockObject 41 | */ 42 | abstract public function getMockConnectionRetrieveFilledResult(); 43 | 44 | /** 45 | * @return \PHPUnit_Framework_MockObject_MockObject 46 | */ 47 | abstract public function getMockConnectionInsertOne(); 48 | 49 | /** 50 | * @return \PHPUnit_Framework_MockObject_MockObject 51 | */ 52 | abstract public function getMockConnectionUpdateOne(); 53 | 54 | /** 55 | * @return \PHPUnit_Framework_MockObject_MockObject 56 | */ 57 | abstract public function getMockConnectionRemoveOne(); 58 | 59 | /** 60 | * @return array 61 | */ 62 | abstract public function provideGenerateQueryShouldReturnSimpleFindById(); 63 | 64 | /** 65 | * @return array 66 | */ 67 | abstract public function provideCollectionAndSearchShouldRetrieveEmptyResult(); 68 | 69 | /** 70 | * @return array 71 | */ 72 | abstract public function provideGenerateQueryShouldUsePartialResultSets(); 73 | 74 | /** 75 | * @return array 76 | */ 77 | abstract public function provideCollectionAndSearchShouldRetrieveFilledResult(); 78 | 79 | /** 80 | * @param string $method 81 | * @param mixed $result 82 | * 83 | * @return \PHPUnit_Framework_MockObject_MockObject 84 | */ 85 | protected function createConnection($method = null, $result = null) 86 | { 87 | $methods = []; 88 | 89 | if ($method) { 90 | $methods[] = $method; 91 | } 92 | 93 | $client = $this 94 | ->getMockBuilder($this->getConnectionInterface()) 95 | ->disableOriginalConstructor() 96 | ->setMethods($methods) 97 | ->getMock(); 98 | 99 | if ($method && $result) { 100 | $client 101 | ->expects($this->once()) 102 | ->method($method) 103 | ->will($this->returnValue($result)); 104 | } 105 | 106 | return $client; 107 | } 108 | 109 | public function testDriverShouldAnInstanceOfDriverInterface() 110 | { 111 | $this->assertInstanceOf(Driver::class, $this->driver); 112 | } 113 | 114 | public function testRetrieveConnection() 115 | { 116 | $this->assertInstanceOf($this->getConnectionInterface(), $this->driver->getConnection()); 117 | } 118 | 119 | public function testShouldRetrieveCurrentCursorValueAndNext() 120 | { 121 | $iterator = new \ArrayIterator(['a', 'b']); 122 | 123 | $this->assertEquals('a', $this->driver->fetch($iterator)); 124 | $this->assertEquals('b', $iterator->current()); 125 | } 126 | 127 | /** 128 | * @dataProvider provideCollectionAndSearchShouldRetrieveEmptyResult 129 | * 130 | * @param string $collection 131 | * @param array $search 132 | */ 133 | public function testFindRetrieveEmptyResult($collection, $search) 134 | { 135 | $driver = $this->createDriver($this->getMockConnectionRetrieveEmptyResult()); 136 | 137 | $this->assertEmpty($driver->find($collection, $search)); 138 | } 139 | 140 | /** 141 | * @dataProvider provideCollectionAndSearchShouldRetrieveFilledResult 142 | * 143 | * @param string $collection 144 | * @param array $search 145 | */ 146 | public function testFindRetrieveFilledResult($collection, $search, $expected) 147 | { 148 | $driver = $this->createDriver($this->getMockConnectionRetrieveFilledResult()); 149 | 150 | $result = $driver->find($collection, $search); 151 | 152 | $this->assertInstanceOf(\Iterator::class, $result); 153 | $this->assertCount(1, $result); 154 | $this->assertEquals($expected, $result); 155 | } 156 | 157 | public function testGenerateQueryShouldReturnSimpleFind() 158 | { 159 | $result = $this->driver->generateQuery(Collection::my_coll()); 160 | $this->assertEquals([], $result); 161 | } 162 | 163 | /** 164 | * @dataProvider provideGenerateQueryShouldReturnSimpleFindById 165 | * 166 | * @param Collection $collection 167 | * @param array $expectedResult 168 | */ 169 | public function testGenerateQueryShouldReturnSimpleFindById(Collection $collection, array $expectedResult) 170 | { 171 | $result = $this->driver->generateQuery($collection); 172 | 173 | $this->assertEquals($expectedResult, $result); 174 | } 175 | 176 | /** 177 | * @dataProvider provideGenerateQueryShouldUsePartialResultSets 178 | * 179 | * @param Collection $mappedCollection 180 | * @param array $expectedResult 181 | */ 182 | public function testGenerateQueryShouldUsePartialResultSets(Collection $mappedCollection, array $expectedResult) 183 | { 184 | $result = $this->driver->generateQuery($mappedCollection); 185 | $this->assertEquals($expectedResult, $result); 186 | } 187 | 188 | public function testInsertDataShouldRetrieveId() 189 | { 190 | $data = new \stdClass(); 191 | $data->name = 'Test'; 192 | 193 | $this->createDriver($this->getMockConnectionInsertOne())->insert('author', $data); 194 | 195 | $this->assertObjectHasAttribute('_id', $data); 196 | } 197 | 198 | public function testUpdateDataShouldWithSuccess() 199 | { 200 | $data = new \stdClass(); 201 | $data->name = 'Test'; 202 | 203 | $this->createDriver($this->getMockConnectionUpdateOne())->update('author', ['name' => 'Test'], $data); 204 | } 205 | 206 | public function testRemoveDataShouldWithSuccess() 207 | { 208 | $this->createDriver($this->getMockConnectionRemoveOne())->remove('author', ['name' => 'Test']); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /tests/MapperTest.php: -------------------------------------------------------------------------------- 1 | getMockForAbstractClass(Driver::class); 13 | $driver->expects($this->once())->method('insert')->willReturnCallback(function ($collection, $document) { 14 | $document->id = 1; 15 | }); 16 | 17 | $mapper = new Mapper($driver); 18 | 19 | $author = new \stdClass(); 20 | $author->name = 'Respect'; 21 | $mapper->authors->persist($author); 22 | $mapper->flush(); 23 | 24 | return $author; 25 | } 26 | 27 | /** 28 | * @depends testInsertANewDocument 29 | */ 30 | public function testUpdateADocument($author) 31 | { 32 | $driver = $this->getMockForAbstractClass(Driver::class); 33 | $driver->expects($this->once())->method('update'); 34 | $driver->expects($this->once())->method('fetch')->willReturnCallback(function (\Iterator $statement) { 35 | return $statement->current(); 36 | }); 37 | $driver->expects($this->once())->method('find')->willReturn((new \ArrayObject([$author]))->getIterator()); 38 | $driver->expects($this->once())->method('generateQuery')->willReturn(['id' => 1]); 39 | 40 | $mapper = new Mapper($driver); 41 | 42 | $author = $mapper->authors[1]->fetch(); 43 | $author->name = 'Respect Structural'; 44 | $mapper->authors->persist($author); 45 | $mapper->flush(); 46 | 47 | return $author; 48 | } 49 | 50 | /** 51 | * @param $author 52 | * @depends testUpdateADocument 53 | */ 54 | public function testRemoveADocument($author) 55 | { 56 | $driver = $this->getMockForAbstractClass(Driver::class); 57 | $driver->expects($this->once())->method('remove'); 58 | $driver->expects($this->once())->method('fetch')->willReturnCallback(function (\Iterator $statement) { 59 | return $statement->current(); 60 | }); 61 | $driver->expects($this->once())->method('find')->willReturn((new \ArrayObject([$author]))->getIterator()); 62 | $driver->expects($this->once())->method('generateQuery')->willReturn(['id' => 1]); 63 | 64 | $mapper = new Mapper($driver); 65 | 66 | $author = $mapper->authors[1]->fetch(); 67 | $mapper->authors->remove($author); 68 | $mapper->flush(); 69 | } 70 | } 71 | --------------------------------------------------------------------------------