diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml new file mode 100644 index 00000000..1baaf6ca --- /dev/null +++ b/.github/workflows/coding-standards.yml @@ -0,0 +1,66 @@ +name: "Coding Standards" + +on: + pull_request: + branches: + - "*.x" + push: + branches: + - "*.x" + schedule: + # Run workflow on every Sunday + - cron: '25 5 * * 0' + +jobs: + coding-standards: + name: "Coding Standards" + runs-on: "ubuntu-20.04" + + strategy: + matrix: + php-version: + - "7.2" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php-version }} + extensions: "mongodb" + key: "extcache-v1" + + - name: Cache extensions + uses: actions/cache@v2 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + extensions: "mongodb" + php-version: "${{ matrix.php-version }}" + tools: "cs2pr" + + - name: "Show driver information" + run: "php --ri mongodb" + + - name: "Cache dependencies installed with Composer" + uses: "actions/cache@v2" + with: + path: "~/.composer/cache" + key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.json') }}" + restore-keys: "php-${{ matrix.php-version }}-composer-normal-" + + - name: "Install dependencies with Composer" + run: "composer install --no-interaction --no-progress --no-suggest" + + # The -q option is required until phpcs v4 is released + - name: "Run PHP_CodeSniffer" + run: "vendor/bin/phpcs -q --no-colors --report=checkstyle | cs2pr" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..cc9ef9f3 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,171 @@ +name: "Tests" + +on: + pull_request: + branches: + - "*.x" + push: + branches: + - "*.x" + schedule: + # Run workflow on every Sunday + - cron: '25 5 * * 0' + +jobs: + verification: + name: "Verification tests" + runs-on: "ubuntu-20.04" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + with: + fetch-depth: 2 + + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: "5.6" + extensions: "mongodb-1.7.5, mongo-1.6.14" + key: "extcache-v1" + + - name: Cache extensions + uses: actions/cache@v2 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "5.6" + tools: "pecl" + extensions: "mongodb-1.7.5, mongo-1.6.14" + coverage: "none" + ini-values: "zend.assertions=1" + + - name: "Show legacy driver information" + run: "php --ri mongo" + + - name: "Show driver information" + run: "php --ri mongodb" + + - name: "Cache dependencies installed with composer" + uses: "actions/cache@v2" + with: + path: "~/.composer/cache" + key: "php-5.6-composer-locked-${{ hashFiles('composer.json') }}" + restore-keys: "php-5.6-composer-normal-" + + - name: "Install dependencies with composer" + run: "composer update --no-interaction --no-progress" + + - id: setup-mongodb + uses: mongodb-labs/drivers-evergreen-tools@master + with: + version: "3.0" + + - name: "Run PHPUnit" + run: "vendor/bin/simple-phpunit -v" + env: + SYMFONY_DEPRECATIONS_HELPER: 999999 + MONGODB_URI: ${{ steps.setup-mongodb.outputs.cluster-uri }} + + phpunit: + name: "PHPUnit tests" + runs-on: "${{ matrix.os }}" + + strategy: + fail-fast: true + matrix: + os: + - "ubuntu-20.04" + php-version: + - "7.2" + - "7.3" + - "7.4" + - "8.0" + - "8.1" + mongodb-version: + - "4.4" + driver-version: + - "stable" + deps: + - "normal" + include: + - deps: "low" + os: "ubuntu-20.04" + php-version: "5.6" + mongodb-version: "3.0" + driver-version: "1.2.0" + - deps: "normal" + os: "ubuntu-20.04" + php-version: "7.0" + mongodb-version: "4.4" + driver-version: "1.9.2" + - deps: "normal" + os: "ubuntu-20.04" + php-version: "7.1" + mongodb-version: "4.4" + driver-version: "1.11.1" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + with: + fetch-depth: 2 + + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php-version }} + extensions: "mongodb-${{ matrix.driver-version }}" + key: "extcache-v1" + + - name: Cache extensions + uses: actions/cache@v2 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "${{ matrix.php-version }}" + tools: "pecl" + extensions: "mongodb-${{ matrix.driver-version }}" + coverage: "none" + ini-values: "zend.assertions=1" + + - name: "Show driver information" + run: "php --ri mongodb" + + - name: "Cache dependencies installed with composer" + uses: "actions/cache@v2" + with: + path: "~/.composer/cache" + key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.json') }}" + restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.deps }}-" + + - name: "Install dependencies with composer" + run: "composer update --no-interaction --no-progress" + if: "${{ matrix.deps == 'normal' }}" + + - name: "Install lowest possible dependencies with composer" + run: "composer update --no-interaction --no-progress --prefer-dist --prefer-lowest" + if: "${{ matrix.deps == 'low' }}" + + - id: setup-mongodb + uses: mongodb-labs/drivers-evergreen-tools@master + with: + version: ${{ matrix.mongodb-version }} + + - name: "Run PHPUnit" + run: "vendor/bin/simple-phpunit -v" + env: + SYMFONY_DEPRECATIONS_HELPER: 999999 + MONGODB_URI: ${{ steps.setup-mongodb.outputs.cluster-uri }} diff --git a/.gitignore b/.gitignore index 18389124..772a9a08 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ composer.lock +.phpcs-cache +.phpunit.result.cache vendor/ tests/scripts/ diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 47a7f6dd..00000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,6 +0,0 @@ -imports: - - php - -tools: - external_code_coverage: - timeout: 1200 # Timeout in seconds. \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5123ec71..00000000 --- a/.travis.yml +++ /dev/null @@ -1,52 +0,0 @@ -dist: trusty -sudo: false -language: php - -services: - - mongodb - -php: - - 7.0 - - 7.1 - - 7.2 - -env: - global: - - DRIVER_VERSION="stable" - -addons: - apt: - sources: - - sourceline: "deb [arch=amd64] https://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.4 multiverse" - key_url: "https://www.mongodb.org/static/pgp/server-3.4.asc" - - "mongodb-upstart" - packages: ['mongodb-org-server'] - -matrix: - fast_finish: true - include: - # Test against legacy driver to ensure validity of the test suite - - php: 5.6 - env: DRIVER_VERSION=stable LEGACY_DRIVER_VERSION=stable - # Test against set of lowest dependencies - - php: 5.6 - env: DRIVER_VERSION="1.2.0" COMPOSER_FLAGS="--prefer-dist --prefer-lowest" - addons: - apt: - sources: - - sourceline: "deb [arch=amd64] https://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.0 multiverse" - key_url: "https://www.mongodb.org/static/pgp/server-3.0.asc" - - "mongodb-upstart" - packages: ['mongodb-org-server'] - -before_install: - - pecl install -f mongodb-${DRIVER_VERSION} - - composer update ${COMPOSER_FLAGS} - - if [ "x$LEGACY_DRIVER_VERSION" != "x" ]; then yes '' | pecl -q install -f mongo-${LEGACY_DRIVER_VERSION}; fi - -script: - - ./vendor/bin/phpunit --coverage-clover=coverage.clover - -after_script: - - wget https://scrutinizer-ci.com/ocular.phar - - php ocular.phar code-coverage:upload --format=php-clover coverage.clover diff --git a/CHANGELOG-1.1.md b/CHANGELOG-1.1.md index 1aeae60f..f4b9e064 100644 --- a/CHANGELOG-1.1.md +++ b/CHANGELOG-1.1.md @@ -3,6 +3,98 @@ CHANGELOG for 1.1.x This changelog references the relevant changes done in minor version updates. +1.1.11 (2019-11-11) +------------------- + +All issues and pull requests under this release may be found under the +[1.1.11](https://github.com/alcaeus/mongo-php-adapter/issues?q=milestone%3A1.1.11) +milestone. + + * [#263](https://github.com/alcaeus/mongo-php-adapter/pull/263) fixes test + failures on PHP 7.4. + * [#262](https://github.com/alcaeus/mongo-php-adapter/pull/262) fixes a memory + leak due to generators that aren't freed. + +1.1.10 (2019-11-06) +------------------- + +All issues and pull requests under this release may be found under the +[1.1.10](https://github.com/alcaeus/mongo-php-adapter/issues?q=milestone%3A1.1.10) +milestone. + + * [#261](https://github.com/alcaeus/mongo-php-adapter/pull/261) fixes missing + interface implementations in cursor classes. + * [#260](https://github.com/alcaeus/mongo-php-adapter/pull/260) fixes issues + when running against MongoDB 4.2 or `ext-mongodb` 1.6. + * [#259](https://github.com/alcaeus/mongo-php-adapter/pull/259) fixes issues on + PHP 7.3 due to `MongoCursor` not implementing `Countable`. + +1.1.9 (2019-08-07) +------------------ + +All issues and pull requests under this release may be found under the +[1.1.9](https://github.com/alcaeus/mongo-php-adapter/issues?q=milestone%3A1.1.9) +milestone. + + * [#255](https://github.com/alcaeus/mongo-php-adapter/pull/255) fixes inserting + documents with identifiers PHP considers empty. + +1.1.8 (2019-07-14) +------------------ + +All issues and pull requests under this release may be found under the +[1.1.8](https://github.com/alcaeus/mongo-php-adapter/issues?q=milestone%3A1.1.8) +milestone. + + * [#253](https://github.com/alcaeus/mongo-php-adapter/pull/253) fixes wrong + handling of `ArrayObject` instances in `MongoCollection::insert`. + +1.1.7 (2019-04-06) +------------------ + +All issues and pull requests under this release may be found under the +[1.1.7](https://github.com/alcaeus/mongo-php-adapter/issues?q=milestone%3A1.1.7) +milestone. + + * [#250](https://github.com/alcaeus/mongo-php-adapter/pull/250) fixes type + conversion when passing write concern to `MongoClient` via URL arguments. + + +1.1.6 (2019-02-08) +------------------ + +All issues and pull requests under this release may be found under the +[1.1.6](https://github.com/alcaeus/mongo-php-adapter/issues?q=milestone%3A1.1.6) +milestone. + + * [#244](https://github.com/alcaeus/mongo-php-adapter/pull/244) fixes a null + access when converting exceptions. + * [#236](https://github.com/alcaeus/mongo-php-adapter/pull/236) allows using + `0` as key in documents. + * [#234](https://github.com/alcaeus/mongo-php-adapter/pull/234) removes an + invalid attribute from phpunit.xml. + + +1.1.5 (2018-03-05) +----------------- + +All issues and pull requests under this release may be found under the +[1.1.5](https://github.com/alcaeus/mongo-php-adapter/issues?q=milestone%3A1.1.5) +milestone. + + * [#222](https://github.com/alcaeus/mongo-php-adapter/pull/222) fixes handling + of `monodb+srv` URLs in `MongoClient`. + +1.1.4 (2018-01-24) +------------------ + +All issues and pull requests under this release may be found under the +[1.1.4](https://github.com/alcaeus/mongo-php-adapter/issues?q=milestone%3A1.1.4) +milestone. + + * [#214](https://github.com/alcaeus/mongo-php-adapter/pull/214) fixes the +return values of MongoBatch calls with unacknowledged write concerns. + 1.1.3 (2017-09-24) ------------------ diff --git a/README.md b/README.md index 01df1f41..1cab6b7a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ compatible with PHP 7. This library aims to provide a compatibility layer for applications that rely on libraries using ext-mongo, e.g. [Doctrine MongoDB ODM](https://github.com/doctrine/mongodb-odm), but want to -migrate to PHP 7 or HHVM on which `ext-mongo` will not run. +migrate to PHP 7 on which `ext-mongo` will not run. You should not be using this library if you do not rely on a library using `ext-mongo`. If you are starting a new project, please check out diff --git a/composer.json b/composer.json index aa2ce17a..dcc3041a 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "alcaeus/mongo-php-adapter", "type": "library", - "description": "Adapter to provide ext-mongo interface on top of mongo-php-libary", + "description": "Adapter to provide ext-mongo interface on top of mongo-php-library", "keywords": ["mongodb", "database"], "license": "MIT", "authors": [ @@ -9,13 +9,15 @@ { "name": "Olivier Lechevalier", "email": "olivier.lechevalier@gmail.com" } ], "require": { - "php": "^5.6 || ^7.0", + "php": "^5.6 || ^7.0 || ^8.0", + "ext-ctype": "*", "ext-hash": "*", "ext-mongodb": "^1.2.0", "mongodb/mongodb": "^1.0.1" }, "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.0" + "symfony/phpunit-bridge": "^4.4.16 || ^5.2", + "squizlabs/php_codesniffer": "^3.2" }, "provide": { "ext-mongo": "1.6.14" @@ -33,8 +35,6 @@ "psr-4": { "Alcaeus\\MongoDbAdapter\\Tests\\": "tests/Alcaeus/MongoDbAdapter" } }, "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } + "branch-version": "1.x" } } diff --git a/lib/Alcaeus/MongoDbAdapter/AbstractCursor.php b/lib/Alcaeus/MongoDbAdapter/AbstractCursor.php index 6e3c8758..f1d76ebb 100644 --- a/lib/Alcaeus/MongoDbAdapter/AbstractCursor.php +++ b/lib/Alcaeus/MongoDbAdapter/AbstractCursor.php @@ -18,6 +18,7 @@ use Alcaeus\MongoDbAdapter\Helper\ReadPreference; use MongoDB\Collection; use MongoDB\Driver\Cursor; +use ReturnTypeWillChange; /** * @internal @@ -52,7 +53,7 @@ abstract class AbstractCursor protected $db; /** - * @var \Iterator + * @var CursorIterator */ protected $iterator; @@ -136,6 +137,7 @@ public function __construct(\MongoClient $connection, $ns) * @link http://www.php.net/manual/en/mongocursor.current.php * @return array */ + #[ReturnTypeWillChange] public function current() { return $this->current; @@ -146,6 +148,7 @@ public function current() * @link http://www.php.net/manual/en/mongocursor.key.php * @return string The current result's _id as a string. */ + #[ReturnTypeWillChange] public function key() { return $this->key; @@ -158,6 +161,7 @@ public function key() * @throws \MongoCursorTimeoutException * @return array Returns the next object */ + #[ReturnTypeWillChange] public function next() { if (! $this->startedIterating) { @@ -181,6 +185,7 @@ public function next() * @throws \MongoCursorTimeoutException * @return void */ + #[ReturnTypeWillChange] public function rewind() { // We can recreate the cursor to allow it to be rewound @@ -196,6 +201,7 @@ public function rewind() * @link http://www.php.net/manual/en/mongocursor.valid.php * @return boolean If the current result is not null. */ + #[ReturnTypeWillChange] public function valid() { return $this->valid; @@ -292,9 +298,8 @@ protected function getOptions($optionNames = null) protected function ensureIterator() { if ($this->iterator === null) { - // MongoDB\Driver\Cursor needs to be wrapped into a \Generator so that a valid \Iterator with working implementations of - // next, current, valid, key and rewind is returned. These methods don't work if we wrap the Cursor inside an \IteratorIterator $this->iterator = $this->wrapTraversable($this->ensureCursor()); + $this->iterator->rewind(); } return $this->iterator; @@ -302,13 +307,11 @@ protected function ensureIterator() /** * @param \Traversable $traversable - * @return \Generator + * @return CursorIterator */ protected function wrapTraversable(\Traversable $traversable) { - foreach ($traversable as $key => $value) { - yield $key => $value; - } + return new CursorIterator($traversable); } /** diff --git a/lib/Alcaeus/MongoDbAdapter/CursorIterator.php b/lib/Alcaeus/MongoDbAdapter/CursorIterator.php new file mode 100644 index 00000000..31057584 --- /dev/null +++ b/lib/Alcaeus/MongoDbAdapter/CursorIterator.php @@ -0,0 +1,40 @@ +useIdAsKey = $useIdAsKey; + } + + #[ReturnTypeWillChange] + public function key() + { + if (!$this->useIdAsKey) { + return parent::key(); + } + + $current = $this->current(); + + if (!isset($current->_id) || (is_object($current->_id) && !$current->_id instanceof ObjectID)) { + return parent::key(); + } + + return (string) $current->_id; + } +} diff --git a/lib/Alcaeus/MongoDbAdapter/ExceptionConverter.php b/lib/Alcaeus/MongoDbAdapter/ExceptionConverter.php index 8838a698..9bc442ae 100644 --- a/lib/Alcaeus/MongoDbAdapter/ExceptionConverter.php +++ b/lib/Alcaeus/MongoDbAdapter/ExceptionConverter.php @@ -16,6 +16,8 @@ namespace Alcaeus\MongoDbAdapter; use MongoDB\Driver\Exception; +use MongoDB\Driver\WriteError; +use MongoDB\Driver\WriteResult; /** * @internal @@ -30,6 +32,12 @@ class ExceptionConverter */ public static function toLegacy(Exception\Exception $e, $fallbackClass = 'MongoException') { + // Starting with ext-mongodb 1.6.0, errors during bulk write are always wrapped in a BulkWriteException. + // If a BulkWriteException wraps another driver exception, use that instead. + if ($e instanceof Exception\BulkWriteException && $e->getPrevious() instanceof Exception\Exception) { + $e = $e->getPrevious(); + } + $message = $e->getMessage(); $code = $e->getCode(); @@ -44,12 +52,13 @@ public static function toLegacy(Exception\Exception $e, $fallbackClass = 'MongoE case Exception\BulkWriteException::class: case Exception\WriteException::class: $writeResult = $e->getWriteResult(); - - if ($writeResult) { + // attempt to retrieve write error + if ($writeResult instanceof WriteResult && is_array($writeResult->getWriteErrors()) && $writeResult->getWriteErrors() !== []) { $writeError = $writeResult->getWriteErrors()[0]; - - $message = $writeError->getMessage(); - $code = $writeError->getCode(); + if ($writeError instanceof WriteError) { + $message = $writeError->getMessage(); + $code = $writeError->getCode(); + } } switch ($code) { diff --git a/lib/Alcaeus/MongoDbAdapter/TypeConverter.php b/lib/Alcaeus/MongoDbAdapter/TypeConverter.php index ad10ba75..51ad7c6c 100644 --- a/lib/Alcaeus/MongoDbAdapter/TypeConverter.php +++ b/lib/Alcaeus/MongoDbAdapter/TypeConverter.php @@ -43,6 +43,8 @@ public static function fromLegacy($value) return $value->toBSONType(); case $value instanceof BSON\Type: return $value; + case $value instanceof \DateTimeInterface: + return self::fromLegacy((array) $value); case is_array($value): case is_object($value): $result = []; @@ -176,7 +178,7 @@ private static function convertBSONObjectToLegacy(BSON\Type $value) case $value instanceof Model\BSONDocument: case $value instanceof Model\BSONArray: return array_map( - ['self', 'toLegacy'], + [self::class, 'toLegacy'], $value->getArrayCopy() ); default: diff --git a/lib/Alcaeus/MongoDbAdapter/TypeInterface.php b/lib/Alcaeus/MongoDbAdapter/TypeInterface.php index 3f079830..2126e161 100644 --- a/lib/Alcaeus/MongoDbAdapter/TypeInterface.php +++ b/lib/Alcaeus/MongoDbAdapter/TypeInterface.php @@ -15,8 +15,6 @@ namespace Alcaeus\MongoDbAdapter; -use MongoDB\BSON\Type; - interface TypeInterface { /** diff --git a/lib/Mongo/MongoClient.php b/lib/Mongo/MongoClient.php index 0e0fae57..a88f10e6 100644 --- a/lib/Mongo/MongoClient.php +++ b/lib/Mongo/MongoClient.php @@ -32,13 +32,13 @@ class MongoClient use Helper\WriteConcern; const VERSION = '1.6.12'; - const DEFAULT_HOST = "localhost" ; - const DEFAULT_PORT = 27017 ; - const RP_PRIMARY = "primary" ; - const RP_PRIMARY_PREFERRED = "primaryPreferred" ; - const RP_SECONDARY = "secondary" ; - const RP_SECONDARY_PREFERRED = "secondaryPreferred" ; - const RP_NEAREST = "nearest" ; + const DEFAULT_HOST = "localhost"; + const DEFAULT_PORT = 27017; + const RP_PRIMARY = "primary"; + const RP_PRIMARY_PREFERRED = "primaryPreferred"; + const RP_SECONDARY = "secondary"; + const RP_SECONDARY_PREFERRED = "secondaryPreferred"; + const RP_NEAREST = "nearest"; /** * @var bool @@ -93,10 +93,10 @@ public function __construct($server = 'default', array $options = ['connect' => $this->applyConnectionOptions($server, $options); $this->server = $server; - if (false === strpos($this->server, 'mongodb://')) { - $this->server = 'mongodb://'.$this->server; + if (false === strpos($this->server, '://')) { + $this->server = 'mongodb://' . $this->server; } - $this->client = new Client($this->server, $options, $driverOptions); + $this->client = new Client($this->server, $options, $driverOptions + ['driver' => ['name' => 'mongo-php-adapter']]); $info = $this->client->__debugInfo(); $this->manager = $info['manager']; @@ -222,7 +222,7 @@ public function getHosts() $results[$key] = [ 'host' => $server->getHost(), 'port' => $server->getPort(), - 'health' => (int) $info['ok'], + 'health' => 1, 'state' => $state, 'ping' => $server->getLatency(), 'lastPing' => null, @@ -352,7 +352,7 @@ private function notImplemented() /** * @return array */ - function __sleep() + public function __sleep() { return [ 'connected', 'status', 'server', 'persistent' @@ -365,7 +365,12 @@ function __sleep() */ private function extractUrlOptions($server) { - $queryOptions = explode('&', parse_url($server, PHP_URL_QUERY)); + $queryOptions = parse_url($server, PHP_URL_QUERY); + if (!$queryOptions) { + return []; + } + + $queryOptions = explode('&', $queryOptions); $options = []; foreach ($queryOptions as $option) { @@ -376,6 +381,8 @@ private function extractUrlOptions($server) $keyValue = explode('=', $option); if ($keyValue[0] === 'readPreferenceTags') { $options[$keyValue[0]][] = $this->getReadPreferenceTags($keyValue[1]); + } elseif (ctype_digit($keyValue[1])) { + $options[$keyValue[0]] = (int) $keyValue[1]; } else { $options[$keyValue[0]] = $keyValue[1]; } diff --git a/lib/Mongo/MongoCode.php b/lib/Mongo/MongoCode.php index 25d6b5f0..dd792f80 100644 --- a/lib/Mongo/MongoCode.php +++ b/lib/Mongo/MongoCode.php @@ -27,7 +27,7 @@ class MongoCode implements \Alcaeus\MongoDbAdapter\TypeInterface private $code; /** - * @var array + * @var array|null */ private $scope; @@ -65,6 +65,6 @@ public function __toString() */ public function toBSONType() { - return new \MongoDB\BSON\Javascript($this->code, $this->scope); + return new \MongoDB\BSON\Javascript($this->code, !empty($this->scope) ? $this->scope : null); } } diff --git a/lib/Mongo/MongoCollection.php b/lib/Mongo/MongoCollection.php index 64070b70..aa858200 100644 --- a/lib/Mongo/MongoCollection.php +++ b/lib/Mongo/MongoCollection.php @@ -20,6 +20,8 @@ use Alcaeus\MongoDbAdapter\Helper; use Alcaeus\MongoDbAdapter\TypeConverter; use Alcaeus\MongoDbAdapter\ExceptionConverter; +use MongoDB\Driver\Exception\CommandException; +use MongoDB\Model\IndexInput; /** * Represents a database collection. @@ -277,7 +279,7 @@ public function validate($scan_data = false) */ public function insert(&$a, array $options = []) { - if (! $this->ensureDocumentHasMongoId($a)) { + if ($this->ensureDocumentHasMongoId($a) === null) { trigger_error(sprintf('%s(): expects parameter %d to be an array or object, %s given', __METHOD__, 1, gettype($a)), E_USER_WARNING); return; } @@ -596,7 +598,7 @@ public function createIndex($keys, array $options = []) } if (! isset($options['name'])) { - $options['name'] = \MongoDB\generate_index_name($keys); + $options['name'] = $this->generateIndexName($keys); } $indexes = iterator_to_array($this->collection->listIndexes()); @@ -626,7 +628,9 @@ public function createIndex($keys, array $options = []) $this->collection->createIndex($keys, $options); } catch (\MongoDB\Driver\Exception\Exception $e) { - throw ExceptionConverter::toLegacy($e, 'MongoResultException'); + if (! $e instanceof CommandException || strpos($e->getMessage(), 'with a different name') === false) { + throw ExceptionConverter::toLegacy($e, 'MongoResultException'); + } } $result = [ @@ -674,7 +678,7 @@ public function deleteIndex($keys) $indexName .= '_1'; } } elseif (is_array($keys)) { - $indexName = \MongoDB\generate_index_name($keys); + $indexName = $this->generateIndexName($keys); } else { throw new \InvalidArgumentException(); } @@ -888,14 +892,14 @@ public function group($keys, array $initial, $reduce, array $condition = []) $command = [ 'group' => [ 'ns' => $this->name, - '$reduce' => (string)$reduce, + '$reduce' => (string) $reduce, 'initial' => $initial, 'cond' => $condition, ], ]; if ($keys instanceof MongoCode) { - $command['group']['$keyf'] = (string)$keys; + $command['group']['$keyf'] = (string) $keys; } else { $command['group']['key'] = $keys; } @@ -904,7 +908,7 @@ public function group($keys, array $initial, $reduce, array $condition = []) } if (array_key_exists('finalize', $condition)) { if ($condition['finalize'] instanceof MongoCode) { - $condition['finalize'] = (string)$condition['finalize']; + $condition['finalize'] = (string) $condition['finalize']; } $command['group']['finalize'] = $condition['finalize']; } @@ -983,7 +987,7 @@ private function convertWriteConcernOptions(array $options) private function checkKeys(array $array) { foreach ($array as $key => $value) { - if (empty($key) && $key !== 0) { + if (empty($key) && $key !== 0 && $key !== '0') { throw new \MongoException('zero-length keys are not allowed, did you use $ with double quotes?'); } @@ -999,12 +1003,12 @@ private function checkKeys(array $array) */ private function ensureDocumentHasMongoId(&$document) { - if (is_array($document)) { + if (is_array($document) || $document instanceof ArrayObject) { if (! isset($document['_id'])) { $document['_id'] = new \MongoId(); } - $this->checkKeys($document); + $this->checkKeys((array) $document); return $document['_id']; } elseif (is_object($document)) { @@ -1036,6 +1040,22 @@ private function checkCollectionName($name) } } + + /** + * @param array $keys Field or fields to use as index. + * @return string + */ + private function generateIndexName($keys) + { + $name = ''; + + foreach ($keys as $field => $type) { + $name .= ($name !== '' ? '_' : '') . $field . '_' . $type; + } + + return $name; + } + /** * @return array */ diff --git a/lib/Mongo/MongoCursor.php b/lib/Mongo/MongoCursor.php index e98a2671..07be69aa 100644 --- a/lib/Mongo/MongoCursor.php +++ b/lib/Mongo/MongoCursor.php @@ -18,17 +18,17 @@ } use Alcaeus\MongoDbAdapter\AbstractCursor; +use Alcaeus\MongoDbAdapter\CursorIterator; use Alcaeus\MongoDbAdapter\TypeConverter; use Alcaeus\MongoDbAdapter\ExceptionConverter; use MongoDB\Driver\Cursor; -use MongoDB\Driver\ReadPreference; use MongoDB\Operation\Find; /** * Result object for database query. * @link http://www.php.net/manual/en/class.mongocursor.php */ -class MongoCursor extends AbstractCursor implements Iterator +class MongoCursor extends AbstractCursor implements Iterator, Countable, MongoCursorInterface { /** * @var bool @@ -133,6 +133,7 @@ public function awaitData($wait = true) * @param bool $foundOnly Send cursor limit and skip information to the count function, if applicable. * @return int The number of documents returned by this cursor's query. */ + #[\ReturnTypeWillChange] public function count($foundOnly = false) { $optionNames = ['hint', 'maxTimeMS']; @@ -472,16 +473,11 @@ protected function ensureCursor() /** * @param \Traversable $traversable - * @return \Generator + * @return CursorIterator */ protected function wrapTraversable(\Traversable $traversable) { - foreach ($traversable as $key => $value) { - if (isset($value->_id) && ($value->_id instanceof \MongoDB\BSON\ObjectID || !is_object($value->_id))) { - $key = (string) $value->_id; - } - yield $key => $value; - } + return new CursorIterator($traversable, true); } /** diff --git a/lib/Mongo/MongoDB.php b/lib/Mongo/MongoDB.php index 61750db3..f9a0f112 100644 --- a/lib/Mongo/MongoDB.php +++ b/lib/Mongo/MongoDB.php @@ -154,7 +154,9 @@ public function getCollectionInfo(array $options = []) 'info' => isset($info['info']) ? (array) $info['info'] : null, 'idIndex' => isset($info['idIndex']) ? (array) $info['idIndex'] : null, ], - function ($item) { return $item !== null; } + function ($item) { + return $item !== null; + } ); }; diff --git a/lib/Mongo/MongoDate.php b/lib/Mongo/MongoDate.php index 6c1e657a..7571ca84 100644 --- a/lib/Mongo/MongoDate.php +++ b/lib/Mongo/MongoDate.php @@ -88,15 +88,14 @@ public function toBSONType() public function toDateTime() { $datetime = new \DateTime(); + $datetime->setTimezone(new \DateTimeZone("UTC")); $datetime->setTimestamp($this->sec); $microSeconds = $this->truncateMicroSeconds($this->usec); if ($microSeconds > 0) { - $datetime = \DateTime::createFromFormat('Y-m-d H:i:s.u', $datetime->format('Y-m-d H:i:s') . '.' . str_pad($microSeconds, 6, '0', STR_PAD_LEFT)); + $datetime = \DateTime::createFromFormat('Y-m-d H:i:s.u e', $datetime->format('Y-m-d H:i:s') . '.' . str_pad($microSeconds, 6, '0', STR_PAD_LEFT) . ' UTC'); } - $datetime->setTimezone(new \DateTimeZone("UTC")); - return $datetime; } diff --git a/lib/Mongo/MongoGridFS.php b/lib/Mongo/MongoGridFS.php index 56718d3d..7105fbaa 100644 --- a/lib/Mongo/MongoGridFS.php +++ b/lib/Mongo/MongoGridFS.php @@ -207,7 +207,7 @@ public function storeBytes($bytes, array $extra = [], array $options = []) try { $file = $this->insertFile($record, $options); } catch (MongoException $e) { - throw new MongoGridFSException('Could not store file: '. $e->getMessage(), $e->getCode(), $e); + throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), $e->getCode(), $e); } try { diff --git a/lib/Mongo/MongoGridFSCursor.php b/lib/Mongo/MongoGridFSCursor.php index bec7b444..3da1542a 100644 --- a/lib/Mongo/MongoGridFSCursor.php +++ b/lib/Mongo/MongoGridFSCursor.php @@ -17,7 +17,7 @@ return; } -class MongoGridFSCursor extends MongoCursor +class MongoGridFSCursor extends MongoCursor implements Countable { /** * @static @@ -68,6 +68,6 @@ public function current() public function key() { $file = $this->current(); - return ($file !== null) ? (string)$file->file['_id'] : null; + return ($file !== null) ? (string) $file->file['_id'] : null; } } diff --git a/lib/Mongo/MongoId.php b/lib/Mongo/MongoId.php index a83b98eb..7a665b41 100644 --- a/lib/Mongo/MongoId.php +++ b/lib/Mongo/MongoId.php @@ -202,6 +202,7 @@ public static function __set_state(array $props) /** * @return stdClass */ + #[ReturnTypeWillChange] public function jsonSerialize() { $object = new stdClass(); diff --git a/lib/Mongo/MongoWriteBatch.php b/lib/Mongo/MongoWriteBatch.php index 399cc965..991f2450 100644 --- a/lib/Mongo/MongoWriteBatch.php +++ b/lib/Mongo/MongoWriteBatch.php @@ -83,7 +83,7 @@ protected function __construct(MongoCollection $collection, $batchType, $writeOp public function add($item) { if (is_object($item)) { - $item = (array)$item; + $item = (array) $item; } $this->validate($item); @@ -132,6 +132,17 @@ final public function execute(array $writeOptions = []) switch ($this->batchType) { case self::COMMAND_UPDATE: + if ($options['writeConcern']->getW() === 0) { + $resultDocument += [ + 'nMatched' => 0, + 'nModified' => 0, + 'nUpserted' => 0, + 'ok' => true, + ]; + + break; + } + $upsertedIds = []; foreach ($writeResult->getUpsertedIds() as $index => $id) { $upsertedIds[] = [ @@ -153,6 +164,15 @@ final public function execute(array $writeOptions = []) break; case self::COMMAND_DELETE: + if ($options['writeConcern']->getW() === 0) { + $resultDocument += [ + 'nRemoved' => 0, + 'ok' => true, + ]; + + break; + } + $resultDocument += [ 'nRemoved' => $writeResult->getDeletedCount(), 'ok' => true, @@ -160,6 +180,15 @@ final public function execute(array $writeOptions = []) break; case self::COMMAND_INSERT: + if ($options['writeConcern']->getW() === 0) { + $resultDocument += [ + 'nInserted' => 0, + 'ok' => true, + ]; + + break; + } + $resultDocument += [ 'nInserted' => $writeResult->getInsertedCount(), 'ok' => true, diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 00000000..ca441e66 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,45 @@ + + + + + + + + + + + + lib + tests + + + */tests/* + + + + + + + + + + + */lib/Mongo/* + + + + + error + + + + + + + + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3a237d32..eaa58e59 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -7,12 +7,13 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" stopOnFailure="false" - syntaxCheck="false" + bootstrap="vendor/autoload.php" > + @@ -20,10 +21,4 @@ ./tests/Alcaeus/MongoDbAdapter/ - - - - ./lib/Alcaeus/MongoDbAdapter - - diff --git a/tests/Alcaeus/MongoDbAdapter/Constraint/Constraint.php b/tests/Alcaeus/MongoDbAdapter/Constraint/Constraint.php new file mode 100644 index 00000000..8a908f1c --- /dev/null +++ b/tests/Alcaeus/MongoDbAdapter/Constraint/Constraint.php @@ -0,0 +1,16 @@ +value = self::prepare($value); + $this->allowExtraRootKeys = $allowExtraRootKeys; + $this->allowExtraKeys = $allowExtraKeys; + $this->allowOperators = $allowOperators; + $this->comparatorFactory = Factory::getInstance(); + } + + private function doEvaluate($other, $description = '', $returnResult = false) + { + $other = self::prepare($other); + $success = false; + $this->lastFailure = null; + + try { + $this->assertMatches($this->value, $other); + $success = true; + } catch (ExpectationFailedException $e) { + /* Rethrow internal assertion failures (e.g. operator type checks, + * EntityMap errors), which are logical errors in the code/test. */ + throw $e; + } catch (RuntimeException $e) { + /* This will generally catch internal errors from failAt(), which + * include a key path to pinpoint the failure. */ + $this->lastFailure = new ComparisonFailure( + $this->value, + $other, + /* TODO: Improve the exporter to canonicalize documents by + * sorting keys and remove spl_object_hash from output. */ + $this->exporter()->export($this->value), + $this->exporter()->export($other), + false, + $e->getMessage() + ); + } + + if ($returnResult) { + return $success; + } + + if (! $success) { + $this->fail($other, $description, $this->lastFailure); + } + } + + private function assertEquals($expected, $actual, $keyPath) + { + $expectedType = is_object($expected) ? get_class($expected) : gettype($expected); + $actualType = is_object($actual) ? get_class($actual) : gettype($actual); + + /* Early check to work around ObjectComparator printing the entire value + * for a failed type comparison. Avoid doing this if either value is + * numeric to allow for flexible numeric comparisons (e.g. 1 == 1.0). */ + if ($expectedType !== $actualType && ! (self::isNumeric($expected) || self::isNumeric($actual))) { + self::failAt(sprintf('%s is not expected type "%s"', $actualType, $expectedType), $keyPath); + } + + try { + $this->comparatorFactory->getComparatorFor($expected, $actual)->assertEquals($expected, $actual); + } catch (ComparisonFailure $e) { + /* Disregard other ComparisonFailure fields, as evaluate() only uses + * the message when creating its own ComparisonFailure. */ + self::failAt($e->getMessage(), $keyPath); + } + } + + private function assertMatches($expected, $actual, $keyPath = '') + { + if ($expected instanceof BSONArray) { + $this->assertMatchesArray($expected, $actual, $keyPath); + + return; + } + + if ($expected instanceof BSONDocument) { + $this->assertMatchesDocument($expected, $actual, $keyPath); + + return; + } + + $this->assertEquals($expected, $actual, $keyPath); + } + + private function assertMatchesArray(BSONArray $expected, $actual, $keyPath) + { + if (! $actual instanceof BSONArray) { + $actualType = is_object($actual) ? get_class($actual) : gettype($actual); + self::failAt(sprintf('%s is not instance of expected class "%s"', $actualType, BSONArray::class), $keyPath); + } + + if (count($expected) !== count($actual)) { + self::failAt(sprintf('$actual count is %d, expected %d', count($actual), count($expected)), $keyPath); + } + + foreach ($expected as $key => $expectedValue) { + $this->assertMatches( + $expectedValue, + $actual[$key], + (empty($keyPath) ? $key : $keyPath . '.' . $key) + ); + } + } + + private function assertMatchesDocument(BSONDocument $expected, $actual, $keyPath) + { + if ($this->allowOperators && self::isOperator($expected)) { + $this->assertMatchesOperator($expected, $actual, $keyPath); + + return; + } + + if (! $actual instanceof BSONDocument) { + $actualType = is_object($actual) ? get_class($actual) : gettype($actual); + self::failAt(sprintf('%s is not instance of expected class "%s"', $actualType, BSONDocument::class), $keyPath); + } + + foreach ($expected as $key => $expectedValue) { + $actualKeyExists = $actual->offsetExists($key); + + if ($this->allowOperators && $expectedValue instanceof BSONDocument && self::isOperator($expectedValue)) { + $operatorName = self::getOperatorName($expectedValue); + + if ($operatorName === '$$exists') { + Assert::assertIsBool($expectedValue['$$exists'], '$$exists requires bool'); + + if ($expectedValue['$$exists'] && ! $actualKeyExists) { + self::failAt(sprintf('$actual does not have expected key "%s"', $key), $keyPath); + } + + if (! $expectedValue['$$exists'] && $actualKeyExists) { + self::failAt(sprintf('$actual has unexpected key "%s"', $key), $keyPath); + } + + continue; + } + + if ($operatorName === '$$unsetOrMatches') { + if (! $actualKeyExists) { + continue; + } + + $expectedValue = $expectedValue['$$unsetOrMatches']; + } + } + + if (! $actualKeyExists) { + self::failAt(sprintf('$actual does not have expected key "%s"', $key), $keyPath); + } + + $this->assertMatches( + $expectedValue, + $actual[$key], + (empty($keyPath) ? $key : $keyPath . '.' . $key) + ); + } + + // Ignore extra keys in root documents + if ($this->allowExtraKeys || ($this->allowExtraRootKeys && empty($keyPath))) { + return; + } + + foreach ($actual as $key => $_) { + if (! $expected->offsetExists($key)) { + self::failAt(sprintf('$actual has unexpected key "%s"', $key), $keyPath); + } + } + } + + private function assertMatchesOperator(BSONDocument $operator, $actual, $keyPath) + { + $name = self::getOperatorName($operator); + + if ($name === '$$unsetOrMatches') { + /* If the operator is used at the top level, consider null values + * for $actual to be unset. If the operator is nested, this check is + * done later during document iteration. */ + if ($keyPath === '' && $actual === null) { + return; + } + + $this->assertMatches( + self::prepare($operator['$$unsetOrMatches']), + $actual, + $keyPath + ); + + return; + } + + throw new LogicException('unsupported operator: ' . $name); + } + + /** @see ConstraintTrait */ + private function doAdditionalFailureDescription($other) + { + if ($this->lastFailure === null) { + return ''; + } + + return $this->lastFailure->getMessage(); + } + + /** @see ConstraintTrait */ + private function doFailureDescription($other) + { + return 'expected value matches actual value'; + } + + /** @see ConstraintTrait */ + private function doMatches($other) + { + $other = self::prepare($other); + + try { + $this->assertMatches($this->value, $other); + } catch (RuntimeException $e) { + return false; + } + + return true; + } + + /** @see ConstraintTrait */ + private function doToString() + { + return 'matches ' . $this->exporter()->export($this->value); + } + + private static function failAt(string $message, string $keyPath) + { + $prefix = empty($keyPath) ? '' : sprintf('Field path "%s": ', $keyPath); + + throw new RuntimeException($prefix . $message); + } + + private static function getOperatorName(BSONDocument $document) + { + foreach ($document as $key => $_) { + if (strpos((string) $key, '$$') === 0) { + return $key; + } + } + + throw new LogicException('should not reach this point'); + } + + private static function isNumeric($value) + { + return is_int($value) || is_float($value) || $value instanceof Int64; + } + + private static function isOperator(BSONDocument $document) + { + if (count($document) !== 1) { + return false; + } + + foreach ($document as $key => $_) { + return strpos((string) $key, '$$') === 0; + } + + throw new LogicException('should not reach this point'); + } + + /** + * Prepare a value for comparison. + * + * If the value is an array or object, it will be converted to a BSONArray + * or BSONDocument. If $value is an array and $isRoot is true, it will be + * converted to a BSONDocument; otherwise, it will be converted to a + * BSONArray or BSONDocument based on its keys. Each value within an array + * or document will then be prepared recursively. + * + * @param mixed $bson + * @return mixed + */ + private static function prepare($bson) + { + if (! is_array($bson) && ! is_object($bson)) { + return $bson; + } + + /* Convert Int64 objects to integers on 64-bit platforms for + * compatibility reasons. */ + if ($bson instanceof Int64 && PHP_INT_SIZE != 4) { + return (int) ((string) $bson); + } + + /* TODO: Convert Int64 objects to integers on 32-bit platforms if they + * can be expressed as such. This is necessary to handle flexible + * numeric comparisons if the server returns 32-bit value as a 64-bit + * integer (e.g. cursor ID). */ + + // Serializable can produce an array or object, so recurse on its output + if ($bson instanceof Serializable) { + return self::prepare($bson->bsonSerialize()); + } + + /* Serializable has already been handled, so any remaining instances of + * Type will not serialize as BSON arrays or objects */ + if ($bson instanceof Type) { + return $bson; + } + + if (is_array($bson) && self::isArrayEmptyOrIndexed($bson)) { + $bson = new BSONArray($bson); + } + + if (! $bson instanceof BSONArray && ! $bson instanceof BSONDocument) { + /* If $bson is an object, any numeric keys may become inaccessible. + * We can work around this by casting back to an array. */ + $bson = new BSONDocument((array) $bson); + } + + foreach ($bson as $key => $value) { + if (is_array($value) || is_object($value)) { + $bson[$key] = self::prepare($value); + } + } + + return $bson; + } + + private static function isArrayEmptyOrIndexed(array $a) + { + if (empty($a)) { + return true; + } + + return array_keys($a) === range(0, count($a) - 1); + } +} diff --git a/tests/Alcaeus/MongoDbAdapter/Constraint/README.md b/tests/Alcaeus/MongoDbAdapter/Constraint/README.md new file mode 100644 index 00000000..36745fe8 --- /dev/null +++ b/tests/Alcaeus/MongoDbAdapter/Constraint/README.md @@ -0,0 +1,6 @@ +Document Constraints for PHPUnit +================================ + +The constraints in this directory have been copied from the MongoDB Library at +https://github.com/mongodb/mongo-php-library. This code is licensed under the +Apache License Version 2.0. diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoBinDataTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoBinDataTest.php index c6fbac9e..5b2abb1b 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoBinDataTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoBinDataTest.php @@ -15,10 +15,10 @@ class MongoBinDataTest extends TestCase public function testCreate() { $bin = new \MongoBinData(self::GUID, \MongoBinData::FUNC); - $this->assertAttributeSame(self::GUID, 'bin', $bin); - $this->assertAttributeSame(\MongoBinData::FUNC, 'type', $bin); + $this->assertSame(self::GUID, $bin->bin); + $this->assertSame(\MongoBinData::FUNC, $bin->type); - $this->assertSame('', (string)$bin); + $this->assertSame('', (string) $bin); return $bin; } @@ -44,7 +44,7 @@ public function testCreateWithBsonBinary() $bsonBinary = new \MongoDB\BSON\Binary(self::GUID, \MongoDB\BSON\Binary::TYPE_UUID); $bin = new \MongoBinData($bsonBinary); - $this->assertAttributeSame(self::GUID, 'bin', $bin); - $this->assertAttributeSame(\MongoBinData::UUID_RFC4122, 'type', $bin); + $this->assertSame(self::GUID, $bin->bin); + $this->assertSame(\MongoBinData::UUID_RFC4122, $bin->type); } } diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoClientTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoClientTest.php index d0edc4dd..de13f786 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoClientTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoClientTest.php @@ -9,9 +9,25 @@ */ class MongoClientTest extends TestCase { + /** + * @dataProvider provideConnectionUri + */ + public function testConnectionUri($uri, $expected) + { + $this->skipTestIf(extension_loaded('mongo')); + $this->assertSame($expected, (string) (new \MongoClient($uri, ['connect' => false]))); + } + + public function provideConnectionUri() + { + yield ['default', sprintf('mongodb://%s:%d', \MongoClient::DEFAULT_HOST, \MongoClient::DEFAULT_PORT)]; + yield ['localhost', 'mongodb://localhost']; + yield ['mongodb://localhost', 'mongodb://localhost']; + } + public function testSerialize() { - $this->assertInternalType('string', serialize($this->getClient())); + $this->assertIsString(serialize($this->getClient())); } public function testGetDb() @@ -58,7 +74,7 @@ public function testGetHosts() { $client = $this->getClient(); $hosts = $client->getHosts(); - $this->assertArraySubset( + $this->assertMatches( [ 'localhost:27017;-;.;' . getmypid() => [ 'host' => 'localhost', @@ -74,7 +90,7 @@ public function testGetHosts() public function testGetHostsExceptionHandling() { $this->expectException(\MongoConnectionException::class); - $this->expectExceptionMessageRegExp('/fake_host/'); + $this->expectErrorMessageMatches('/fake_host/'); $client = $this->getClient(null, 'mongodb://fake_host'); $client->getHosts(); @@ -246,7 +262,7 @@ public function testConnectWithUsernameAndPassword() $collection->insert($document); } - + public function testConnectWithUsernameAndPasswordInConnectionUrl() { $this->expectException(\MongoConnectionException::class); @@ -260,6 +276,13 @@ public function testConnectWithUsernameAndPasswordInConnectionUrl() $collection->insert($document); } + public function testConnectionUriOptionIntegerTypeCasting() + { + $client = new \MongoClient('mongodb://localhost/db?w=0&wtimeout=0', ['connect' => false]); + + $this->assertSame(['w' => 0, 'wtimeout' => 0], $client->getWriteConcern()); + } + /** * @param array $options * @return string diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCodeTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCodeTest.php index b55568f9..11a593f6 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCodeTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCodeTest.php @@ -4,6 +4,7 @@ use Alcaeus\MongoDbAdapter\Tests\TestCase; use Alcaeus\MongoDbAdapter\TypeInterface; +use ReflectionProperty; /** * @author alcaeus @@ -13,23 +14,49 @@ class MongoCodeTest extends TestCase public function testCreate() { $code = new \MongoCode('code', ['scope' => 'bleh']); - $this->assertAttributeSame('code', 'code', $code); - $this->assertAttributeSame(['scope' => 'bleh'], 'scope', $code); - $this->assertSame('code', (string)$code); + $this->assertSame('code', $this->getAttributeValue($code, 'code')); + $this->assertSame(['scope' => 'bleh'], $this->getAttributeValue($code, 'scope')); + + $this->assertSame('code', (string) $code); return $code; } - /** - * @depends testCreate - */ - public function testConvertToBson(\MongoCode $code) + public function testCreateWithoutScope() + { + $code = new \MongoCode('code'); + + $this->assertSame('code', $this->getAttributeValue($code, 'code')); + $this->assertSame([], $this->getAttributeValue($code, 'scope')); + + $this->assertSame('code', (string) $code); + + return $code; + } + + public function testConvertToBson() + { + $code = new \MongoCode('code', ['scope' => 'bleh']); + + $this->skipTestUnless($code instanceof TypeInterface); + + $bsonCode = $code->toBSONType(); + $this->assertInstanceOf('MongoDB\BSON\Javascript', $bsonCode); + $this->assertSame('code', $bsonCode->getCode()); + $this->assertEquals((object) ['scope' => 'bleh'], $bsonCode->getScope()); + } + + public function testConvertToBsonWithoutScope() { + $code = new \MongoCode('code'); + $this->skipTestUnless($code instanceof TypeInterface); $bsonCode = $code->toBSONType(); $this->assertInstanceOf('MongoDB\BSON\Javascript', $bsonCode); + $this->assertSame('code', $bsonCode->getCode()); + $this->assertNull($bsonCode->getScope()); } public function testCreateWithBsonObject() @@ -39,7 +66,15 @@ public function testCreateWithBsonObject() $bsonCode = new \MongoDB\BSON\Javascript('code', ['scope' => 'bleh']); $code = new \MongoCode($bsonCode); - $this->assertAttributeSame('code', 'code', $code); - $this->assertAttributeSame(['scope' => 'bleh'], 'scope', $code); + $this->assertSame('code', $this->getAttributeValue($code, 'code')); + $this->assertSame(['scope' => 'bleh'], $this->getAttributeValue($code, 'scope')); + } + + private function getAttributeValue(\MongoCode $code, $attribute) + { + $property = new ReflectionProperty($code, $attribute); + $property->setAccessible(true); + + return $property->getValue($code); } } diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCollectionTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCollectionTest.php index 5f533d23..501cbdf0 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCollectionTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCollectionTest.php @@ -2,10 +2,14 @@ namespace Alcaeus\MongoDbAdapter\Tests\Mongo; +use ArrayObject; use MongoDB\BSON\Regex; use MongoDB\Driver\ReadPreference; use Alcaeus\MongoDbAdapter\Tests\TestCase; +use MongoId; use PHPUnit\Framework\Error\Warning; +use function extension_loaded; +use function strcasecmp; /** * @author alcaeus @@ -14,7 +18,7 @@ class MongoCollectionTest extends TestCase { public function testSerialize() { - $this->assertInternalType('string', serialize($this->getCollection())); + $this->assertIsString(serialize($this->getCollection())); } public function testGetNestedCollections() @@ -44,19 +48,17 @@ public function testCreateRecord() $object = $newCollection->findOne(); $this->assertNotNull($object); - $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', '_id', $object); + $this->assertInstanceOf('MongoDB\BSON\ObjectID', $object->_id); $this->assertSame($id, (string) $object->_id); - $this->assertObjectHasAttribute('foo', $object); - $this->assertAttributeSame('bar', 'foo', $object); + $this->assertNotNull($object->foo); + $this->assertSame('bar', $object->foo); } public function testInsertInvalidData() { // Dirty hack to support both PHPUnit 5.x and 6.x - $className = class_exists(Warning::class) ? Warning::class : \PHPUnit_Framework_Error_Warning::class; - $this->expectException($className); - - $this->expectExceptionMessage('MongoCollection::insert(): expects parameter 1 to be an array or object, integer given'); + $this->expectWarning(); + $this->expectWarningMessage('MongoCollection::insert(): expects parameter 1 to be an array or object, integer given'); $document = 8; $this->getCollection()->insert($document); @@ -78,6 +80,34 @@ public function testInsertArrayWithNumericKeys() $this->assertSame(1, $this->getCollection()->count(['_id' => $document['_id']])); } + /** + * @dataProvider emptyIdProvider + */ + public function testInsertArrayWithEmptyIds($id) + { + $document = ['_id' => $id]; + $this->getCollection()->insert($document); + + $this->assertSame(1, $this->getCollection()->count(['_id' => $id])); + } + + public function emptyIdProvider() + { + return [ + 'Zero as string' => ['0'], + 'Zero as int' => [0], + 'Empty string' => [''], + ]; + } + + public function testInsertArrayWithEmptyId() + { + $document = ['_id' => '']; + $this->getCollection()->insert($document); + + $this->assertSame(1, $this->getCollection()->count(['_id' => $document['_id']])); + } + public function testInsertEmptyObject() { $document = (object) []; @@ -95,6 +125,15 @@ public function testInsertObjectWithPrivateProperties() $this->getCollection()->insert($document); } + public function testInsertArrayObjectWithProtectedProperties() + { + $document = new ArrayObjectWithProtectedProperties(['foo' => 'bar']); + $this->getCollection()->insert($document); + + $this->assertInstanceOf('MongoId', $document['_id']); + $this->assertEquals(['_id' => $document['_id'], 'foo' => 'bar'], $this->getCollection()->findOne(['_id' => $document['_id']])); + } + public function testInsertWithInvalidKey() { $document = ['*' => 'foo']; @@ -130,6 +169,20 @@ public function testInsertWithNumericKey() $this->assertSame(1, $this->getCollection()->count(['foo'])); } + public function testInsertWithAlphaNumericKey() + { + /** + * Force the array to store the key as a string "0". + * Initialising like ['0' => 'foo'] casts the string to an int. + */ + $document = new \stdClass(); + $document->{'0'} = 'foo'; + $document = (array) $document; + + $this->getCollection()->insert($document); + $this->assertSame(1, $this->getCollection()->count(['0' => 'foo'])); + } + public function testInsertDuplicate() { $collection = $this->getCollection(); @@ -142,7 +195,7 @@ public function testInsertDuplicate() unset($document['_id']); $this->expectException(\MongoDuplicateKeyException::class); - $this->expectExceptionMessageRegExp('/E11000 duplicate key error .* mongo-php-adapter\.test/'); + $this->expectErrorMessageMatches('/E11000 duplicate key error .* mongo-php-adapter\.test/'); $this->expectExceptionCode(11000); $collection->insert($document); } @@ -196,7 +249,7 @@ public function testInsertMany() ['foo' => 'bar'], ['bar' => 'foo'] ]; - $this->assertArraySubset($expected, $this->getCollection()->batchInsert($documents)); + $this->assertMatches($expected, $this->getCollection()->batchInsert($documents)); foreach ($documents as $document) { $this->assertInstanceOf('MongoId', $document['_id']); @@ -218,7 +271,7 @@ public function testInsertManyWithNonNumericKeys() 'a' => ['foo' => 'bar'], 'b' => ['bar' => 'foo'] ]; - $this->assertArraySubset($expected, $this->getCollection()->batchInsert($documents)); + $this->assertMatches($expected, $this->getCollection()->batchInsert($documents)); $newCollection = $this->getCheckDatabase()->selectCollection('test'); $this->assertSame(2, $newCollection->count()); @@ -238,7 +291,7 @@ public function testBatchInsertContinuesOnError() 8, 'b' => ['bar' => 'foo'] ]; - $this->assertArraySubset($expected, $this->getCollection()->batchInsert($documents, ['continueOnError' => true])); + $this->assertMatches($expected, $this->getCollection()->batchInsert($documents, ['continueOnError' => true])); $newCollection = $this->getCheckDatabase()->selectCollection('test'); $this->assertSame(1, $newCollection->count()); @@ -250,7 +303,7 @@ public function testBatchInsertException() $documents = [['_id' => $id, 'foo' => 'bar'], ['_id' => $id, 'foo' => 'bleh']]; $this->expectException(\MongoDuplicateKeyException::class); - $this->expectExceptionMessageRegExp('/E11000 duplicate key error .* mongo-php-adapter.test.*_id_/'); + $this->expectErrorMessageMatches('/E11000 duplicate key error .* mongo-php-adapter.test.*_id_/'); $this->expectExceptionCode(11000); $this->getCollection()->batchInsert($documents); @@ -362,7 +415,7 @@ public function testUpdateReplaceOne() public function testUpdateReplaceMultiple() { $this->expectException(\MongoWriteConcernException::class); - $this->expectExceptionMessageRegExp('/multi update only works with \$ operators/', 9); + $this->expectErrorMessageMatches('/multi update only works with \$ operators/', 9); $this->getCollection()->update(['foo' => 'bar'], ['foo' => 'foo'], ['multiple' => true]); } @@ -552,7 +605,7 @@ public function testFindWithProjection($projection) foreach ($cursor as $document) { $this->assertCount(2, $document); $this->assertArrayHasKey('_id', $document); - $this->assertArraySubset(['bar' => 'bar'], $document); + $this->assertMatches(['bar' => 'bar'], $document); } } @@ -614,7 +667,7 @@ public function testFindWithProjectionExcludeId($projection) foreach ($cursor as $document) { $this->assertCount(1, $document); $this->assertArrayNotHasKey('_id', $document); - $this->assertArraySubset(['bar' => 'bar'], $document); + $this->assertMatches(['bar' => 'bar'], $document); } } @@ -714,7 +767,7 @@ public function testFindOneWithProjection() $document = $this->getCollection()->findOne(['foo' => 'foo'], ['bar' => true]); $this->assertCount(2, $document); - $this->assertArraySubset(['bar' => 'bar'], $document); + $this->assertMatches(['bar' => 'bar'], $document); } public function testFindOneWithLegacyProjection() @@ -724,7 +777,7 @@ public function testFindOneWithLegacyProjection() $document = $this->getCollection()->findOne(['foo' => 'foo'], ['bar']); $this->assertCount(2, $document); - $this->assertArraySubset(['bar' => 'bar'], $document); + $this->assertMatches(['bar' => 'bar'], $document); } public function testFindOneNotFound() @@ -748,7 +801,7 @@ public function testDistinct() $this->prepareData(); $values = $this->getCollection()->distinct('foo'); - $this->assertInternalType('array', $values); + $this->assertIsArray($values); sort($values); $this->assertEquals(['bar', 'foo'], $values); @@ -759,7 +812,7 @@ public function testDistinctWithQuery() $this->prepareData(); $values = $this->getCollection()->distinct('foo', ['foo' => 'bar']); - $this->assertInternalType('array', $values); + $this->assertIsArray($values); $this->assertEquals(['bar'], $values); } @@ -791,6 +844,8 @@ public function testDistinctWithIdQuery() public function testAggregate() { + $this->skipTestIf(extension_loaded('mongo')); + $collection = $this->getCollection(); $this->prepareData(); @@ -807,8 +862,8 @@ public function testAggregate() ] ]; - $result = $collection->aggregate($pipeline); - $this->assertInternalType('array', $result); + $result = $collection->aggregate($pipeline, ['cursor' => true]); + $this->assertIsArray($result); $this->assertArrayHasKey('result', $result); $this->assertEquals([ @@ -819,6 +874,8 @@ public function testAggregate() public function testAggregateWithMultiplePilelineOperatorsAsArguments() { + $this->skipTestIf(version_compare($this->getServerVersion(), '3.6.0', '>='), 'Test does not apply to MongoDB >= 3.6.'); + $collection = $this->getCollection(); $this->prepareData(); @@ -842,7 +899,7 @@ public function testAggregateWithMultiplePilelineOperatorsAsArguments() $this->fail($msg); } - $this->assertInternalType('array', $result); + $this->assertIsArray($result); $this->assertArrayHasKey('result', $result); $this->assertEquals([ @@ -853,6 +910,8 @@ public function testAggregateWithMultiplePilelineOperatorsAsArguments() public function testAggregateInvalidPipeline() { + $this->skipTestIf(extension_loaded('mongo')); + $collection = $this->getCollection(); $pipeline = [ @@ -863,7 +922,7 @@ public function testAggregateInvalidPipeline() $this->expectException(\MongoResultException::class); $this->expectExceptionMessage('Unrecognized pipeline stage name'); - $collection->aggregate($pipeline); + $collection->aggregate($pipeline, ['cursor' => true]); } public function testAggregateTimeoutException() @@ -886,7 +945,7 @@ public function testAggregateTimeoutException() ] ]; - $collection->aggregate($pipeline, ['maxTimeMS' => 1]); + $collection->aggregate($pipeline, ['maxTimeMS' => 1, 'cursor' => true]); } public function testAggregateCursor() @@ -930,7 +989,7 @@ public function testReadPreference() $this->assertSame(['type' => \MongoClient::RP_SECONDARY_PREFERRED, 'tagsets' => [['a' => 'b']]], $collection->getReadPreference()); $this->assertTrue($collection->setSlaveOkay(false)); - $this->assertArraySubset(['type' => \MongoClient::RP_PRIMARY], $collection->getReadPreference()); + $this->assertMatches(['type' => \MongoClient::RP_PRIMARY], $collection->getReadPreference()); } public function testReadPreferenceIsSetInDriver() @@ -1012,10 +1071,9 @@ public function testSaveInsert() $object = $newCollection->findOne(); $this->assertNotNull($object); - $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', '_id', $object); + $this->assertInstanceOf('MongoDB\BSON\ObjectID', $object->_id); $this->assertSame($id, (string) $object->_id); - $this->assertObjectHasAttribute('foo', $object); - $this->assertAttributeSame('bar', 'foo', $object); + $this->assertSame('bar', $object->foo); } public function testRemoveOne() @@ -1055,13 +1113,13 @@ public function testSaveUpdate() $object = $newCollection->findOne(); $this->assertNotNull($object); - $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', '_id', $object); + $this->assertInstanceOf('MongoDB\BSON\ObjectID', $object->_id); $this->assertSame($id, (string) $object->_id); - $this->assertObjectHasAttribute('foo', $object); - $this->assertAttributeSame('foo', 'foo', $object); + $this->assertSame('foo', $object->foo); } - public function testSavingShouldReplaceTheWholeDocument() { + public function testSavingShouldReplaceTheWholeDocument() + { $id = '54203e08d51d4a1f868b456e'; $collection = $this->getCollection(); @@ -1076,7 +1134,7 @@ public function testSavingShouldReplaceTheWholeDocument() { $object = $newCollection->findOne(); $this->assertNotNull($object); - $this->assertObjectNotHasAttribute('foo', $object); + $this->assertArrayNotHasKey('bar', $object); } public function testSaveDuplicate() @@ -1339,8 +1397,8 @@ public function testDeleteIndexUsingIndexName() $expected['code'] = 27; } - // Using assertArraySubset because newer versions (3.4.7?) also return `codeName` - $this->assertArraySubset($expected, $this->getCollection()->deleteIndex('bar')); + // Using assertMatches because newer versions (3.4.7?) also return `codeName` + $this->assertMatches($expected, $this->getCollection()->deleteIndex('bar')); $this->assertCount(2, iterator_to_array($newCollection->listIndexes())); } @@ -1390,17 +1448,14 @@ public function testDeleteIndexes() public function testDeleteIndexesForNonExistingCollection() { - $expected = [ - 'ok' => 0.0, - 'errmsg' => 'ns not found', - ]; + $result = $this->getCollection('nonExisting')->deleteIndexes(); + $this->assertSame(0.0, $result['ok']); + $this->assertMatchesRegularExpression('#ns not found#', $result['errmsg']); if (version_compare($this->getServerVersion(), '3.4.0', '>=')) { + $this->assertSame(26, $result['code']); $expected['code'] = 26; } - - // Using assertArraySubset because newer versions (3.4.7?) also return `codeName` - $this->assertArraySubset($expected, $this->getCollection('nonExisting')->deleteIndexes()); } public function dataGetIndexInfo() @@ -1551,7 +1606,7 @@ public function testFindAndModifyUpdate() $object = $newCollection->findOne(); $this->assertNotNull($object); - $this->assertAttributeSame('foo', 'foo', $object); + $this->assertSame('foo', $object->foo); } public function testFindAndModifyUpdateWithUpdateOptions() @@ -1576,8 +1631,8 @@ public function testFindAndModifyUpdateWithUpdateOptions() $object = $newCollection->findOne(); $this->assertNotNull($object); - $this->assertAttributeSame('foo', 'bar', $object); - $this->assertObjectNotHasAttribute('foo', $object); + $this->assertSame('foo', $object->bar); + $this->assertArrayNotHasKey('foo', $object); } public function testFindAndModifyWithUpdateParamAndOption() @@ -1605,8 +1660,8 @@ public function testFindAndModifyWithUpdateParamAndOption() $object = $newCollection->findOne(); $this->assertNotNull($object); - $this->assertAttributeSame('foobar', 'foo', $object); - $this->assertObjectNotHasAttribute('bar', $object); + $this->assertSame('foobar', $object->foo); + $this->assertArrayNotHasKey('bar', $object); } public function testFindAndModifyUpdateReplace() @@ -1627,8 +1682,8 @@ public function testFindAndModifyUpdateReplace() $object = $newCollection->findOne(); $this->assertNotNull($object); - $this->assertAttributeSame('boo', 'foo', $object); - $this->assertObjectNotHasAttribute('bar', $object); + $this->assertSame('boo', $object->foo); + $this->assertArrayNotHasKey('bar', $object); } public function testFindAndModifyUpdateReturnNew() @@ -1669,6 +1724,8 @@ public function testFindAndModifyWithFields() public function testGroup() { + $this->skipTestIf(version_compare($this->getServerVersion(), '4.2.0', '>='), 'Test does not apply to MongoDB >= 4.2.'); + $collection = $this->getCollection(); $document1 = ['a' => 2]; @@ -1684,7 +1741,7 @@ public function testGroup() $result = $collection->group($keys, $initial, $reduce, $condition); - $this->assertArraySubset( + $this->assertMatches( [ 'retval' => [['count' => 1.0]], 'count' => 1.0, @@ -1740,7 +1797,7 @@ public function testMapReduce() 'map' => new \MongoCode($map), 'reduce' => new \MongoCode($reduce), 'query' => (object) [], - 'out' => ['inline' => true], + 'out' => ['inline' => 1], 'finalize' => new \MongoCode($finalize), ]; @@ -1765,8 +1822,12 @@ public function testMapReduce() ], ]; + usort($result['results'], function ($a, $b) { + return strcasecmp($a['_id'], $b['_id']); + }); + $this->assertSame(1.0, $result['ok']); - $this->assertSame($expected, $result['results']); + $this->assertEquals($expected, $result['results']); } public function testFindAndModifyResultException() @@ -1828,12 +1889,11 @@ public function testValidate() $collection->insert($document); $result = $collection->validate(); - $this->assertArraySubset( + $this->assertMatches( [ 'ns' => 'mongo-php-adapter.test', 'nrecords' => 1, 'nIndexes' => 1, - 'keysPerIndex' => ['mongo-php-adapter.test.$_id_' => 1], 'valid' => true, 'errors' => [], ], @@ -1850,7 +1910,7 @@ public function testDrop() 'nIndexesWas' => 1, 'ok' => 1.0 ]; - $this->assertSame($expected, $this->getCollection()->drop()); + $this->assertEquals($expected, $this->getCollection()->drop()); } public function testEmptyCollectionName() @@ -1976,3 +2036,8 @@ class PrivatePropertiesStub { private $foo = 'bar'; } + +class ArrayObjectWithProtectedProperties extends ArrayObject +{ + protected $something = 'baz'; +} diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCommandCursorTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCommandCursorTest.php index e5a8d5c3..3d557608 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCommandCursorTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCommandCursorTest.php @@ -2,6 +2,7 @@ namespace Alcaeus\MongoDbAdapter\Tests\Mongo; +use MongoCursorInterface; use MongoDB\Database; use MongoDB\Driver\ReadPreference; use Alcaeus\MongoDbAdapter\Tests\TestCase; @@ -15,7 +16,7 @@ public function testSerialize() { $this->prepareData(); $cursor = $this->getCollection()->aggregateCursor([['$match' => ['foo' => 'bar']]]); - $this->assertInternalType('string', serialize($cursor)); + $this->assertIsString(serialize($cursor)); } public function testInfo() @@ -58,7 +59,7 @@ public function testInfo() 'connection_type_desc' => 'STANDALONE', ]; - $this->assertArraySubset($expected, $cursor->info()); + $this->assertMatches($expected, $cursor->info()); $i = 0; foreach ($array as $key => $value) { @@ -136,7 +137,7 @@ public function dataCommandAppliesCorrectReadPreference() 'mapReduceWithOutInline' => [ [ 'mapReduce' => (string) $this->getCollection(), - 'out' => ['inline' => true], + 'out' => ['inline' => 1], ], ReadPreference::RP_SECONDARY, ], @@ -202,4 +203,12 @@ public function dataCommandAppliesCorrectReadPreference() ], ]; } + + public function testInterfaces() + { + $this->prepareData(); + $cursor = $this->getCollection()->aggregateCursor([['$match' => ['foo' => 'bar']]]); + + $this->assertInstanceOf(MongoCursorInterface::class, $cursor); + } } diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCursorTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCursorTest.php index abc1bdde..d2af27fb 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCursorTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoCursorTest.php @@ -4,6 +4,8 @@ use Alcaeus\MongoDbAdapter\Tests\TestCase; use Alcaeus\MongoDbAdapter\TypeConverter; +use Countable; +use MongoCursorInterface; use MongoDB\Driver\ReadPreference; use MongoDB\Model\BSONDocument; use MongoDB\Operation\Find; @@ -17,7 +19,7 @@ public function testSerialize() { $this->prepareData(); $cursor = $this->getCollection()->find(['foo' => 'bar']); - $this->assertInternalType('string', serialize($cursor)); + $this->assertIsString(serialize($cursor)); } public function testCursorConvertsTypes() @@ -200,7 +202,8 @@ public function testCursorAppliesOptions($checkOptionCallback, \Closure $applyOp public static function getCursorOptions() { - function getMissingOptionCallback($optionName) { + function getMissingOptionCallback($optionName) + { return function ($value) use ($optionName) { return is_array($value) && @@ -208,7 +211,8 @@ function getMissingOptionCallback($optionName) { }; } - function getBasicCheckCallback($expected, $optionName) { + function getBasicCheckCallback($expected, $optionName) + { return function ($value) use ($expected, $optionName) { return is_array($value) && @@ -217,7 +221,8 @@ function getBasicCheckCallback($expected, $optionName) { }; } - function getModifierCheckCallback($expected, $modifierName) { + function getModifierCheckCallback($expected, $modifierName) + { return function ($value) use ($expected, $modifierName) { return is_array($value) && @@ -429,23 +434,23 @@ public function testExplain() 'parsedQuery' => [ 'foo' => ['$eq' => 'bar'] ], - 'winningPlan' => [], - 'rejectedPlans' => [], + 'winningPlan' => ['$$exists' => true], + 'rejectedPlans' => ['$$exists' => true], ], 'executionStats' => [ 'executionSuccess' => true, 'nReturned' => 1, 'totalKeysExamined' => 0, 'totalDocsExamined' => 3, - 'executionStages' => [], - 'allPlansExecution' => [], + 'executionStages' => ['$$exists' => true], + 'allPlansExecution' => ['$$exists' => true], ], 'serverInfo' => [ 'port' => 27017, ], ]; - $this->assertArraySubset($expected, $cursor->explain()); + $this->assertMatches($expected, $cursor->explain()); } public function testExplainWithEmptyProjection() @@ -463,23 +468,23 @@ public function testExplainWithEmptyProjection() 'parsedQuery' => [ 'foo' => ['$eq' => 'bar'] ], - 'winningPlan' => [], - 'rejectedPlans' => [], + 'winningPlan' => ['$$exists' => true], + 'rejectedPlans' => ['$$exists' => true], ], 'executionStats' => [ 'executionSuccess' => true, 'nReturned' => 2, 'totalKeysExamined' => 0, 'totalDocsExamined' => 3, - 'executionStages' => [], - 'allPlansExecution' => [], + 'executionStages' => ['$$exists' => true], + 'allPlansExecution' => ['$$exists' => true], ], 'serverInfo' => [ 'port' => 27017, ], ]; - $this->assertArraySubset($expected, $cursor->explain()); + $this->assertMatches($expected, $cursor->explain()); } public function testExplainConvertsQuery() @@ -494,23 +499,36 @@ public function testExplainConvertsQuery() 'plannerVersion' => 1, 'namespace' => 'mongo-php-adapter.test', 'indexFilterSet' => false, - 'winningPlan' => [], - 'rejectedPlans' => [], + 'winningPlan' => ['$$exists' => true], + 'rejectedPlans' => ['$$exists' => true], ], 'executionStats' => [ 'executionSuccess' => true, 'nReturned' => 2, 'totalKeysExamined' => 0, 'totalDocsExamined' => 3, - 'executionStages' => [], - 'allPlansExecution' => [], + 'executionStages' => ['$$exists' => true], + 'allPlansExecution' => ['$$exists' => true], ], 'serverInfo' => [ 'port' => 27017, ], ]; - $this->assertArraySubset($expected, $cursor->explain()); + $this->assertMatches($expected, $cursor->explain()); + } + + public function testInterfaces() + { + $collection = $this->getCollection(); + $cursor = $collection->find(); + + $this->assertInstanceOf(MongoCursorInterface::class, $cursor); + + // The countable interface is necessary for compatibility with PHP 7.3+, but not implemented by MongoCursor + if (! extension_loaded('mongo')) { + $this->assertInstanceOf(Countable::class, $cursor); + } } diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDBRefTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDBRefTest.php index ba39c8ca..fb5000b8 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDBRefTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDBRefTest.php @@ -85,7 +85,7 @@ public function testGet() $db->selectCollection('test')->insert($document); $fetchedRef = \MongoDBRef::get($db, ['$ref' => 'test', '$id' => $id]); - $this->assertInternalType('array', $fetchedRef); + $this->assertIsArray($fetchedRef); $this->assertEquals($document, $fetchedRef); } @@ -99,7 +99,7 @@ public function testGetThroughMongoDB() $db->selectCollection('test')->insert($document); $fetchedRef = $db->getDBRef(['$ref' => 'test', '$id' => $id]); - $this->assertInternalType('array', $fetchedRef); + $this->assertIsArray($fetchedRef); $this->assertEquals($document, $fetchedRef); } diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDBTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDBTest.php index 1636341e..9b7d18db 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDBTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDBTest.php @@ -12,7 +12,7 @@ class MongoDBTest extends TestCase { public function testSerialize() { - $this->assertInternalType('string', serialize($this->getDatabase())); + $this->assertIsString(serialize($this->getDatabase())); } public function testEmptyDatabaseName() @@ -126,8 +126,8 @@ public function testCommandError() 'code' => 13, ]; - // Using assertArraySubset because newer versions (3.4.7?) also return `codeName` - $this->assertArraySubset($expected, $db->command(['listDatabases' => 1])); + // Using assertMatches because newer versions (3.4.7?) also return `codeName` + $this->assertMatches($expected, $db->command(['listDatabases' => 1])); } public function testCommandCursorTimeout() @@ -164,7 +164,7 @@ public function testReadPreference() $this->assertTrue($database->setSlaveOkay(false)); // Only test a subset since we don't keep tagsets around for RP_PRIMARY - $this->assertArraySubset(['type' => \MongoClient::RP_PRIMARY], $database->getReadPreference()); + $this->assertMatches(['type' => \MongoClient::RP_PRIMARY], $database->getReadPreference()); } public function testReadPreferenceIsSetInDriver() @@ -180,7 +180,6 @@ public function testReadPreferenceIsSetInDriver() $this->assertSame(ReadPreference::RP_SECONDARY, $readPreference->getMode()); $this->assertSame([['a' => 'b']], $readPreference->getTagSets()); - } public function testReadPreferenceIsInherited() @@ -241,6 +240,8 @@ public function testForceError() public function testExecute() { + $this->skipTestIf(version_compare($this->getServerVersion(), '4.2.0', '>='), 'Eval no longer works on MongoDB 4.2.0 and newer'); + $db = $this->getDatabase(); $document = ['foo' => 'bar']; $this->getCollection()->insert($document); @@ -291,7 +292,7 @@ public function testGetCollectionInfo() ], ]; } - $this->assertEquals($expected, $collectionInfo); + $this->assertMatches($expected, $collectionInfo); return; } } @@ -385,11 +386,13 @@ public function testDropCollection() 'nIndexesWas' => 1, 'ok' => 1.0 ]; - $this->assertSame($expected, $this->getDatabase()->dropCollection('test')); + $this->assertEquals($expected, $this->getDatabase()->dropCollection('test')); } public function testRepair() { + $this->skipTestIf(version_compare($this->getServerVersion(), '4.2.0', '>='), 'The "repairDatabase" has been removed in MongoDB 4.2.0'); + $this->assertSame(['ok' => 1.0], $this->getDatabase()->repair()); } } diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDateTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDateTest.php index db58c4bb..6f75386b 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDateTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDateTest.php @@ -17,7 +17,8 @@ public function testTimeZoneDoesNotAlterReturnedDateTime() ini_set("date.timezone", "UTC"); // Today at 8h 8m 8s - $timestamp = mktime (8, 8, 8); $date = new \MongoDate($timestamp); + $timestamp = mktime(8, 8, 8); + $date = new \MongoDate($timestamp); $this->assertSame('08:08:08', $date->toDateTime()->format("H:i:s")); @@ -31,8 +32,8 @@ public function testTimeZoneDoesNotAlterReturnedDateTime() public function testCreate() { $date = new \MongoDate(1234567890, 123456); - $this->assertAttributeSame(1234567890, 'sec', $date); - $this->assertAttributeSame(123000, 'usec', $date); + $this->assertSame(1234567890, $date->sec); + $this->assertSame(123000, $date->usec); $this->assertSame('0.12300000 1234567890', (string) $date); $dateTime = $date->toDateTime(); @@ -67,8 +68,8 @@ public function testConvertToBson(\MongoDate $date) public function testCreateWithString() { $date = new \MongoDate('1234567890', '123456'); - $this->assertAttributeSame(1234567890, 'sec', $date); - $this->assertAttributeSame(123000, 'usec', $date); + $this->assertSame(1234567890, $date->sec); + $this->assertSame(123000, $date->usec); } public function testCreateWithBsonDate() @@ -78,15 +79,15 @@ public function testCreateWithBsonDate() $bsonDate = new \MongoDB\BSON\UTCDateTime(1234567890123); $date = new \MongoDate($bsonDate); - $this->assertAttributeSame(1234567890, 'sec', $date); - $this->assertAttributeSame(123000, 'usec', $date); + $this->assertSame(1234567890, $date->sec); + $this->assertSame(123000, $date->usec); } public function testSupportMillisecondsWithLeadingZeroes() { $date = new \MongoDate('1234567890', '012345'); - $this->assertAttributeSame(1234567890, 'sec', $date); - $this->assertAttributeSame(12000, 'usec', $date); + $this->assertSame(1234567890, $date->sec); + $this->assertSame(12000, $date->usec); $this->assertSame('0.01200000 1234567890', (string) $date); $dateTime = $date->toDateTime(); @@ -94,4 +95,33 @@ public function testSupportMillisecondsWithLeadingZeroes() $this->assertSame(1234567890, $dateTime->getTimestamp()); $this->assertSame('012000', $dateTime->format('u')); } + + public function testDSTTransitionDoesNotProduceWrongResults() + { + $initialTZ = ini_get("date.timezone"); + + ini_set("date.timezone", "Europe/Madrid"); + + $date = new \MongoDate(1603584000); + $dateTime = $date->toDateTime(); + + $this->assertSame(1603584000, $dateTime->getTimestamp()); + + ini_set("date.timezone", $initialTZ); + } + + public function testDSTTransitionDoesNotProduceWrongResultsWithMicroSeconds() + { + $initialTZ = ini_get("date.timezone"); + + ini_set("date.timezone", "Europe/Madrid"); + + $date = new \MongoDate(1603584000, 123456); + $dateTime = $date->toDateTime(); + + $this->assertSame(1603584000, $dateTime->getTimestamp()); + $this->assertSame('123000', $dateTime->format('u')); + + ini_set("date.timezone", $initialTZ); + } } diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDeleteBatchTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDeleteBatchTest.php index 1e41e31a..6502ae95 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDeleteBatchTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoDeleteBatchTest.php @@ -9,7 +9,7 @@ class MongoDeleteBatchTest extends TestCase public function testSerialize() { $batch = new \MongoDeleteBatch($this->getCollection()); - $this->assertInternalType('string', serialize($batch)); + $this->assertIsString(serialize($batch)); } public function testDeleteOne() @@ -58,6 +58,29 @@ public function testDeleteMany() $this->assertSame(0, $newCollection->count()); } + public function testDeleteManyWithoutAck() + { + $collection = $this->getCollection(); + $batch = new \MongoDeleteBatch($collection); + + $document = ['foo' => 'bar']; + $collection->insert($document); + unset($document['_id']); + $collection->insert($document); + + $this->assertTrue($batch->add(['q' => ['foo' => 'bar'], 'limit' => 0])); + + $expected = [ + 'nRemoved' => 0, + 'ok' => true, + ]; + + $this->assertSame($expected, $batch->execute(['w' => 0])); + + $newCollection = $this->getCheckDatabase()->selectCollection('test'); + $this->assertSame(0, $newCollection->count()); + } + public function testValidateItem() { $collection = $this->getCollection(); diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoGridFSCursorTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoGridFSCursorTest.php index 81c815b4..afbe9c4e 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoGridFSCursorTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoGridFSCursorTest.php @@ -3,6 +3,7 @@ namespace Alcaeus\MongoDbAdapter\Tests\Mongo; use Alcaeus\MongoDbAdapter\Tests\TestCase; +use Countable; class MongoGridFSCursorTest extends TestCase { @@ -13,7 +14,7 @@ public function testSerialize() $gridfs->storeBytes('bar', ['filename' => 'bar.txt']); $cursor = $gridfs->find(['filename' => 'foo.txt']); - $this->assertInternalType('string', serialize($cursor)); + $this->assertIsString(serialize($cursor)); } public function testCursorItems() @@ -25,11 +26,11 @@ public function testCursorItems() $cursor = $gridfs->find(['filename' => 'foo.txt']); $this->assertCount(1, $cursor); foreach ($cursor as $key => $value) { - $this->assertSame((string)$id, $key); + $this->assertSame((string) $id, $key); $this->assertInstanceOf('MongoGridFSFile', $value); $this->assertSame('foo', $value->getBytes()); - $this->assertArraySubset([ + $this->assertMatches([ 'filename' => 'foo.txt', 'chunkSize' => 261120, 'length' => 3, @@ -37,4 +38,16 @@ public function testCursorItems() ], $value->file); } } + + public function testInterfaces() + { + $this->skipTestIf(extension_loaded('mongo')); + + $gridfs = $this->getGridFS(); + $id = $gridfs->storeBytes('foo', ['filename' => 'foo.txt']); + $gridfs->storeBytes('bar', ['filename' => 'bar.txt']); + + $cursor = $gridfs->find(['filename' => 'foo.txt']); + $this->assertInstanceOf(Countable::class, $cursor); + } } diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoGridFSFileTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoGridFSFileTest.php index d718b97b..119bf9fd 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoGridFSFileTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoGridFSFileTest.php @@ -12,14 +12,14 @@ public function testSerialize() $file = $this->getGridFS()->findOne(['filename' => 'foo']); $this->assertInstanceOf(\MongoGridFSFile::class, $file); - $this->assertInternalType('string', serialize($file)); + $this->assertIsString(serialize($file)); } public function testFileProperty() { $file = $this->getFile(); $this->assertArrayHasKey('_id', $file->file); - $this->assertArraySubset( + $this->assertMatches( [ 'length' => 666, 'filename' => 'file', diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoGridFSTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoGridFSTest.php index 4d163b4f..044ee86c 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoGridFSTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoGridFSTest.php @@ -8,7 +8,7 @@ class MongoGridFSTest extends TestCase { public function testSerialize() { - $this->assertInternalType('string', serialize($this->getGridFS())); + $this->assertIsString(serialize($this->getGridFS())); } public function testChunkProperty() @@ -70,33 +70,29 @@ public function testStoringData() $record = $newCollection->findOne(); $this->assertNotNull($record); - $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', '_id', $record); + $this->assertInstanceOf('MongoDB\BSON\ObjectID', $record->_id); $this->assertSame((string) $id, (string) $record->_id); - $this->assertObjectHasAttribute('foo', $record); - $this->assertAttributeSame('bar', 'foo', $record); - $this->assertObjectHasAttribute('length', $record); - $this->assertAttributeSame(4, 'length', $record); - $this->assertObjectHasAttribute('chunkSize', $record); - $this->assertAttributeSame(2, 'chunkSize', $record); - $this->assertObjectHasAttribute('md5', $record); - $this->assertAttributeSame('e2fc714c4727ee9395f324cd2e7f331f', 'md5', $record); + $this->assertSame('bar', $record->foo); + $this->assertSame(4, $record->length); + $this->assertSame(2, $record->chunkSize); + $this->assertSame('e2fc714c4727ee9395f324cd2e7f331f', $record->md5); $chunksCursor = $newChunksCollection->find([], ['sort' => ['n' => 1]]); $chunks = iterator_to_array($chunksCursor); $firstChunk = $chunks[0]; $this->assertNotNull($firstChunk); - $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', 'files_id', $firstChunk); + $this->assertInstanceOf('MongoDB\BSON\ObjectID', $firstChunk->files_id); $this->assertSame((string) $id, (string) $firstChunk->files_id); - $this->assertAttributeSame(0, 'n', $firstChunk); - $this->assertAttributeInstanceOf('MongoDB\BSON\Binary', 'data', $firstChunk); + $this->assertSame(0, $firstChunk->n); + $this->assertInstanceOf('MongoDB\BSON\Binary', $firstChunk->data); $this->assertSame('ab', (string) $firstChunk->data->getData()); $secondChunck = $chunks[1]; $this->assertNotNull($secondChunck); - $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', 'files_id', $secondChunck); + $this->assertInstanceOf('MongoDB\BSON\ObjectID', $secondChunck->files_id); $this->assertSame((string) $id, (string) $secondChunck->files_id); - $this->assertAttributeSame(1, 'n', $secondChunck); - $this->assertAttributeInstanceOf('MongoDB\BSON\Binary', 'data', $secondChunck); + $this->assertSame(1, $secondChunck->n); + $this->assertInstanceOf('MongoDB\BSON\Binary', $secondChunck->data); $this->assertSame('cd', (string) $secondChunck->data->getData()); } @@ -164,29 +160,24 @@ public function testStoreFile() $size = filesize($filename); $record = $newCollection->findOne(); $this->assertNotNull($record); - $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', '_id', $record); + $this->assertInstanceOf('MongoDB\BSON\ObjectID', $record->_id); $this->assertSame((string) $id, (string) $record->_id); - $this->assertObjectHasAttribute('foo', $record); - $this->assertAttributeSame('bar', 'foo', $record); - $this->assertObjectHasAttribute('length', $record); - $this->assertAttributeSame($size, 'length', $record); - $this->assertObjectHasAttribute('chunkSize', $record); - $this->assertAttributeSame(100, 'chunkSize', $record); - $this->assertObjectHasAttribute('md5', $record); - $this->assertAttributeSame($md5, 'md5', $record); - $this->assertObjectHasAttribute('filename', $record); - $this->assertAttributeSame($filename, 'filename', $record); - - $numberOfChunks = (int)ceil($size / 100); + $this->assertSame('bar', $record->foo); + $this->assertSame($size, $record->length); + $this->assertSame(100, $record->chunkSize); + $this->assertSame($md5, $record->md5); + $this->assertSame($filename, $record->filename); + + $numberOfChunks = (int) ceil($size / 100); $this->assertSame($numberOfChunks, $newChunksCollection->count()); $expectedContent = substr(file_get_contents(__FILE__), 0, 100); $firstChunk = $newChunksCollection->findOne([], ['sort' => ['n' => 1]]); $this->assertNotNull($firstChunk); - $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', 'files_id', $firstChunk); + $this->assertInstanceOf('MongoDB\BSON\ObjectID', $firstChunk->files_id); $this->assertSame((string) $id, (string) $firstChunk->files_id); - $this->assertAttributeSame(0, 'n', $firstChunk); - $this->assertAttributeInstanceOf('MongoDB\BSON\Binary', 'data', $firstChunk); + $this->assertSame(0, $firstChunk->n); + $this->assertInstanceOf('MongoDB\BSON\Binary', $firstChunk->data); $this->assertSame($expectedContent, (string) $firstChunk->data->getData()); } @@ -209,29 +200,24 @@ public function testStoreFileResource() $filename = basename(__FILE__); $record = $newCollection->findOne(); $this->assertNotNull($record); - $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', '_id', $record); + $this->assertInstanceOf('MongoDB\BSON\ObjectID', $record->_id); $this->assertSame((string) $id, (string) $record->_id); - $this->assertObjectHasAttribute('foo', $record); - $this->assertAttributeSame('bar', 'foo', $record); - $this->assertObjectHasAttribute('length', $record); - $this->assertAttributeSame($size, 'length', $record); - $this->assertObjectHasAttribute('chunkSize', $record); - $this->assertAttributeSame(100, 'chunkSize', $record); - $this->assertObjectHasAttribute('md5', $record); - $this->assertAttributeSame($md5, 'md5', $record); - $this->assertObjectHasAttribute('filename', $record); - $this->assertAttributeSame('test.php', 'filename', $record); - - $numberOfChunks = (int)ceil($size / 100); + $this->assertSame('bar', $record->foo); + $this->assertSame($size, $record->length); + $this->assertSame(100, $record->chunkSize); + $this->assertSame($md5, $record->md5); + $this->assertSame('test.php', $record->filename); + + $numberOfChunks = (int) ceil($size / 100); $this->assertSame($numberOfChunks, $newChunksCollection->count()); $expectedContent = substr(file_get_contents(__FILE__), 0, 100); $firstChunk = $newChunksCollection->findOne([], ['sort' => ['n' => 1]]); $this->assertNotNull($firstChunk); - $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', 'files_id', $firstChunk); + $this->assertInstanceOf('MongoDB\BSON\ObjectID', $firstChunk->files_id); $this->assertSame((string) $id, (string) $firstChunk->files_id); - $this->assertAttributeSame(0, 'n', $firstChunk); - $this->assertAttributeInstanceOf('MongoDB\BSON\Binary', 'data', $firstChunk); + $this->assertSame(0, $firstChunk->n); + $this->assertInstanceOf('MongoDB\BSON\Binary', $firstChunk->data); $this->assertSame($expectedContent, (string) $firstChunk->data->getData()); } @@ -260,20 +246,15 @@ public function testStoreUpload() $size = filesize(__FILE__); $record = $newCollection->findOne(); $this->assertNotNull($record); - $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', '_id', $record); + $this->assertInstanceOf('MongoDB\BSON\ObjectID', $record->_id); $this->assertSame((string) $id, (string) $record->_id); - $this->assertObjectHasAttribute('foo', $record); - $this->assertAttributeSame('bar', 'foo', $record); - $this->assertObjectHasAttribute('length', $record); - $this->assertAttributeSame($size, 'length', $record); - $this->assertObjectHasAttribute('chunkSize', $record); - $this->assertAttributeSame(100, 'chunkSize', $record); - $this->assertObjectHasAttribute('md5', $record); - $this->assertAttributeSame($md5, 'md5', $record); - $this->assertObjectHasAttribute('filename', $record); - $this->assertAttributeSame('test.php', 'filename', $record); - - $numberOfChunks = (int)ceil($size / 100); + $this->assertSame('bar', $record->foo); + $this->assertSame($size, $record->length); + $this->assertSame(100, $record->chunkSize); + $this->assertSame($md5, $record->md5); + $this->assertSame('test.php', $record->filename); + + $numberOfChunks = (int) ceil($size / 100); $this->assertSame($numberOfChunks, $newChunksCollection->count()); } @@ -332,7 +313,7 @@ public function testPut() $this->assertSame(1, $newCollection->count()); $size = filesize(__FILE__); - $numberOfChunks = (int)ceil($size / 100); + $numberOfChunks = (int) ceil($size / 100); $this->assertSame($numberOfChunks, $newChunksCollection->count()); } @@ -346,7 +327,7 @@ public function testStoreByteExceptionWhileInsertingRecord() $collection->insert($document); $this->expectException(\MongoGridFSException::class); - $this->expectExceptionMessageRegExp('/Could not store file:.* E11000 duplicate key error .* mongo-php-adapter\.fs\.files/'); + $this->expectErrorMessageMatches('/Could not store file:.* E11000 duplicate key error .* mongo-php-adapter\.fs\.files/'); $this->expectExceptionCode(11000); $collection->storeBytes('foo', ['_id' => $id]); @@ -361,7 +342,7 @@ public function testStoreByteExceptionWhileInsertingChunks() $collection->chunks->insert($document); $this->expectException(\MongoGridFSException::class); - $this->expectExceptionMessageRegExp('/Could not store file:.* E11000 duplicate key error .* mongo-php-adapter\.fs\.chunks/'); + $this->expectErrorMessageMatches('/Could not store file:.* E11000 duplicate key error .* mongo-php-adapter\.fs\.chunks/'); $this->expectExceptionCode(11000); $collection->storeBytes('foo'); @@ -376,7 +357,7 @@ public function testStoreFileExceptionWhileInsertingRecord() $collection->insert($document); $this->expectException(\MongoGridFSException::class); - $this->expectExceptionMessageRegExp('/Could not store file:.* E11000 duplicate key error .* mongo-php-adapter\.fs\.files/'); + $this->expectErrorMessageMatches('/Could not store file:.* E11000 duplicate key error .* mongo-php-adapter\.fs\.files/'); $this->expectExceptionCode(11000); $collection->storeFile(__FILE__, ['_id' => $id]); @@ -391,7 +372,7 @@ public function testStoreFileExceptionWhileInsertingChunks() $collection->chunks->insert($document); $this->expectException(\MongoGridFSException::class); - $this->expectExceptionMessageRegExp('/Could not store file:.* E11000 duplicate key error .* mongo-php-adapter\.fs\.chunks/'); + $this->expectErrorMessageMatches('/Could not store file:.* E11000 duplicate key error .* mongo-php-adapter\.fs\.chunks/'); $this->expectExceptionCode(11000); $collection->storeFile(__FILE__); @@ -406,7 +387,7 @@ public function testStoreFileExceptionWhileUpdatingFileRecord() $collection->insert($document); $this->expectException(\MongoGridFSException::class); - $this->expectExceptionMessageRegExp('/Could not store file:.* E11000 duplicate key error .* mongo-php-adapter\.fs\.files/'); + $this->expectErrorMessageMatches('/Could not store file:.* E11000 duplicate key error .* mongo-php-adapter\.fs\.files/'); $this->expectExceptionCode(11000); $collection->storeFile(fopen(__FILE__, 'r')); diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoIdTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoIdTest.php index 81953bea..1a9bcd32 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoIdTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoIdTest.php @@ -4,6 +4,7 @@ use Alcaeus\MongoDbAdapter\Tests\TestCase; use MongoDB\BSON\ObjectID; +use ReflectionProperty; /** * @author alcaeus @@ -40,12 +41,11 @@ public function testCreateWithString() $this->assertSame(34335, $id->getPID()); } - /** - * @expectedException \MongoException - * @expectedExceptionMessage Invalid object ID - */ public function testCreateWithInvalidStringThrowsMongoException() { + $this->expectException('\MongoException'); + $this->expectExceptionMessage('Invalid object ID'); + new \MongoId('invalid'); } @@ -59,7 +59,7 @@ public function testCreateWithObjectId() $id = new \MongoId($objectId); $this->assertSame($original, (string) $id); - $this->assertAttributeNotSame($objectId, 'objectID', $id); + $this->assertNotSame($objectId, $this->getAttributeValue($id, 'objectID')); } /** @@ -83,4 +83,12 @@ public static function dataIsValid() 'object' => [false, new \stdClass()], ]; } + + private function getAttributeValue(\MongoId $id, $attribute) + { + $property = new ReflectionProperty($id, $attribute); + $property->setAccessible(true); + + return $property->getValue($id); + } } diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoInsertBatchTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoInsertBatchTest.php index 06a47839..a7badfac 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoInsertBatchTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoInsertBatchTest.php @@ -9,7 +9,7 @@ class MongoInsertBatchTest extends TestCase public function testSerialize() { $batch = new \MongoInsertBatch($this->getCollection()); - $this->assertInternalType('string', serialize($batch)); + $this->assertIsString(serialize($batch)); } public function testInsertBatch() @@ -30,8 +30,28 @@ public function testInsertBatch() $this->assertSame(2, $newCollection->count()); $record = $newCollection->findOne(); $this->assertNotNull($record); - $this->assertObjectHasAttribute('foo', $record); - $this->assertAttributeSame('bar', 'foo', $record); + $this->assertSame('bar', $record->foo); + } + + public function testInsertBatchWithoutAck() + { + $batch = new \MongoInsertBatch($this->getCollection()); + + $this->assertTrue($batch->add(['foo' => 'bar'])); + $this->assertTrue($batch->add(['bar' => 'foo'])); + + $expected = [ + 'nInserted' => 0, + 'ok' => true, + ]; + + $this->assertSame($expected, $batch->execute(['w' => 0])); + + $newCollection = $this->getCheckDatabase()->selectCollection('test'); + $this->assertSame(2, $newCollection->count()); + $record = $newCollection->findOne(); + $this->assertNotNull($record); + $this->assertSame('bar', $record->foo); } public function testInsertBatchError() @@ -60,7 +80,7 @@ public function testInsertBatchError() } catch (\MongoWriteConcernException $e) { $this->assertSame('Failed write', $e->getMessage()); $this->assertSame(911, $e->getCode()); - $this->assertArraySubset($expected, $e->getDocument()); + $this->assertMatches($expected, $e->getDocument()); } } } diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoLogTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoLogTest.php index daceb6cb..fda63162 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoLogTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoLogTest.php @@ -8,7 +8,8 @@ class MongoLogTest extends Testcase { public function testSetCallback() { - $foo = function() {}; + $foo = function () { + }; $this->assertTrue(\MongoLog::setCallback($foo)); $this->assertSame($foo, \MongoLog::getCallback()); } diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoRegexTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoRegexTest.php index 6ae20d44..a515bbe7 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoRegexTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoRegexTest.php @@ -13,8 +13,8 @@ class MongoRegexTest extends TestCase public function testCreate() { $regex = new \MongoRegex('/abc/i'); - $this->assertAttributeSame('abc', 'regex', $regex); - $this->assertAttributeSame('i', 'flags', $regex); + $this->assertSame('abc', $regex->regex); + $this->assertSame('i', $regex->flags); $this->assertSame('/abc/i', (string) $regex); @@ -41,7 +41,7 @@ public function testCreateWithBsonType() $bsonRegex = new \MongoDB\BSON\Regex('abc', 'i'); $regex = new \MongoRegex($bsonRegex); - $this->assertAttributeSame('abc', 'regex', $regex); - $this->assertAttributeSame('i', 'flags', $regex); + $this->assertSame('abc', $regex->regex); + $this->assertSame('i', $regex->flags); } } diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoTimestampTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoTimestampTest.php index 16c8f2e4..c9d38ea6 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoTimestampTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoTimestampTest.php @@ -13,8 +13,8 @@ class MongoTimestampTest extends TestCase public function testCreate() { $timestamp = new \MongoTimestamp(1234567890, 987654321); - $this->assertAttributeSame(1234567890, 'sec', $timestamp); - $this->assertAttributeSame(987654321, 'inc', $timestamp); + $this->assertSame(1234567890, $timestamp->sec); + $this->assertSame(987654321, $timestamp->inc); $this->assertSame('1234567890', (string) $timestamp); @@ -38,8 +38,8 @@ public function testCreateWithGlobalInc() $timestamp1 = new \MongoTimestamp(1234567890); $timestamp2 = new \MongoTimestamp(1234567890); - $this->assertAttributeSame(0, 'inc', $timestamp1); - $this->assertAttributeSame(1, 'inc', $timestamp2); + $this->assertSame(0, $timestamp1->inc); + $this->assertSame(1, $timestamp2->inc); } public function testCreateWithBsonTimestamp() @@ -49,8 +49,8 @@ public function testCreateWithBsonTimestamp() $bsonTimestamp = new \MongoDB\BSON\Timestamp(987654321, 1234567890); $timestamp = new \MongoTimestamp($bsonTimestamp); - $this->assertAttributeSame(1234567890, 'sec', $timestamp); - $this->assertAttributeSame(987654321, 'inc', $timestamp); + $this->assertSame(1234567890, $timestamp->sec); + $this->assertSame(987654321, $timestamp->inc); } public function testContructorArgumentOrderDiffers() @@ -63,6 +63,6 @@ public function testContructorArgumentOrderDiffers() $bsonTimestamp = new \MongoDB\BSON\Timestamp(12345, 67890); $timestamp = new \MongoTimestamp(67890, 12345); - $this->assertSame((string) $bsonTimestamp, (string)$timestamp->toBSONType()); + $this->assertSame((string) $bsonTimestamp, (string) $timestamp->toBSONType()); } } diff --git a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoUpdateBatchTest.php b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoUpdateBatchTest.php index 89458dfa..858fe8c7 100644 --- a/tests/Alcaeus/MongoDbAdapter/Mongo/MongoUpdateBatchTest.php +++ b/tests/Alcaeus/MongoDbAdapter/Mongo/MongoUpdateBatchTest.php @@ -9,7 +9,7 @@ class MongoUpdateBatchTest extends TestCase public function testSerialize() { $batch = new \MongoUpdateBatch($this->getCollection()); - $this->assertInternalType('string', serialize($batch)); + $this->assertIsString(serialize($batch)); } public function testUpdateOne() @@ -35,8 +35,7 @@ public function testUpdateOne() $this->assertSame(1, $newCollection->count()); $record = $newCollection->findOne(); $this->assertNotNull($record); - $this->assertObjectHasAttribute('foo', $record); - $this->assertAttributeSame('foo', 'foo', $record); + $this->assertSame('foo', $record->foo); } public function testUpdateOneException() @@ -71,7 +70,7 @@ public function testUpdateOneException() } catch (\MongoWriteConcernException $e) { $this->assertSame('Failed write', $e->getMessage()); $this->assertSame(911, $e->getCode()); - $this->assertArraySubset($expected, $e->getDocument()); + $this->assertMatches($expected, $e->getDocument()); } } @@ -100,8 +99,35 @@ public function testUpdateMany() $this->assertSame(2, $newCollection->count()); $record = $newCollection->findOne(); $this->assertNotNull($record); - $this->assertObjectHasAttribute('foo', $record); - $this->assertAttributeSame('foo', 'foo', $record); + $this->assertSame('foo', $record->foo); + } + + public function testUpdateManyWithoutAck() + { + $collection = $this->getCollection(); + $batch = new \MongoUpdateBatch($collection); + + $document = ['foo' => 'bar']; + $collection->insert($document); + unset($document['_id']); + $collection->insert($document); + + $this->assertTrue($batch->add(['q' => ['foo' => 'bar'], 'u' => ['$set' => ['foo' => 'foo']], 'multi' => true])); + + $expected = [ + 'nMatched' => 0, + 'nModified' => 0, + 'nUpserted' => 0, + 'ok' => true, + ]; + + $this->assertSame($expected, $batch->execute(['w' => 0])); + + $newCollection = $this->getCheckDatabase()->selectCollection('test'); + $this->assertSame(2, $newCollection->count()); + $record = $newCollection->findOne(); + $this->assertNotNull($record); + $this->assertSame('foo', $record->foo); } public function testUpdateManyException() @@ -136,7 +162,7 @@ public function testUpdateManyException() } catch (\MongoWriteConcernException $e) { $this->assertSame('Failed write', $e->getMessage()); $this->assertSame(911, $e->getCode()); - $this->assertArraySubset($expected, $e->getDocument()); + $this->assertMatches($expected, $e->getDocument()); } } @@ -162,7 +188,7 @@ public function testUpsert() ]; $result = $batch->execute(); - $this->assertArraySubset($expected, $result); + $this->assertMatches($expected, $result); $this->assertInstanceOf('MongoId', $result['upserted'][0]['_id']); @@ -171,8 +197,7 @@ public function testUpsert() $this->assertSame(2, $newCollection->count()); $record = $newCollection->findOne(); $this->assertNotNull($record); - $this->assertObjectHasAttribute('foo', $record); - $this->assertAttributeSame('bar', 'foo', $record); + $this->assertSame('bar', $record->foo); } public function testValidateItem() diff --git a/tests/Alcaeus/MongoDbAdapter/TestCase.php b/tests/Alcaeus/MongoDbAdapter/TestCase.php index 153ad167..1250d1e0 100644 --- a/tests/Alcaeus/MongoDbAdapter/TestCase.php +++ b/tests/Alcaeus/MongoDbAdapter/TestCase.php @@ -2,17 +2,29 @@ namespace Alcaeus\MongoDbAdapter\Tests; +use Alcaeus\MongoDbAdapter\Tests\Constraint\Matches; use MongoDB\Client; use PHPUnit\Framework\TestCase as BaseTestCase; +use Symfony\Bridge\PhpUnit\SetUpTearDownTrait; abstract class TestCase extends BaseTestCase { + use SetUpTearDownTrait; + const INDEX_VERSION_1 = 1; const INDEX_VERSION_2 = 2; - protected function tearDown() + private function doTearDown() { $this->getCheckDatabase()->drop(); + + parent::tearDown(); + } + + public function assertMatches($expected, $value, $message = '') + { + $constraint = new Matches($expected, true, true, true); + $this->assertThat($value, $constraint, $message); } /** @@ -20,7 +32,7 @@ protected function tearDown() */ protected function getCheckClient() { - return new Client('mongodb://localhost', ['connect' => true]); + return new Client(MONGODB_URI, ['connect' => true]); } /** @@ -36,7 +48,7 @@ protected function getCheckDatabase() * @param array|null $options * @return \MongoClient */ - protected function getClient($options = null, $uri = 'mongodb://localhost') + protected function getClient($options = null, $uri = MONGODB_URI) { $args = [$uri]; if ($options !== null) { @@ -139,7 +151,7 @@ protected function checkFailPoint() /* command not found */ if ($e->getCode() == 59) { $this->markTestSkipped( - 'This test require the mongo daemon to be started with the test flag: --setParameter enableTestCommands=1' + 'This test require the mongo daemon to be started with the test flag: --setParameter enableTestCommands=1' ); } } @@ -153,18 +165,19 @@ protected function failMaxTimeMS() /** * @param bool $condition */ - protected function skipTestUnless($condition) + protected function skipTestUnless($condition, $message = null) { - $this->skipTestIf(! $condition); + $this->skipTestIf(! $condition, $message); } /** * @param bool $condition + * @param string|null $message */ - protected function skipTestIf($condition) + protected function skipTestIf($condition, $message = null) { if ($condition) { - $this->markTestSkipped('Test only applies when running against mongo-php-adapter'); + $this->markTestSkipped($message !== null ? $message : 'Test only applies when running against mongo-php-adapter'); } } @@ -183,7 +196,11 @@ protected function getServerVersion() protected function getFeatureCompatibilityVersion() { $featureCompatibilityVersion = $this->getClient()->selectDB('admin')->command(['getParameter' => true, 'featureCompatibilityVersion' => true]); - return isset($featureCompatibilityVersion['featureCompatibilityVersion']) ? $featureCompatibilityVersion['featureCompatibilityVersion'] : '3.2'; + if (! isset($featureCompatibilityVersion['featureCompatibilityVersion'])) { + return '3.2'; + } + + return isset($featureCompatibilityVersion['featureCompatibilityVersion']['version']) ? $featureCompatibilityVersion['featureCompatibilityVersion']['version'] : $featureCompatibilityVersion['featureCompatibilityVersion']; } /** @@ -199,6 +216,6 @@ protected function getDefaultIndexVersion() // Check featureCompatibilityFlag $compatibilityVersion = $this->getFeatureCompatibilityVersion(); - return $compatibilityVersion === '3.4' ? self::INDEX_VERSION_2 : self::INDEX_VERSION_1; + return version_compare($compatibilityVersion, '3.4', '>=') ? self::INDEX_VERSION_2 : self::INDEX_VERSION_1; } } diff --git a/tests/Alcaeus/MongoDbAdapter/TypeConverterTest.php b/tests/Alcaeus/MongoDbAdapter/TypeConverterTest.php index 635c0c25..7bab4a7c 100644 --- a/tests/Alcaeus/MongoDbAdapter/TypeConverterTest.php +++ b/tests/Alcaeus/MongoDbAdapter/TypeConverterTest.php @@ -3,7 +3,6 @@ namespace Alcaeus\MongoDbAdapter\Tests; use MongoDB\BSON; -use MongoDB\Driver\Exception; use Alcaeus\MongoDbAdapter\TypeConverter; use MongoDB\Model\BSONDocument; @@ -38,6 +37,14 @@ public static function converterData() 'nestedArrays' => [ [['foo' => 'bar']], [new BSONDocument(['foo' => 'bar'])] ], + 'dateTime' => [ + \DateTime::createFromFormat('Y-m-d\TH:i:sP', '2021-06-30T12:34:56-7'), + new BSONDocument([ + 'date' => '2021-06-30 12:34:56.000000', + 'timezone_type' => 1, + 'timezone' => '-07:00', + ]), + ], ]; }