diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 12495e9..8deed9a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: ['8.0', '8.1', '8.2', '8.3'] # add PHP versions as required + php-versions: ['8.3'] # add PHP versions as required steps: - name: Checkout repository diff --git a/Dockerfile-php-8.0 b/Dockerfile-php-8.0 deleted file mode 100755 index 0d3c013..0000000 --- a/Dockerfile-php-8.0 +++ /dev/null @@ -1,57 +0,0 @@ -FROM composer:2.0 as composer - -ARG TESTING=false -ENV TESTING=$TESTING - -WORKDIR /usr/local/src/ - -COPY composer.lock /usr/local/src/ -COPY composer.json /usr/local/src/ - -RUN composer update --ignore-platform-reqs --optimize-autoloader \ - --no-plugins --no-scripts --prefer-dist - -FROM appwrite/utopia-base:php-8.0-0.1.0 as compile - -ENV PHP_MONGO_VERSION=1.11.1 - -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - -RUN \ - apk update \ - && apk add --no-cache postgresql-libs postgresql-dev make automake autoconf gcc g++ git \ - && docker-php-ext-install opcache pgsql pdo_mysql pdo_pgsql \ - && rm -rf /var/cache/apk/* - -## MongoDB Extension -FROM compile AS mongodb -RUN \ - git clone --depth 1 --branch $PHP_MONGO_VERSION https://github.com/mongodb/mongo-php-driver.git \ - && cd mongo-php-driver \ - && git submodule update --init \ - && phpize \ - && ./configure \ - && make && make install - -FROM compile as final - -LABEL maintainer="team@appwrite.io" - -WORKDIR /usr/src/code - -RUN echo extension=mongodb.so >> /usr/local/etc/php/conf.d/mongodb.ini - -RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" - -RUN echo "opcache.enable_cli=1" >> $PHP_INI_DIR/php.ini - -RUN echo "memory_limit=1024M" >> $PHP_INI_DIR/php.ini - -COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor -COPY --from=mongodb /usr/local/lib/php/extensions/no-debug-non-zts-20200930/mongodb.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/ - -# Add Source Code -COPY . /usr/src/code - -CMD [ "tail", "-f", "/dev/null" ] - diff --git a/Dockerfile-php-8.1 b/Dockerfile-php-8.1 deleted file mode 100644 index 97bfddc..0000000 --- a/Dockerfile-php-8.1 +++ /dev/null @@ -1,57 +0,0 @@ -FROM composer:2.0 as composer - -ARG TESTING=false -ENV TESTING=$TESTING - -WORKDIR /usr/local/src/ - -COPY composer.lock /usr/local/src/ -COPY composer.json /usr/local/src/ - -RUN composer update --ignore-platform-reqs --optimize-autoloader \ - --no-plugins --no-scripts --prefer-dist - -FROM appwrite/utopia-base:php-8.1-0.1.0 as compile - -ENV PHP_MONGO_VERSION=1.11.1 - -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - -RUN \ - apk update \ - && apk add --no-cache postgresql-libs postgresql-dev make automake autoconf gcc g++ git \ - && docker-php-ext-install opcache pgsql pdo_mysql pdo_pgsql \ - && rm -rf /var/cache/apk/* - -## MongoDB Extension -FROM compile AS mongodb -RUN \ - git clone --depth 1 --branch $PHP_MONGO_VERSION https://github.com/mongodb/mongo-php-driver.git \ - && cd mongo-php-driver \ - && git submodule update --init \ - && phpize \ - && ./configure \ - && make && make install - -FROM compile as final - -LABEL maintainer="team@appwrite.io" - -WORKDIR /usr/src/code - -RUN echo extension=mongodb.so >> /usr/local/etc/php/conf.d/mongodb.ini - -RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" - -RUN echo "opcache.enable_cli=1" >> $PHP_INI_DIR/php.ini - -RUN echo "memory_limit=1024M" >> $PHP_INI_DIR/php.ini - -COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor -COPY --from=mongodb /usr/local/lib/php/extensions/no-debug-non-zts-20210902/mongodb.so /usr/local/lib/php/extensions/no-debug-non-zts-20210902/ - -# Add Source Code -COPY . /usr/src/code - -CMD [ "tail", "-f", "/dev/null" ] - diff --git a/Dockerfile-php-8.2 b/Dockerfile-php-8.2 deleted file mode 100644 index 214e372..0000000 --- a/Dockerfile-php-8.2 +++ /dev/null @@ -1,57 +0,0 @@ -FROM composer:2.0 as composer - -ARG TESTING=false -ENV TESTING=$TESTING - -WORKDIR /usr/local/src/ - -COPY composer.lock /usr/local/src/ -COPY composer.json /usr/local/src/ - -RUN composer update --ignore-platform-reqs --optimize-autoloader \ - --no-plugins --no-scripts --prefer-dist - -FROM appwrite/utopia-base:php-8.2-0.1.0 as compile - -ENV PHP_MONGO_VERSION=1.11.1 - -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - -RUN \ - apk update \ - && apk add --no-cache postgresql-libs postgresql-dev make automake autoconf gcc g++ git \ - && docker-php-ext-install opcache pgsql pdo_mysql pdo_pgsql \ - && rm -rf /var/cache/apk/* - -## MongoDB Extension -FROM compile AS mongodb -RUN \ - git clone --depth 1 --branch $PHP_MONGO_VERSION https://github.com/mongodb/mongo-php-driver.git \ - && cd mongo-php-driver \ - && git submodule update --init \ - && phpize \ - && ./configure \ - && make && make install - -FROM compile as final - -LABEL maintainer="team@appwrite.io" - -WORKDIR /usr/src/code - -RUN echo extension=mongodb.so >> /usr/local/etc/php/conf.d/mongodb.ini - -RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" - -RUN echo "opcache.enable_cli=1" >> $PHP_INI_DIR/php.ini - -RUN echo "memory_limit=1024M" >> $PHP_INI_DIR/php.ini - -COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor -COPY --from=mongodb /usr/local/lib/php/extensions/no-debug-non-zts-20220829/mongodb.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/ - -# Add Source Code -COPY . /usr/src/code - -CMD [ "tail", "-f", "/dev/null" ] - diff --git a/Dockerfile-php-8.3 b/Dockerfile-php-8.3 index cb031e1..d65b1c8 100644 --- a/Dockerfile-php-8.3 +++ b/Dockerfile-php-8.3 @@ -13,7 +13,7 @@ RUN composer update --ignore-platform-reqs --optimize-autoloader \ FROM appwrite/utopia-base:php-8.3-0.1.0 as compile -ENV PHP_MONGO_VERSION=1.11.1 +ENV PHP_MONGO_VERSION=2.1.1 RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone diff --git a/composer.json b/composer.json index caf2163..8ca49e8 100755 --- a/composer.json +++ b/composer.json @@ -29,14 +29,14 @@ }, "require": { "php": ">=8.0", - "ext-mongodb": "*", - "mongodb/mongodb": "^1.21" + "ext-mongodb": "2.1.1", + "mongodb/mongodb": "2.1.0" }, "require-dev": { "fakerphp/faker": "^1.14", "phpunit/phpunit": "^9.4", "swoole/ide-helper": "4.8.0", "laravel/pint": "1.2.*", - "phpstan/phpstan": "1.8.*" + "phpstan/phpstan": "2.1.*" } } diff --git a/composer.lock b/composer.lock index 52cee61..444eb47 100644 --- a/composer.lock +++ b/composer.lock @@ -4,27 +4,28 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "104758a98a297851d8985a60cbfa65bc", + "content-hash": "66c3842bf94f4946d1277b41cf28219d", "packages": [ { "name": "mongodb/mongodb", - "version": "1.21.1", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/mongodb/mongo-php-library.git", - "reference": "37bc8df3a67ddf8380704a5ba5dbd00e92ec1f6a" + "reference": "3bbe7ba9578724c7e1f47fcd17c881c0995baaad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mongodb/mongo-php-library/zipball/37bc8df3a67ddf8380704a5ba5dbd00e92ec1f6a", - "reference": "37bc8df3a67ddf8380704a5ba5dbd00e92ec1f6a", + "url": "https://api.github.com/repos/mongodb/mongo-php-library/zipball/3bbe7ba9578724c7e1f47fcd17c881c0995baaad", + "reference": "3bbe7ba9578724c7e1f47fcd17c881c0995baaad", "shasum": "" }, "require": { "composer-runtime-api": "^2.0", - "ext-mongodb": "^1.21.0", + "ext-mongodb": "^2.1", "php": "^8.1", - "psr/log": "^1.1.4|^2|^3" + "psr/log": "^1.1.4|^2|^3", + "symfony/polyfill-php85": "^1.32" }, "replace": { "mongodb/builder": "*" @@ -78,9 +79,9 @@ ], "support": { "issues": "https://github.com/mongodb/mongo-php-library/issues", - "source": "https://github.com/mongodb/mongo-php-library/tree/1.21.1" + "source": "https://github.com/mongodb/mongo-php-library/tree/2.1.0" }, - "time": "2025-02-28T17:24:20+00:00" + "time": "2025-05-23T10:48:05+00:00" }, { "name": "psr/log", @@ -131,6 +132,82 @@ "source": "https://github.com/php-fig/log/tree/3.0.2" }, "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd", + "reference": "6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-02T08:40:52+00:00" } ], "packages-dev": [ @@ -571,20 +648,20 @@ }, { "name": "phpstan/phpstan", - "version": "1.8.11", + "version": "2.1.19", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "46e223dd68a620da18855c23046ddb00940b4014" + "reference": "473a8c30e450d87099f76313edcbb90852f9afdf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/46e223dd68a620da18855c23046ddb00940b4014", - "reference": "46e223dd68a620da18855c23046ddb00940b4014", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/473a8c30e450d87099f76313edcbb90852f9afdf", + "reference": "473a8c30e450d87099f76313edcbb90852f9afdf", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -609,8 +686,11 @@ "static analysis" ], "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.8.11" + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" }, "funding": [ { @@ -620,13 +700,9 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2022-10-24T15:45:13+00:00" + "time": "2025-07-21T19:58:24+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2241,7 +2317,7 @@ "prefer-lowest": false, "platform": { "php": ">=8.0", - "ext-mongodb": "*" + "ext-mongodb": "2.1.1" }, "platform-dev": [], "plugin-api-version": "2.2.0" diff --git a/src/Auth.php b/src/Auth.php index 2cc1435..c2c988f 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -10,7 +10,7 @@ class Auth private string $secret; private string $authzid; private string $gs2Header; - private string $cnonce; + private ?string $cnonce = null; private string $firstMessageBare; private string $saltedPassword; private string $authMessage; diff --git a/src/Client.php b/src/Client.php index 0a9b3d1..29d7ce1 100644 --- a/src/Client.php +++ b/src/Client.php @@ -2,7 +2,8 @@ namespace Utopia\Mongo; -use MongoDB\BSON; +use MongoDB\BSON\Document; +use MongoDB\BSON\ObjectId; use Swoole\Client as SwooleClient; use Swoole\Coroutine\Client as CoroutineClient; use stdClass; @@ -36,6 +37,10 @@ class Client public const COMMAND_AGGREGATE = "aggregate"; public const COMMAND_DISTINCT = "distinct"; public const COMMAND_MAP_REDUCE = "mapReduce"; + public const COMMAND_START_SESSION = "startSession"; + public const COMMAND_COMMIT_TRANSACTION = "commitTransaction"; + public const COMMAND_ABORT_TRANSACTION = "abortTransaction"; + public const COMMAND_END_SESSIONS = "endSessions"; /** @@ -137,7 +142,7 @@ public function query(array $command, ?string $db = null): stdClass|array|int '$db' => $db ?? $this->database, ]); - $sections = BSON\fromPHP($params); + $sections = Document::fromPHP($params); $message = pack('V*', 21 + strlen($sections), $this->id, 0, 2013, 0) . "\0" . $sections; return $this->send($message); } @@ -197,11 +202,11 @@ private function receive(): stdClass|array|int (!isset($responseLength)) || ($receivedLength < $responseLength) ); - /** - * @var stdClass $result - */ - $result = BSON\toPHP(substr($res, 21, $responseLength - 21)); - + $bsonString = substr($res, 21, $responseLength - 21); + $result = Document::fromBSON($bsonString)->toPHP(); + if (is_array($result)) { + $result = (object) $result; + } if (property_exists($result, "writeErrors")) { // Throws Utopia\Mongo\Exception throw new Exception( @@ -435,7 +440,7 @@ public function insert(string $collection, array $document, array $options = []) $docObj->{$key} = $value; } - $docObj->_id ??= new BSON\ObjectId(); + $docObj->_id ??= new ObjectId(); $this->query(array_merge([ self::COMMAND_INSERT => $collection, @@ -460,7 +465,7 @@ public function insertMany(string $collection, array $documents, array $options $docObj->{$key} = $value; } - $docObj->_id ??= new BSON\ObjectId(); + $docObj->_id ??= new ObjectId(); $docObjs[] = $docObj; } @@ -534,18 +539,20 @@ public function update(string $collection, array $where = [], array $updates = [ } /** - * Perform multiple upserts in a single update command (wire protocol batch). - * Each operation should have 'filter' and 'update' keys. + * Insert, or update, document(s) with support for bulk operations. + * https://docs.mongodb.com/manual/reference/command/update/#syntax * * @param string $collection - * @param array $operations + * @param array $operations Array of operations, each with 'filter' and 'update' keys * @param array $options + * * @return self * @throws Exception */ - public function bulkUpsert(string $collection, array $operations, array $options = []): self + public function upsert(string $collection, array $operations, array $options = []): self { $updates = []; + foreach ($operations as $op) { $cleanUpdate = []; foreach ($op['update'] as $k => $v) { @@ -554,12 +561,16 @@ public function bulkUpsert(string $collection, array $operations, array $options } } - $updates[] = [ + $updateOperation = [ 'q' => $op['filter'], 'u' => $cleanUpdate, 'upsert' => true, + 'multi' => isset($op['multi']) ? $op['multi'] : false, ]; + + $updates[] = $updateOperation; } + $this->query( array_merge( [ @@ -572,48 +583,7 @@ public function bulkUpsert(string $collection, array $operations, array $options return $this; } - /** - * Insert, or update, a document/s. - * https://docs.mongodb.com/manual/reference/command/update/#syntax - * - * @param string $collection - * @param array $where - * @param array $updates - * @param array $options - * - * @return Client - * @throws Exception - */ - public function upsert(string $collection, array $where = [], array $updates = [], array $options = []): self - { - $cleanUpdates = []; - - foreach ($updates as $k => $v) { - if (\is_null($v)) { - continue; - } - $cleanUpdates[$k] = $v; - } - - - $this->query( - array_merge( - [ - 'update' => $collection, - 'updates' => [ - [ - 'q' => ['_uid' => $where['_uid']], - 'u' => ['$set' => $cleanUpdates], - ] - ], - ], - $options - ) - ); - - return $this; - } /** * Find a document/s. @@ -754,6 +724,100 @@ public function aggregate(string $collection, array $pipeline): stdClass ]); } + /** + * Start a new logical session. Returns the session id object.. + * + * @return object + * @throws Exception + */ + public function startSession(): object + { + $result = $this->query([ + self::COMMAND_START_SESSION => 1 + ], 'admin'); + + return $result->id->id; + } + + /** + * Commit a transaction. + * + * @param array $lsid + * @param int $txnNumber + * @param bool $autocommit + * @return mixed + * @throws Exception + */ + public function commitTransaction(array $lsid, int $txnNumber, bool $autocommit = false) + { + $txnNumber = new \MongoDB\BSON\Int64($txnNumber); + + $result = $this->query([ + self::COMMAND_COMMIT_TRANSACTION => 1, + 'lsid' => $lsid, + 'txnNumber' => $txnNumber, + 'autocommit' => $autocommit + ], 'admin'); + + // End the session after successful commit + $this->endSessions([$lsid]); + + return $result; + } + + /** + * Abort (rollback) a transaction. + * + * @param array $lsid + * @param int $txnNumber + * @param bool $autocommit + * @return mixed + * @throws Exception + */ + public function abortTransaction(array $lsid, int $txnNumber, bool $autocommit = false) + { + $txnNumber = new \MongoDB\BSON\Int64($txnNumber); + + $result = $this->query([ + self::COMMAND_ABORT_TRANSACTION => 1, + 'lsid' => $lsid, + 'txnNumber' => $txnNumber, + 'autocommit' => $autocommit + ], 'admin'); + + // End the session after successful rollback + $this->endSessions([$lsid]); + + return $result; + } + + /** + * End sessions. + * + * @param array $lsids + * @param array $options + * @return mixed + * @throws Exception + */ + public function endSessions(array $lsids, array $options = []) + { + // Extract session IDs from the format ['id' => sessionId] and format as objects + $sessionIds = array_map(function ($lsid) { + $sessionId = $lsid['id'] ?? $lsid; + return ['id' => $sessionId]; + }, $lsids); + + return $this->query( + array_merge( + [ + self::COMMAND_END_SESSIONS => $sessionIds, + ], + $options + ), + 'admin' + ); + } + /** * Convert an assoc array to an object (stdClass). * @@ -822,4 +886,26 @@ private function cleanFilters($filters): array return $cleanedFilters; } + + private ?bool $replicaSet = null; + + /** + * Check if MongoDB is running as a replica set. + * + * @return bool True if this is a replica set, false if standalone + * @throws Exception + */ + public function isReplicaSet(): bool + { + if ($this->replicaSet !== null) { + return $this->replicaSet; + } + + $result = $this->query([ + 'isMaster' => 1, + ], 'admin'); + + $this->replicaSet = property_exists($result, 'setName'); + return $this->replicaSet; + } } diff --git a/tests/MongoTest.php b/tests/MongoTest.php index 6ab8325..aba9be3 100644 --- a/tests/MongoTest.php +++ b/tests/MongoTest.php @@ -226,7 +226,7 @@ public function testExceedTimeException() } - public function testBulkUpsert() + public function testUpsert() { $this->getDatabase()->insert( 'movies_upsert', @@ -238,7 +238,7 @@ public function testBulkUpsert() ] ); - $this->getDatabase()->bulkUpsert('movies_upsert', [ + $this->getDatabase()->upsert('movies_upsert', [ [ 'filter' => ['name' => 'Gone with the wind'], 'update' => [ @@ -255,7 +255,6 @@ public function testBulkUpsert() ]); $documents = $this->getDatabase()->find('movies_upsert')->cursor->firstBatch ?? []; - var_dump($documents); self::assertCount(2, $documents); self::assertEquals(4, $documents[0]->counter); self::assertEquals('The godfather 2', $documents[1]->name);