diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..cb199a2a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,23 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +# Folders +/.github export-ignore +/docs export-ignore +/tests export-ignore +# Files +/.coveralls.yml export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/CODE_OF_CONDUCT.md export-ignore +/composer.lock export-ignore +/docker-compose.override.yml.example export-ignore +/docker-compose.yml export-ignore +/Makefile export-ignore +/mkdocs.yml export-ignore +/phpcs.xml export-ignore +/phpunit.xml export-ignore +/phpunit.xml.dist export-ignore +/sami.php export-ignore diff --git a/.github/pre-commit b/.github/pre-commit new file mode 100755 index 00000000..a6cfe6af --- /dev/null +++ b/.github/pre-commit @@ -0,0 +1,52 @@ +#!/bin/bash + +STAGED_FILES_CMD=`git diff --cached --name-only --diff-filter=ACMR | grep \.php` + +# Determine if a file list is passed +if [ "$#" -eq 1 ] +then + oIFS=$IFS + IFS=' + ' + SFILES="$1" + IFS=$oIFS +fi +SFILES=${SFILES:-$STAGED_FILES_CMD} + +# Fix path for docker +for FILE in $SFILES +do + FILES="$FILES $FILE" +done + +if [ "$FILES" != "" ] +then + echo -e "\033[1;33m"Running Code Sniffer..."\033[0m" + docker-compose run --rm --no-deps -T php vendor/bin/phpcs $FILES + + if [ $? != 0 ] + then + # Allows us to read user input below, assigns stdin to keyboard + exec < /dev/tty + + read -p "There are some Coding Standards violations. Do you want to fix the auto-fixable ones? (Yes) " choice + [ "$choice" = "" ] && choice='Y' + + case ${choice:0:1} in + y|Y ) + echo -e "\033[1;33m"Running Code Beautifier..."\033[0m" + docker-compose run --rm --no-deps -T php vendor/bin/phpcbf $FILES + echo -e "\033[0;32m"Done. Please add the fixes before commit."\033[0m" + + exit 1 + ;; + * ) + echo -e "\033[41m"Please, fix the Coding Standards violations before commit."\033[0m" + + exit 1 + ;; + esac + fi +fi + +exit $? diff --git a/.gitignore b/.gitignore index fd9ae985..097df324 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /.coverage /build /site -composer.lock +.phpcs-cache +docker-compose.override.yml diff --git a/.styleci.yml b/.styleci.yml deleted file mode 100644 index 974f5fa5..00000000 --- a/.styleci.yml +++ /dev/null @@ -1 +0,0 @@ -preset: symfony diff --git a/.travis.yml b/.travis.yml index 7728915f..6fb3df30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,26 @@ language: php php: - - 7.0 - 7.1 - 7.2 + - 7.3 + +cache: + directories: + - $HOME/.composer/cache + +services: mongodb before_install: - - if [[ $TRAVIS_PHP_VERSION != 7.2 ]]; then pecl install mongodb; fi + - if [[ $(phpenv version-name) = "7.1" ]]; then pecl install mongodb; fi - echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini before_script: - composer install --no-interaction script: - - mkdir -p build/logs - - phpunit -c phpunit.xml.dist && make sniff + - vendor/bin/phpcs + - vendor/bin/phpunit -c phpunit.xml.dist -after_script: - - php vendor/bin/coveralls -v +after_success: + - if [[ $(phpenv version-name) = "7.2" ]]; then php vendor/bin/php-coveralls -v; fi diff --git a/LICENSE b/LICENSE index 613682dd..bedd0a02 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2013-2017 Leroy Merlin Brasil +Copyright (c) Leroy Merlin Brazil Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index cc002d00..4461d871 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ sniff: - docker-compose run --rm php vendor/bin/phpcs ./src --standard='./coding_standard.xml' -n + docker-compose run --rm php vendor/bin/phpcs phpunit: docker-compose run --rm php vendor/bin/phpunit diff --git a/README.md b/README.md index b7632949..6ad8a58a 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,45 @@ -# Mongolid ODM for MongoDB (PHP7) +# Mongolid ODM for MongoDB -> Easy, powerful and ultrafast ODM for PHP7 build on top of the [new mongodb driver](https://docs.mongodb.org/ecosystem/drivers/php/). +

Mongolid

-![Mongolid](https://user-images.githubusercontent.com/1991286/28967747-fe5c258a-78f2-11e7-91c7-8850ffb32004.png) +

+Build Status +Coverage Status +Latest Stable Version +Total Downloads +License +

-Mongolid supports both **ActiveRecord** and **DataMapper** patterns. **You choose! (:** +## About Mongolid +Easy, powerful and ultrafast ODM for PHP 7.1+ build on top of the [new mongodb driver](https://docs.mongodb.org/ecosystem/drivers/php/). -[![Build Status](https://travis-ci.org/leroy-merlin-br/mongolid.svg?branch=master)](https://travis-ci.org/leroy-merlin-br/mongolid) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/cc45e93bb0d0413d9e0355c7377d4d33)](https://www.codacy.com/app/zizaco/mongolid?utm_source=github.com&utm_medium=referral&utm_content=leroy-merlin-br/mongolid&utm_campaign=Badge_Grade) -[![StyleCI](https://styleci.io/repos/9799450/shield?branch=master)](https://styleci.io/repos/9799450) -[![Coverage Status](https://coveralls.io/repos/github/leroy-merlin-br/mongolid/badge.svg?branch=master)](https://coveralls.io/github/leroy-merlin-br/mongolid?branch=master) -[![Latest Stable Version](https://poser.pugx.org/leroy-merlin-br/mongolid/v/stable)](https://packagist.org/packages/leroy-merlin-br/mongolid) -[![Total Downloads](https://poser.pugx.org/leroy-merlin-br/mongolid/downloads)](https://packagist.org/packages/leroy-merlin-br/mongolid) -[![Latest Unstable Version](https://poser.pugx.org/leroy-merlin-br/mongolid/v/unstable)](https://packagist.org/packages/leroy-merlin-br/mongolid) -[![License](https://poser.pugx.org/leroy-merlin-br/mongolid/license)](https://packagist.org/packages/leroy-merlin-br/mongolid) +Mongolid supports **ActiveRecord** pattern. -[![SensioLabsInsight](https://insight.sensiolabs.com/projects/25636a94-9a5d-4438-bd5e-9f9694104529/small.png)](https://insight.sensiolabs.com/projects/25636a94-9a5d-4438-bd5e-9f9694104529) - - ## Introduction - Mongolid ODM (Object Document Mapper) provides a beautiful, simple implementation for working with MongoDB. Each database collection can have a corresponding "Model" which is used to interact with that collection. -> **Note:** If you are working with Laravel, take a look at [mongolid-laravel repository](https://github.com/leroy-merlin-br/mongolid-laravel). - - -## Installation - -You can install library through Composer: - -``` -$ composer require leroy-merlin-br/mongolid -``` +**Note:** If you are working with Laravel, take a look at [mongolid-laravel repository](https://github.com/leroy-merlin-br/mongolid-laravel). -### Requirements - -- PHP**7** +## Requirements +- PHP **7.1** or superior - [MongoDB Driver](http://php.net/manual/en/set.mongodb.php) -> **Note:** If you are looking for the old PHP 5.x version, head to the [v0.8 branch](https://github.com/leroy-merlin-br/mongolid/tree/v0.8-dev). - -## [Read the Docs: leroy-merlin-br.github.com/mongolid](http://leroy-merlin-br.github.com/mongolid) -[![Mongolid Docs](https://dl.dropboxusercontent.com/u/12506137/libs_bundles/MongolidDocs.png)](http://leroy-merlin-br.github.com/mongolid) - - -## Troubleshooting - -**"PHP Fatal error: Class 'MongoDB\Client' not found in ..."** - -The `MongoDB\Client` class is contained in the [**new** MongoDB driver](http://pecl.php.net/package/mongodb) for PHP. [Here is an installation guide](http://www.php.net/manual/en/mongodb.installation.php). The driver is a PHP extension written in C and maintained by [MongoDB](https://mongodb.com). Mongolid and most other MongoDB PHP libraries utilize it in order to be fast and reliable. - -**"Class 'MongoDB\Client' not found in ..." in CLI persists even with MongoDB driver installed.** - -Make sure that the **php.ini** file used in the CLI environment includes the MongoDB extension. In some systems, the default PHP installation uses different **.ini** files for the web and CLI environments. - -Run `php -i | grep 'Configuration File'` in a terminal to check the **.ini** that is being used. - -To check if PHP in the CLI environment is importing the driver properly run `php -i | grep -i 'mongo'` in your terminal. You should get output similar to: +## Installation +You can install the library through Composer: ``` -$ php -i | grep -i 'mongo' -MongoDB support => enabled -MongoDB extension version => 1.2.8 -MongoDB extension stability => stable -libmongoc bundled version => 1.5.5 +$ composer require leroy-merlin-br/mongolid ``` -**"This package requires php >=7.0 but your PHP version (X.X.X) does not satisfy that requirement."** - -The new (and improved) version 2.0 of Mongolid requires php7. If you are looking for the old PHP 5.x version, head to the [v0.8 branch](https://github.com/leroy-merlin-br/mongolid/tree/v0.8-dev). +## Documentation +You can access the full documentation [here](http://leroy-merlin-br.github.com/mongolid). - ## License +Mongolid is free software distributed under the terms of the [MIT license](LICENSE). -Mongolid is free software distributed under the terms of the [MIT license](http://opensource.org/licenses/MIT) - - ## Additional information +Made with ❤ by [Leroy Merlin Brazil](https://github.com/leroy-merlin-br) and [all contributors](https://github.com/leroy-merlin-br/mongolid/graphs/contributors). -Mongolid was proudly built by the [Leroy Merlin Brazil](https://github.com/leroy-merlin-br) team. [See all the contributors](https://github.com/leroy-merlin-br/mongolid/graphs/contributors). - -Any questions, feel free to contact us. +If you have any questions, feel free to contact us. -Any issues, please [report here](https://github.com/Zizaco/mongolid) +If you any issues, please [report here](https://github.com/leroy-merlin-br/mongolid/issues). diff --git a/bootstrap/bootstrap.php b/bootstrap/bootstrap.php deleted file mode 100644 index 50031e64..00000000 --- a/bootstrap/bootstrap.php +++ /dev/null @@ -1,12 +0,0 @@ - - - Extends PSR-2 making sure that the docblocks are well written - - - - diff --git a/composer.json b/composer.json index 1ccda769..ffd4e9be 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "leroy-merlin-br/mongolid", "description": "Easy, powerful and ultrafast ODM for PHP and MongoDB.", - "keywords": ["odm","mongodb","nosql"], + "keywords": ["odm", "mongodb", "mongo", "nosql", "active record", "laravel"], "license": "MIT", "authors": [ { @@ -11,31 +11,45 @@ { "name": "Guilherme Guitte", "email": "guilherme.guitte@gmail.com" + }, + { + "name": "Boitatá", + "email": "boitata@leroymerlin.com.br" } ], "require": { - "php": ">=7.0", - "mongodb/mongodb": "^1.3", + "php": ">=7.1", + "ext-mongodb": "*", + "mongodb/mongodb": "^1.4", "illuminate/container": "^5.4" }, "require-dev": { - "mockery/mockery": "^1.0", - "satooshi/php-coveralls": "^1.0", - "phpunit/phpunit": "^6.4", - "squizlabs/php_codesniffer": "^2.0", - "sami/sami": "^4.0" + "leroy-merlin-br/coding-standard": "^0.1", + "mockery/mockery": "^1.2", + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^6.5", + "sami/sami": "^4.1" }, "autoload": { "psr-4": { - "Mongolid\\": "src/Mongolid" - }, - "classmap": [ - "tests/TestCase.php" - ] + "Mongolid\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Mongolid\\": "tests/Unit", + "Mongolid\\Tests\\": "tests" + } }, "extra": { "branch-alias": { - "dev-master": "v2.1.x-dev" + "dev-master": "v3.0.x-dev" } + }, + "config": { + "sort-packages": true + }, + "suggest": { + "leroy-merlin-br/mongolid-laravel": "Easy, powerful and ultrafast MongoDB ODM for Laravel." } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..3b88b95e --- /dev/null +++ b/composer.lock @@ -0,0 +1,3401 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "7061baa6f32a4285b8cf6eddf36c5150", + "packages": [ + { + "name": "doctrine/inflector", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5527a48b7313d15261292c149e55e26eae771b0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5527a48b7313d15261292c149e55e26eae771b0a", + "reference": "5527a48b7313d15261292c149e55e26eae771b0a", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Common String Manipulations with regard to casing and singular/plural rules.", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "inflection", + "pluralize", + "singularize", + "string" + ], + "time": "2018-01-09T20:05:19+00:00" + }, + { + "name": "illuminate/container", + "version": "v5.7.9", + "source": { + "type": "git", + "url": "https://github.com/illuminate/container.git", + "reference": "73cde7bd4985eefb1d468a745e1d50d03e276121" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/container/zipball/73cde7bd4985eefb1d468a745e1d50d03e276121", + "reference": "73cde7bd4985eefb1d468a745e1d50d03e276121", + "shasum": "" + }, + "require": { + "illuminate/contracts": "5.7.*", + "illuminate/support": "5.7.*", + "php": "^7.1.3", + "psr/container": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.7-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Container\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Container package.", + "homepage": "https://laravel.com", + "time": "2018-10-07T15:52:17+00:00" + }, + { + "name": "illuminate/contracts", + "version": "v5.7.9", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "64df81d3382d876f1c1d3d5481d89c93b61b8279" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/64df81d3382d876f1c1d3d5481d89c93b61b8279", + "reference": "64df81d3382d876f1c1d3d5481d89c93b61b8279", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "psr/container": "^1.0", + "psr/simple-cache": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.7-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "https://laravel.com", + "time": "2018-10-08T13:34:14+00:00" + }, + { + "name": "illuminate/support", + "version": "v5.7.9", + "source": { + "type": "git", + "url": "https://github.com/illuminate/support.git", + "reference": "ea95697233b06650382eb0f5798be22b4e520dea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/support/zipball/ea95697233b06650382eb0f5798be22b4e520dea", + "reference": "ea95697233b06650382eb0f5798be22b4e520dea", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^1.1", + "ext-mbstring": "*", + "illuminate/contracts": "5.7.*", + "nesbot/carbon": "^1.26.3", + "php": "^7.1.3" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "suggest": { + "illuminate/filesystem": "Required to use the composer class (5.7.*).", + "moontoast/math": "Required to use ordered UUIDs (^1.1).", + "ramsey/uuid": "Required to use Str::uuid() (^3.7).", + "symfony/process": "Required to use the composer class (^4.1).", + "symfony/var-dumper": "Required to use the dd function (^4.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.7-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + }, + "files": [ + "helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Support package.", + "homepage": "https://laravel.com", + "time": "2018-10-07T15:51:39+00:00" + }, + { + "name": "mongodb/mongodb", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/mongodb/mongo-php-library.git", + "reference": "bd148eab0493e38354e45e2cd7db59b90fdcad79" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mongodb/mongo-php-library/zipball/bd148eab0493e38354e45e2cd7db59b90fdcad79", + "reference": "bd148eab0493e38354e45e2cd7db59b90fdcad79", + "shasum": "" + }, + "require": { + "ext-hash": "*", + "ext-json": "*", + "ext-mongodb": "^1.5.0", + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "MongoDB\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Jeremy Mikola", + "email": "jmikola@gmail.com" + }, + { + "name": "Derick Rethans", + "email": "github@derickrethans.nl" + }, + { + "name": "Katherine Walker", + "email": "katherine.walker@mongodb.com" + } + ], + "description": "MongoDB driver library", + "homepage": "https://jira.mongodb.org/browse/PHPLIB", + "keywords": [ + "database", + "driver", + "mongodb", + "persistence" + ], + "time": "2018-07-18T14:33:41+00:00" + }, + { + "name": "nesbot/carbon", + "version": "1.34.0", + "source": { + "type": "git", + "url": "https://github.com/briannesbitt/Carbon.git", + "reference": "1dbd3cb01c5645f3e7deda7aa46ef780d95fcc33" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/1dbd3cb01c5645f3e7deda7aa46ef780d95fcc33", + "reference": "1dbd3cb01c5645f3e7deda7aa46ef780d95fcc33", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/translation": "~2.6 || ~3.0 || ~4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2", + "phpunit/phpunit": "^4.8.35 || ^5.7" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "http://nesbot.com" + } + ], + "description": "A simple API extension for DateTime.", + "homepage": "http://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "time": "2018-09-20T19:36:25+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494", + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "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 for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2018-09-21T13:07:52+00:00" + }, + { + "name": "symfony/translation", + "version": "v4.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "9f0b61e339160a466ebcde167a6c5521c810e304" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/9f0b61e339160a466ebcde167a6c5521c810e304", + "reference": "9f0b61e339160a466ebcde167a6c5521c810e304", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/config": "<3.4", + "symfony/dependency-injection": "<3.4", + "symfony/yaml": "<3.4" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.4|~4.0", + "symfony/console": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/finder": "~2.8|~3.0|~4.0", + "symfony/intl": "~3.4|~4.0", + "symfony/yaml": "~3.4|~4.0" + }, + "suggest": { + "psr/log-implementation": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Translation Component", + "homepage": "https://symfony.com", + "time": "2018-10-02T16:36:10+00:00" + } + ], + "packages-dev": [ + { + "name": "blackfire/php-sdk", + "version": "v1.17.1", + "source": { + "type": "git", + "url": "https://github.com/blackfireio/php-sdk.git", + "reference": "a85703a57df9da0d840f98c4e044ea70d3621444" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/blackfireio/php-sdk/zipball/a85703a57df9da0d840f98c4e044ea70d3621444", + "reference": "a85703a57df9da0d840f98c4e044ea70d3621444", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "php": ">=5.2.0" + }, + "suggest": { + "ext-blackfire": "The C version of the Blackfire probe", + "ext-zlib": "To push config to remote profiling targets" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5.x-dev" + } + }, + "autoload": { + "files": [ + "src/autostart.php" + ], + "psr-4": { + "Blackfire\\": "src/Blackfire" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Blackfire.io", + "email": "support@blackfire.io" + } + ], + "description": "Blackfire.io PHP SDK", + "keywords": [ + "performance", + "profiler", + "uprofiler", + "xhprof" + ], + "time": "2018-07-16T09:18:18+00:00" + }, + { + "name": "composer/ca-bundle", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8afa52cd417f4ec417b4bfe86b68106538a87660", + "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5", + "psr/log": "^1.0", + "symfony/process": "^2.5 || ^3.0 || ^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "time": "2018-10-18T06:09:13+00:00" + }, + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v0.4.4", + "source": { + "type": "git", + "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "reference": "2e41850d5f7797cbb1af7b030d245b3b24e63a08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/2e41850d5f7797cbb1af7b030d245b3b24e63a08", + "reference": "2e41850d5f7797cbb1af7b030d245b3b24e63a08", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0", + "php": "^5.3|^7", + "squizlabs/php_codesniffer": "*" + }, + "require-dev": { + "composer/composer": "*", + "wimg/php-compatibility": "^8.0" + }, + "suggest": { + "dealerdirect/qa-tools": "All the PHP QA tools you'll need" + }, + "type": "composer-plugin", + "extra": { + "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "f.nijhof@dealerdirect.nl", + "homepage": "http://workingatdealerdirect.eu", + "role": "Developer" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://workingatdealerdirect.eu", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "time": "2017-12-06T16:27:17+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "^6.2.3", + "squizlabs/php_codesniffer": "^3.0.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2017-07-22T11:58:36+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.3-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2018-04-22T15:46:56+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20T10:07:11+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2017-03-20T17:10:46+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.0.0", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "776503d3a8e85d4f9a1148614f95b7a608b046ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/776503d3a8e85d4f9a1148614f95b7a608b046ad", + "reference": "776503d3a8e85d4f9a1148614f95b7a608b046ad", + "shasum": "" + }, + "require": { + "php": "^5.3|^7.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "1.3.3", + "phpunit/phpunit": "~4.0", + "satooshi/php-coveralls": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "time": "2016-01-20T08:20:44+00:00" + }, + { + "name": "leroy-merlin-br/coding-standard", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/leroy-merlin-br/coding-standard.git", + "reference": "f98cb91c03f5e591218ad6c22d159ce7cc3de86a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/leroy-merlin-br/coding-standard/zipball/f98cb91c03f5e591218ad6c22d159ce7cc3de86a", + "reference": "f98cb91c03f5e591218ad6c22d159ce7cc3de86a", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", + "php": "^7.1", + "slevomat/coding-standard": "^4.8.0", + "squizlabs/php_codesniffer": "^3.3.2" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Boitatá", + "email": "boitata@leroymerlin.com.br" + } + ], + "description": "The coding standard for PHP projects on LMBR", + "keywords": [ + "checks", + "code", + "coding", + "cs", + "leroy-merlin", + "php", + "rules", + "sniffer", + "sniffs", + "standard", + "style" + ], + "time": "2018-10-08T14:35:54+00:00" + }, + { + "name": "michelf/php-markdown", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/michelf/php-markdown.git", + "reference": "01ab082b355bf188d907b9929cd99b2923053495" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/michelf/php-markdown/zipball/01ab082b355bf188d907b9929cd99b2923053495", + "reference": "01ab082b355bf188d907b9929cd99b2923053495", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Michelf\\": "Michelf/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Michel Fortin", + "email": "michel.fortin@michelf.ca", + "homepage": "https://michelf.ca/", + "role": "Developer" + }, + { + "name": "John Gruber", + "homepage": "https://daringfireball.net/" + } + ], + "description": "PHP Markdown", + "homepage": "https://michelf.ca/projects/php-markdown/", + "keywords": [ + "markdown" + ], + "time": "2018-01-15T00:49:33+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "100633629bf76d57430b86b7098cd6beb996a35a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/100633629bf76d57430b86b7098cd6beb996a35a", + "reference": "100633629bf76d57430b86b7098cd6beb996a35a", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "~2.0", + "lib-pcre": ">=7.0", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "~5.7.10|~6.5|~7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Mockery": "library/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "http://blog.astrumfutura.com" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "http://davedevelopment.co.uk" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "time": "2018-10-02T21:52:37+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.8.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", + "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2018-06-11T23:09:50+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v3.1.5", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/bb87e28e7d7b8d9a7fda231d37457c9210faf6ce", + "reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "~4.0|~5.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "time": "2018-02-28T20:30:58+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^1.0.1", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2017-03-05T18:14:27+00:00" + }, + { + "name": "phar-io/version", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2017-03-05T17:38:23+00:00" + }, + { + "name": "php-coveralls/php-coveralls", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-coveralls/php-coveralls.git", + "reference": "3b00c229726f892bfdadeaf01ea430ffd04a939d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-coveralls/php-coveralls/zipball/3b00c229726f892bfdadeaf01ea430ffd04a939d", + "reference": "3b00c229726f892bfdadeaf01ea430ffd04a939d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.0", + "php": "^5.5 || ^7.0", + "psr/log": "^1.0", + "symfony/config": "^2.1 || ^3.0 || ^4.0", + "symfony/console": "^2.1 || ^3.0 || ^4.0", + "symfony/stopwatch": "^2.0 || ^3.0 || ^4.0", + "symfony/yaml": "^2.0 || ^3.0 || ^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.4.3 || ^6.0" + }, + "suggest": { + "symfony/http-kernel": "Allows Symfony integration" + }, + "bin": [ + "bin/php-coveralls" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "psr-4": { + "PhpCoveralls\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kitamura Satoshi", + "email": "with.no.parachute@gmail.com", + "homepage": "https://www.facebook.com/satooshi.jp", + "role": "Original creator" + }, + { + "name": "Takashi Matsuo", + "email": "tmatsuo@google.com" + }, + { + "name": "Google Inc" + }, + { + "name": "Dariusz Ruminski", + "email": "dariusz.ruminski@gmail.com", + "homepage": "https://github.com/keradus" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-coveralls/php-coveralls/graphs/contributors" + } + ], + "description": "PHP client library for Coveralls API", + "homepage": "https://github.com/php-coveralls/php-coveralls", + "keywords": [ + "ci", + "coverage", + "github", + "test" + ], + "time": "2018-05-22T23:11:08+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "e6a969a640b00d8daa3c66518b0405fb41ae0c4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e6a969a640b00d8daa3c66518b0405fb41ae0c4b", + "reference": "e6a969a640b00d8daa3c66518b0405fb41ae0c4b", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "phpDocumentor": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "time": "2016-01-25T08:17:30+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", + "sebastian/comparator": "^1.1|^2.0|^3.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5|^3.2", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2018-08-05T17:53:17+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "5.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "c89677919c5dd6d3b3852f230a663118762218ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac", + "reference": "c89677919c5dd6d3b3852f230a663118762218ac", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.0", + "phpunit/php-file-iterator": "^1.4.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^2.0.1", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-xdebug": "^2.5.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2018-04-06T15:36:58+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2017-11-27T13:52:08+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2017-02-26T11:10:40+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "791198a2c6254db10131eecfe8c06670700904db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", + "reference": "791198a2c6254db10131eecfe8c06670700904db", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2017-11-27T05:48:46+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "6.5.13", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "0973426fb012359b2f18d3bd1e90ef1172839693" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693", + "reference": "0973426fb012359b2f18d3bd1e90ef1172839693", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "^1.6.1", + "phar-io/manifest": "^1.0.1", + "phar-io/version": "^1.0", + "php": "^7.0", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^5.3", + "phpunit/php-file-iterator": "^1.4.3", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^1.0.9", + "phpunit/phpunit-mock-objects": "^5.0.9", + "sebastian/comparator": "^2.1", + "sebastian/diff": "^2.0", + "sebastian/environment": "^3.1", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^1.0", + "sebastian/version": "^2.0.1" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2", + "phpunit/dbunit": "<3.0" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-xdebug": "*", + "phpunit/php-invoker": "^1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2018-09-08T15:10:43+00:00" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "5.0.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f", + "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.5", + "php": "^7.0", + "phpunit/php-text-template": "^1.2.1", + "sebastian/exporter": "^3.1" + }, + "conflict": { + "phpunit/phpunit": "<6.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5.11" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2018-08-09T05:50:03+00:00" + }, + { + "name": "pimple/pimple", + "version": "v3.2.3", + "source": { + "type": "git", + "url": "https://github.com/silexphp/Pimple.git", + "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/9e403941ef9d65d20cba7d54e29fe906db42cf32", + "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/container": "^1.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "psr-0": { + "Pimple": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Pimple, a simple Dependency Injection Container", + "homepage": "http://pimple.sensiolabs.org", + "keywords": [ + "container", + "dependency injection" + ], + "time": "2018-01-21T07:42:36+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2016-10-10T12:19:37+00:00" + }, + { + "name": "sami/sami", + "version": "v4.1.2", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/Sami.git", + "reference": "19b8a82b858bd31544c468317c8307188ccb4022" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/Sami/zipball/19b8a82b858bd31544c468317c8307188ccb4022", + "reference": "19b8a82b858bd31544c468317c8307188ccb4022", + "shasum": "" + }, + "require": { + "blackfire/php-sdk": "^1.5.18", + "michelf/php-markdown": "~1.3", + "nikic/php-parser": "~3.0", + "php": "^7.1.3", + "phpdocumentor/reflection-docblock": "~2.0", + "pimple/pimple": "~3.0", + "symfony/console": "~3.0|~4.0", + "symfony/filesystem": "~3.0|~4.0", + "symfony/finder": "~3.0|~4.0", + "symfony/process": "~3.0|~4.0", + "symfony/yaml": "~3.0|~4.0", + "twig/twig": "~2.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "~4.0" + }, + "bin": [ + "sami.php" + ], + "type": "application", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Sami\\": "Sami/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Sami, an API documentation generator", + "homepage": "http://sami.sensiolabs.org", + "keywords": [ + "phpdoc" + ], + "abandoned": true, + "time": "2018-07-02T13:20:39+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2017-03-04T06:30:41+00:00" + }, + { + "name": "sebastian/comparator", + "version": "2.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/diff": "^2.0 || ^3.0", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2018-02-01T13:46:46+00:00" + }, + { + "name": "sebastian/diff", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2017-08-03T08:09:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2017-07-01T08:51:00+00:00" + }, + { + "name": "sebastian/exporter", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", + "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2017-04-03T13:19:02+00:00" + }, + { + "name": "sebastian/global-state", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2017-04-27T15:39:26+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2017-08-03T12:35:26+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "773f97c67f28de00d397be301821b06708fca0be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", + "reference": "773f97c67f28de00d397be301821b06708fca0be", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "time": "2017-03-29T09:07:27+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2017-03-03T06:23:57+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28T20:34:47+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "slevomat/coding-standard", + "version": "4.8.5", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "057f3f154cf4888b60eb4cdffadc509a3ae9dccd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/057f3f154cf4888b60eb4cdffadc509a3ae9dccd", + "reference": "057f3f154cf4888b60eb4cdffadc509a3ae9dccd", + "shasum": "" + }, + "require": { + "php": "^7.1", + "squizlabs/php_codesniffer": "^3.3.0" + }, + "require-dev": { + "jakub-onderka/php-parallel-lint": "1.0.0", + "phing/phing": "2.16.1", + "phpstan/phpstan": "0.9.2", + "phpstan/phpstan-phpunit": "0.9.4", + "phpstan/phpstan-strict-rules": "0.9", + "phpunit/phpunit": "7.3.5" + }, + "type": "phpcodesniffer-standard", + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "time": "2018-10-05T12:10:21+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "6ad28354c04b364c3c71a34e4a18b629cc3b231e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/6ad28354c04b364c3c71a34e4a18b629cc3b231e", + "reference": "6ad28354c04b364c3c71a34e4a18b629cc3b231e", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "http://www.squizlabs.com/php-codesniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2018-09-23T23:08:17+00:00" + }, + { + "name": "symfony/config", + "version": "v4.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "b3d4d7b567d7a49e6dfafb6d4760abc921177c96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/b3d4d7b567d7a49e6dfafb6d4760abc921177c96", + "reference": "b3d4d7b567d7a49e6dfafb6d4760abc921177c96", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/filesystem": "~3.4|~4.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<3.4" + }, + "require-dev": { + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~3.4|~4.0", + "symfony/finder": "~3.4|~4.0", + "symfony/yaml": "~3.4|~4.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Config Component", + "homepage": "https://symfony.com", + "time": "2018-09-08T13:24:10+00:00" + }, + { + "name": "symfony/console", + "version": "v4.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "dc7122fe5f6113cfaba3b3de575d31112c9aa60b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/dc7122fe5f6113cfaba3b3de575d31112c9aa60b", + "reference": "dc7122fe5f6113cfaba3b3de575d31112c9aa60b", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~3.4|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.4|~4.0" + }, + "suggest": { + "psr/log-implementation": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2018-10-03T08:15:46+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v4.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "596d12b40624055c300c8b619755b748ca5cf0b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/596d12b40624055c300c8b619755b748ca5cf0b5", + "reference": "596d12b40624055c300c8b619755b748ca5cf0b5", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-ctype": "~1.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2018-10-02T12:40:59+00:00" + }, + { + "name": "symfony/finder", + "version": "v4.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "1f17195b44543017a9c9b2d437c670627e96ad06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/1f17195b44543017a9c9b2d437c670627e96ad06", + "reference": "1f17195b44543017a9c9b2d437c670627e96ad06", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2018-10-03T08:47:56+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2018-08-06T14:22:27+00:00" + }, + { + "name": "symfony/process", + "version": "v4.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "ee33c0322a8fee0855afcc11fff81e6b1011b529" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/ee33c0322a8fee0855afcc11fff81e6b1011b529", + "reference": "ee33c0322a8fee0855afcc11fff81e6b1011b529", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2018-10-02T12:40:59+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v4.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "5bfc064125b73ff81229e19381ce1c34d3416f4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5bfc064125b73ff81229e19381ce1c34d3416f4b", + "reference": "5bfc064125b73ff81229e19381ce1c34d3416f4b", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Stopwatch Component", + "homepage": "https://symfony.com", + "time": "2018-10-02T12:40:59+00:00" + }, + { + "name": "symfony/yaml", + "version": "v4.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "367e689b2fdc19965be435337b50bc8adf2746c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/367e689b2fdc19965be435337b50bc8adf2746c9", + "reference": "367e689b2fdc19965be435337b50bc8adf2746c9", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/console": "<3.4" + }, + "require-dev": { + "symfony/console": "~3.4|~4.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2018-10-02T16:36:10+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "time": "2017-04-07T12:08:54+00:00" + }, + { + "name": "twig/twig", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "6a5f676b77a90823c2d4eaf76137b771adf31323" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/6a5f676b77a90823c2d4eaf76137b771adf31323", + "reference": "6a5f676b77a90823c2d4eaf76137b771adf31323", + "shasum": "" + }, + "require": { + "php": "^7.0", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "psr/container": "^1.0", + "symfony/debug": "^2.7", + "symfony/phpunit-bridge": "^3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + }, + "autoload": { + "psr-0": { + "Twig_": "lib/" + }, + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + }, + { + "name": "Twig Team", + "homepage": "https://twig.symfony.com/contributors", + "role": "Contributors" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "time": "2018-07-13T07:18:09+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.1", + "ext-mongodb": "*" + }, + "platform-dev": [] +} diff --git a/docker-compose.yml b/docker-compose.yml index c633d274..6d190663 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,11 +2,13 @@ version: '3' services: php: - image: leroymerlinbr/php + build: docker/php depends_on: - db volumes: - .:/var/www/html + environment: + - DB_HOST=db db: image: mongo:4.0 diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile new file mode 100644 index 00000000..464546fd --- /dev/null +++ b/docker/php/Dockerfile @@ -0,0 +1,10 @@ +FROM leroymerlinbr/php:7.2 + +USER root + +RUN pecl install xdebug \ + && docker-php-ext-enable xdebug + +COPY custom.ini /usr/local/etc/php/conf.d/custom.ini + +USER www-data:www-data diff --git a/docker/php/custom.ini b/docker/php/custom.ini new file mode 100644 index 00000000..70a5c913 --- /dev/null +++ b/docker/php/custom.ini @@ -0,0 +1,4 @@ +error_reporting=E_ALL +log_errors=On +memory_limit=-1 +xdebug.default_enable=1 diff --git a/docs/basics.md b/docs/basics.md index 30732b51..3648a1a5 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -2,40 +2,39 @@ You can install library through Composer: -``` -$ composer require leroy-merlin-br/mongolid +```shell +composer require leroy-merlin-br/mongolid ``` ### Requirements -- PHP**7** +- PHP **7.1** - [MongoDB Driver](http://php.net/manual/en/set.mongodb.php) -> **Note:** If you are looking for the old PHP 5.x version, head to the [v0.8 branch](https://github.com/leroy-merlin-br/mongolid/tree/v0.8-dev). - ## Setup -If you are not using Laravel, you should initialize the Mongolid connection pool and container manually. -The minimalistic way of doing it is to use `Mongolid\Manager`: +If you are not using Laravel, you should initialize the Mongolid connection and container manually. +The minimalistic way of doing it is to use `Mongolid\Connection\Manager`: ```php setConnection(new Connection('mongodb://localhost:27017')); ``` Now you are ready to create your own models :smile: ## Basic Usage -> **Note:** Mongolid does support [**DataMapper** pattern](./datamapper.md), but in order to understand it let's begin with the **ActiveRecord** pattern: - ```php -class Post extends Mongolid\ActiveRecord {} +class Post extends \Mongolid\Model\AbstractModel +{ +} ``` Note that we did not tell Mongolid which collection to use for our `Post` model. So, in this case, Mongolid **will not save the model into the database**. This can be used for models that represents objects that will be embedded within another object and will not have their own collection. @@ -43,7 +42,7 @@ Note that we did not tell Mongolid which collection to use for our `Post` model. You may specify a collection by defining a `collection` property on your model: ```php -class Post extends ActiveRecord { +class Post extends \Mongolid\Model\AbstractModel { protected $collection = 'posts'; @@ -59,56 +58,55 @@ Once a model is defined, you are ready to start retrieving and creating document **Retrieving All Models** ```php - $posts = Post::all(); +$posts = Post::all(); ``` **Retrieving A Document By Primary Key** ```php - $post = Post::first('4af9f23d8ead0e1d32000000'); +$post = Post::first('4af9f23d8ead0e1d32000000'); - // or +// or - $post = Post::first(new MongoDB\BSON\ObjectID('4af9f23d8ead0e1d32000000')); +$post = Post::first(new MongoDB\BSON\ObjectID('4af9f23d8ead0e1d32000000')); ``` **Retrieving One Document By attribute** ```php - $user = Post::first(['title'=>'How Monglid saved the day']); +$user = Post::first(['title' => 'How Mongolid saved the day']); ``` **Retrieving Many Documents By attribute** ```php - $posts = Post::where(['category'=>'coding']); +$posts = Post::where(['category' => 'coding']); ``` **Querying Using Mongolid Models** ```php - $posts = Post::where(['votes'=>['$gt'=>100]])->limit(10); // Mongolid\Cursor\Cursor +$posts = Post::where(['votes' => ['$gt' => 100]])->limit(10); // Mongolid\Cursor\Cursor - foreach ($posts as $post) - { - var_dump($post->title); - } +foreach ($posts as $post) { + var_dump($post->title); +} ``` **Mongolid Count** ```php - $count = Post::where(['votes'=>['$gt'=>100]])->count(); // integer +$count = Post::where(['votes' => ['$gt' => 100]])->count(); // int ``` Pretty easy right? -## Monglid Cursor +## Mongolid Cursor In MongoDB, a cursor is used to iterate through the results of a database query. For example, to query the database and see all results: ```php - $cursor = User::where(['kind'=>'visitor']); +$cursor = User::where(['kind' => 'visitor']); ``` In the above example, the $cursor variable will be a `Mongolid\Cursor\Cursor`. @@ -120,36 +118,36 @@ The Mongolid's `Cursor` wraps the original `MongoDB\Driver\Cursor` object of the The `Mongolid\Cursor\Cursor` object has alot of methods that helps you to iterate, refine and get information. For example: ```php - $cursor = User::where(['kind'=>'visitor']); +$cursor = User::where(['kind'=>'visitor']); - // Sorts the results by given fields. In the example bellow, it sorts by username DESC - $cursor->sort(['username'=>-1]); +// Sorts the results by given fields. In the example bellow, it sorts by username DESC +$cursor->sort(['username'=>-1]); - // Limits the number of results returned. - $cursor->limit(10); +// Limits the number of results returned. +$cursor->limit(10); - // Skips a number of results. Good for pagination - $cursor->skip(20); +// Skips a number of results. Good for pagination +$cursor->skip(20); - // Checks if the cursor is reading a valid result. - $cursor->valid(); +// Checks if the cursor is reading a valid result. +$cursor->valid(); - // Returns the first result - $cursor->first(); +// Returns the first result +$cursor->first(); ``` You can also chain some methods: ```php - $page = 2; +$page = 2; - // In order to display 10 results per page - $cursor = User::all()->sort(['_id'=>1])->skip(10 * $page)->limit(10); +// In order to display 10 results per page +$cursor = User::all()->sort(['_id'=>1])->skip(10 * $page)->limit(10); - // Then iterate through it - foreach($cursor as $user) { - // do something - } +// Then iterate through it +foreach($cursor as $user) { + // do something +} ``` ## Insert, Update, Delete @@ -159,11 +157,11 @@ To create a new document in the database from a model, simply create a new model **Saving A New Model** ```php - $post = new Post; +$post = new Post(); - $post->title = 'Foo bar john doe'; +$post->title = 'Foo bar john doe'; - $post->save(); +$post->save(); ``` > **Note:** Typically, your Mongolid models will have auto-generated `_id` keys. However, if you wish to specify your own keys, set the `_id` attribute. @@ -173,11 +171,11 @@ To update a model, you may retrieve it, change an attribute, and use the `save` **Updating A Retrieved Model** ```php - $post = Post::first('4af9f23d8ead0e1d32000000'); +$post = Post::first('4af9f23d8ead0e1d32000000'); - $post->subject = 'technology'; +$post->subject = 'technology'; - $post->save(); +$post->save(); ``` To delete a model, simply call the `delete` method on the instance: @@ -185,27 +183,27 @@ To delete a model, simply call the `delete` method on the instance: **Deleting An Existing Model** ```php - $post = Post::first('4af9f23d8ead0e1d32000000'); +$post = Post::first('4af9f23d8ead0e1d32000000'); - $post->delete(); +$post->delete(); ``` ## Mass Assignment -If you are extending `Mongolid\ActiveRecord` you can set an array of attributes to the model using the `fill` method. These attributes are then assigned to the model via mass-assignment. This is convenient; however, can be a **serious** security concern when blindly passing user input into a model. If user input is blindly passed into a model, the user is free to modify **any** and **all** of the model's attributes. By default, all attributes are fillable. +If you are extending `Mongolid\Model\AbstractModel` you can set an array of attributes to the model using the `fill` method. These attributes are then assigned to the model via mass-assignment. This is convenient; however, can be a **serious** security concern when blindly passing user input into a model. If user input is blindly passed into a model, the user is free to modify **any** and **all** of the model's attributes. By default, all attributes are fillable. -`Mongolid\ActiveRecord` (and `Mongolid\Model\Attributes` trait) will use the `fillable` or `guarded` properties on your model. +`Mongolid\Model\AbstractModel` (and `Mongolid\Model\Attributes` trait) will use the `fillable` or `guarded` properties on your model. The `fillable` property specifies which attributes should be mass-assignable. This can be set at the class or instance level. **Defining Fillable Attributes On A Model** ```php - class Post extends ActiveRecord { +class Post extends \Mongolid\Model\AbstractModel { - protected $fillable = ['title', 'category', 'body']; + protected $fillable = ['title', 'category', 'body']; - } +} ``` In this example, only the three listed attributes will be mass-assignable. @@ -215,11 +213,11 @@ The inverse of `fillable` is `guarded`, and serves as a "black-list" instead of **Defining Guarded Attributes On A Model** ```php - class Post extends ActiveRecord { +class Post extends \Mongolid\Model\AbstractModel { - protected $guarded = ['_id', 'votes']; + protected $guarded = ['_id', 'votes']; - } +} ``` In the example above, the `id` and `votes` attributes may **not** be mass assigned. All other attributes will be mass assignable. @@ -227,8 +225,8 @@ In the example above, the `id` and `votes` attributes may **not** be mass assign You can mass assign attributes using the `fill` method: ```php - $post = new Post; - $post->fill(['title' => 'Bacon']); +$post = new Post; +$post->fill(['title' => 'Bacon']); ``` ## Converting To Arrays / JSON @@ -238,15 +236,15 @@ When building JSON APIs, you may often need to convert your models to arrays or **Converting A Model To An Array** ```php - $user = User::with('roles')->first(); +$user = User::first(); - return $user->toArray(); +return $user->toArray(); ``` Note that [cursors](#cursor) can be converted to array too: ```php - return User::all()->toArray(); +return User::all()->toArray(); ``` To convert a model to JSON, you may use the `toJson` method: @@ -254,5 +252,5 @@ To convert a model to JSON, you may use the `toJson` method: **Converting A Model To JSON** ```php - return User::find(1)->toJson(); +return User::find(1)->toJson(); ``` diff --git a/docs/datamapper.md b/docs/datamapper.md deleted file mode 100644 index 571b31c3..00000000 --- a/docs/datamapper.md +++ /dev/null @@ -1,228 +0,0 @@ -## Introduction - -In the [Basics](./basics.md#basic-usage) section you learned how to use Mongolid with the ActiveRecord pattern. The following section you explain what changes are necessary in order to use the DataMapper pattern of Mongolid. - -> **Note:** To use Mongolid in the DataMapper pattern is optional. If you are looking for a more [Domain Driven Design](https://en.wikipedia.org/wiki/Domain-driven_design) approach in your project it may be interesting to your. But if you are satisfied with what you've learned in the other sections, feel free to skip this one. - -## Basics - -First of all, you have to define a _Schema_ for your model. This is the way to map objects into the database. But don't worry, your schema can be dynamic, which means that you can define other fields than the specified ones. - -> **Note:** The _Mongolid Schema_ is equivalent to [mapping your objects using annotation or xml](http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/xml-mapping.html) in other ORM or ODM libraries. - -In order to define a schema you should extend `Mongolid\Schema\Schema` or `Mongolid\Schema\DynamicSchema`: - -```php - 'objectId', - 'title' => 'string', - 'body' => 'string', - 'views' => 'int', - 'created_at' => 'createdAtTimestamp', - 'updated_at' => 'updatedAtTimestamp' - ]; -} -``` - -Then you should register an instance of the schema into the `Mongolid\Manager`: - -```php -$manager->registerSchema(new ArticleSchema); -``` - -Now you just have to create your own Domain Entity (: - -```php -class Article -{ - public $_id - public $title - public $body - public $views - public $created_at - public $updated_at - - public function render() { - return "# {$this->title}\n\n{$this->body}"; - } -} -``` - -## Interacting with the database - -Interact with the database using the `DataMapper` retrieved through the Mongolid Manager: - -```php -$article = $manager->getMapper(Article::class) - ->where(['title' => 'Foobar'])->first(); - -get_class($article) // Article - -$article->render() // # Foobar\n\nBody -$article->title = 'How Mongolid saved the day'; - -$manager->getMapper(Article::class)->save($article); // true -``` - -## Schema definition - -When defining your schema you can eighter extend the `Mongolid\Schema\Schema` or `Mongolid\Schema\DynamicSchema`. The main difference is the `$dynamic` property value. - -**Making a schema dynamic** - -The `$dynamic` property is a `boolean` that tells if the schema will accept additional fields that are not specified in the $fields property. This is usefull if you doesn't have a strict document format or if you want to take full advantage of the "schemaless" nature of MongoDB. - -**Defining the collection and the Domain Entity to be mapped** - -`$collection` property should be the name of the collection where this kind of document is going to be saved or retrieved from. And the `$entityClass` should be the name of the class that will be used to represent a document of the Schema when retrieve from the database. - -**Defining the fields** - -The `$fields` property is an array that tells how a document should look like. For each field of the document you can specify a type or how it will be "formated". - -If an scalar type is used, it will perform a cast operation in the value. Othewise the schema will use the type as the name of the method to be called. - -See `Mongolid\Schema\Schema::objectId` method for example. It means that if a field type (in `$fields`) is defined as `"objectId"`, it will pass trought the `Mongolid\Schema\Schema::objectId` before being saved in the database. - -Because of this you can create your own custom field types easily. Just create a new public method in your schema and you are ready to use it's name as a type definition in `$fields`. - -The last option is to define a field as another schema by using the syntax _'schema.<Class>'_ This represents one or more embedded documents that will be formated using another _Schema_ class. - -### Default Schema $field types - -By default the `Mongolid\Schema\Schema` contains the following types: - -| type | description | -|--------------------|--------------------------------------------------------------------------------------------------------------------| -| <scalar type> | Casts field to `int`, `integer`, `bool`, `boolean`, `float`, `double`, `real` or `string`. | -| objectId | If the field is not defined or if it's a string compatible with ObjectId notation it will be saved as an ObjectId. | -| sequence | If value is zero or not defined a new auto-increment integer will be "generated" for that collection. | -| createdAtTimestamp | Prepares the field to be the datetime that the document has been created. (MongoDB\BSON\UTCDateTime) | -| updatedAtTimestamp | Prepares the field to be now whenever the document is saved. (MongoDB\BSON\UTCDateTime) | -| schema.<Class> | Delegates the objects or arrays within the field to be mapped by another schema. | - -But you can easily create your own types. For example: - -```php -class MySchema extends Mongolid\Schema\Schema -{ - ... - - $fields = [ - '_id' => 'sequence', - 'name' => 'uppercaseString' // Will be processed by the method below - ]; - - public function uppercaseString(string $value) { - return strtoupper($value); - } -} -``` - -### Schema definition for embeded documents - -By using the `"schema."` syntax you can create schemas and map Entities for embeded documents. For example, with the definition below: - -```php -class PostSchema extends Mongolid\Schema\Schema -{ - public $entityClass = 'Post'; - public $collection = 'posts'; - - $fields = [ - '_id' => 'objectId', - 'title' => 'string', - 'body' => 'string', - 'comments' => 'schema.CommentSchema' // Embeds comments - ]; -} - -class CommentSchema extends Mongolid\Schema\Schema -{ - public $entityClass = 'Comment'; - public $collection = null; // Optional since all comments will be embedded - - $fields = [ - '_id' => 'objectId', - 'body' => 'string', - 'author' => 'string' - ]; -} -``` - -The MongoDB document - -```javascript -{ - _id: ObjectId("5099803df3f4948bd2f98391"), - title: "Foo bar", - body: "Lorem ipsum", - comments: [ - { - _id: ObjectId("507f1f77bcf86cd799439011"), - body: "Awesome!", - author: "John Doe", - }, - { - _id: ObjectId("507f191e810c19729de860ea"), - body: "Cool!", - author: "Alan Turing", - } - ] -} -``` - -...Will be mapped to the actual domain Entities: - -```php -$post = $manager->getMapper(Post::class)->first('5099803df3f4948bd2f98391'); - -get_class($post) // Post - -get_class($post->comments[1]) // Comment; - -$post->comments[1]->author // Alan Turing - -// And if you are using Mongolid\Model\Relations trait in your entity ;) -$post->comments()->sort(['author' => 1])->first()->name // Alan Turing -``` - -## Entity helpers - -Mongolid provides some helpers for Domain Entities in the form of traits. - -### Attribute trait - -The `Mongolid\Model\Attributes` trait adds attribute getters, setters and the `fill` method that can be used with `$fillable` and `$guarded` properties to make sure that only the correct attributes will be set. - -By including this trait all the entity attributes will be isolated in the `$attributes` property of your entity and [mass assignment capabilities](./basics.md#mass-assignment) will be available. - -See `Mongolid\Model\Attributes` for more information. - -### Relations trait - -The `Mongolid\Model\Relations` trait adds functionality for handling relations between entities. It will enable `embedsOne`, `embedsMany`, `referencesOne`,`referencesMany` methods and all the [relationship capabilities](./relationships.md) in the entity. - -**Using relationship methods in DataMapper pattern** - -When using relationship methods (`embedsMany` for example) in DataMapper pattern, instead of referencing the _Entity_ class you should reference the _Schema_ class, for example: - -```php - ... - public function comments() - { - return $this->embedsMany('CommentSchema', 'comments'); - } -``` - -In the example above Mongolid will use the `CommentSchema` to determine which Entity object should be used to map data of the `comments` field. - -See `Mongolid\Model\Relations` for more information. diff --git a/docs/index.md b/docs/index.md index 04bc74c3..ca360835 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,7 +2,7 @@ > Easy, powerful and ultrafast ODM for PHP7 build on top of the [new mongodb driver](https://docs.mongodb.org/ecosystem/drivers/php/). -Mongolid supports both **ActiveRecord** and **DataMapper** patterns. **You choose! (:** +Mongolid supports **ActiveRecord** pattern. [![Build Status](https://travis-ci.org/leroy-merlin-br/mongolid.svg?branch=master)](https://travis-ci.org/leroy-merlin-br/mongolid) [![Coverage Status](https://coveralls.io/repos/github/leroy-merlin-br/mongolid/badge.svg?branch=master)](https://coveralls.io/github/leroy-merlin-br/mongolid?branch=master) diff --git a/docs/relationships.md b/docs/relationships.md index a05789f9..a4798bef 100644 --- a/docs/relationships.md +++ b/docs/relationships.md @@ -18,38 +18,32 @@ A Embeds One relationship is a very basic relation. For example, a `User` model **Defining A Embeds One Relation** ```php - // models/Person.php - class Person extends ActiveRecord { - - // This model is saved in the collection people - protected $collection = 'people'; - - // Method that will be used to access the phone - public function phone() - { - return $this->embedsOne('Phone', 'phone'); - } +class Person extends \Mongolid\Model\AbstractModel { + // This model is saved in the collection people + protected $collection = 'people'; + // Method that will be used to access the phone + public function phone() + { + return $this->embedsOne(Phone::class, 'phone'); } +} - // models/Phone.php - class Phone extends ActiveRecord { - - // This model will be embedded only - protected $collection = null; - - public function getFullPhone() - { - return '+' . $this->regionCode . $this->number; - } +class Phone extends \Mongolid\Model\AbstractModel { + // This model will be embedded only + protected $collection = null; + public function getFullPhone() + { + return '+' . $this->regionCode . $this->number; } +} ``` The first argument passed to the `embedsOne` method is the name of the related model. The second argument is in what attribute that object will be embedded. Once the relationship is defined, we may retrieve it using: ```php - $phone = User::find('4af9f23d8ead0e1d32000000')->phone(); +$phone = User::find('4af9f23d8ead0e1d32000000')->phone(); ``` Which will translate to: @@ -61,48 +55,48 @@ Which will translate to: In order to embed a document to be used in a Embeds One relationship, simply do the following: ```php - // The object that will be embeded - $phoneObj = new Phone; - $phoneObj->regionCode = '55'; - $phoneObj->number = '1532323232'; +// The object that will be embedded +$phone = new Phone(); +$phone->regionCode = '55'; +$phone->number = '1532323232'; - // The object that will contain the phone - $user = User::first('4af9f23d8ead0e1d32000000'); +// The object that will contain the phone +$user = User::first('4af9f23d8ead0e1d32000000'); - // This method will embed the $phoneObj into the phone attribute of the user - $user->embed('phone', $phoneObj); +// This method will embed the $phone into the phone attribute of the user +$user->embed('phone', $phone); - // This is an alias to the method called above. - $user->embedToPhone($phoneObj); +// This is an alias to the method called above. +$user->embedToPhone($phone); - // Not recomended, but also works - $user->phone = $phoneObj->attributes; +// Not recomended, but also works +$user->phone = $phone->attributes(); - // Or (not recomended) - $user->phone = $phoneObj->toArray(); +// Or (not recomended) +$user->phone = $phone->toArray(); - // Or even (not recomended) - $user->phone = [ - 'regionCode' => $phoneObj->regionCode, - 'number' => $phoneObj->number - ]; +// Or even (not recomended) +$user->phone = [ + 'regionCode' => $phone->regionCode, + 'number' => $phone->number +]; - $user->save(); +$user->save(); - // Now we can retrieve the object by calling - $user->phone(); // Will return a Phone object similar to $phoneObj +// Now we can retrieve the object by calling +$user->phone(); // Will return a Phone object similar to $phone ``` > **Note:** When using Mongolid models you will need to call the `save()` method after embeding or attaching objects. The changes will only persists after you call the 'save()' method. -It's recomended that you don't embed your models by setting the attribute directly. The `embed` method will include an `_id` to identify your embeded document and allow the usage of `unembed` and `embed` to update models. +It's recommended that you don't embed your models by setting the attribute directly. The `embed` method will include an `_id` to identify your embedded document and allow the usage of `unembed` and `embed` to update models. ```php - $user->embed('phone', $phoneObj); // Now, $phoneObj have an _id +$user->embed('phone', $phone); // Now, $phone have an _id - $phoneObj->regionCode = 77; // Update the region code - - $user->embed($phoneObj); // Will update +$phone->regionCode = 77; // Update the region code + +$user->embed($phone); // Will update ``` ### Embeds many @@ -110,69 +104,64 @@ It's recomended that you don't embed your models by setting the attribute direct An example of a Embeds Many relation is a blog post that "has many" comments. We can model this relation like so: ```php - // models/Post.php - class Post extends ActiveRecord { - - protected $collection = 'posts'; - - public function comments() - { - return $this->embedsMany('Comment', 'comments'); - } +class Post extends \Mongolid\Model\AbstractModel { + protected $collection = 'posts'; + public function comments() + { + return $this->embedsMany(Comment::class, 'comments'); } - // models/Comment.php - class Comment extends ActiveRecord { - - // This model will be embedded only - protected $collection = null; +} - } +class Comment extends \Mongolid\Model\AbstractModel { + // This model will be embedded only + protected $collection = null; +} ``` Now we can access the post's comments `EmbeddedCursor` through the comments method: ```php - $comments = Post::find('4af9f23d8ead0e1d32000000')->comments(); +$comments = Post::find('4af9f23d8ead0e1d32000000')->comments(); ``` Now you can iterate and perform cursor operations in the `EmbeddedCursor` that is retrieved ```php - foreach($comments->limit(10) as $comment) - { - // do something - } +foreach($comments->limit(10) as $comment) +{ + // do something +} ``` In order to embed a document to be used in a Embeds Many relationship, you should use the `embed` method or the alias `embedTo`: ```php - $commentA = new Comment; - $commentA->content = 'Cool feature bro!'; +$commentA = new Comment(); +$commentA->content = 'Cool feature bro!'; - $commentB = new Comment; - $commentB->content = 'Awesome!'; +$commentB = new Comment(); +$commentB->content = 'Awesome!'; - $post = Post::first('4af9f23d8ead0e1d32000000'); +$post = Post::first('4af9f23d8ead0e1d32000000'); - // Both ways work - $post->embedToComments($commentA); - $post->embed('Comments', $commentB); +// Both ways work +$post->embedToComments($commentA); +$post->embed('Comments', $commentB); - $post->save(); +$post->save(); ``` > **Note:** When using Mongolid models you will need to call the `save()` method after embeding or attaching objects. The changes will only persists after you call the 'save()' method. -The `embed` method will include an `_id` to identify your embeded document and allow the usage of `embed` and `unembed` to update or delete embeded documents: +The `embed` method will include an `_id` to identify your embedded document and allow the usage of `embed` and `unembed` to update or delete embedded documents: ```php - $commentB->content = "Pretty awesome!"; +$commentB->content = "Pretty awesome!"; - $post->unembed($commentA); // Removes 'Cool feature bro!' - $post->embed($commentB); // Updates 'Awesome' to 'Pretty awesome' +$post->unembed($commentA); // Removes 'Cool feature bro!' +$post->embed($commentB); // Updates 'Awesome' to 'Pretty awesome' ``` ### References One @@ -188,30 +177,25 @@ In general, use references when embedding would result in duplication of data an **Defining A References One Relation** ```php - // models/Post.php - class Post extends ActiveRecord { - - protected $collection = 'posts'; - - public function author() - { - return $this->referencesOne('User', 'author'); - } +class Post extends \Mongolid\Model\AbstractModel { + protected $collection = 'posts'; + public function author() + { + return $this->referencesOne(User::class, 'author'); } - // models/User.php - class User extends ActiveRecord { - - protected $collection = 'users'; +} - } +class User extends \Mongolid\Model\AbstractModel { + protected $collection = 'users'; +} ``` The first argument passed to the `referencesOne` method is the name of the related model, the second argument is the attribute where the referenced model `_id` will be stored. Once the relationship is defined, we may retrieve it using the following method: ```php - $user = Post::find('4af9f23d8ead0e1d32000000')->author(); +$user = Post::find('4af9f23d8ead0e1d32000000')->author(); ``` This statement will perform the following: @@ -223,26 +207,26 @@ This statement will perform the following: In order to set a reference to a document, simply set the attribute used in the relationship to the reference's `_id` or use the attach method or it's alias. For example: ```php - // The object that will be embeded - $userObj = new User; - $userObj->name = 'John'; - $userObj->save() // This will populates the $userObj->_id +// The object that will be embedded +$user = new User(); +$user->name = 'John'; +$user->save() // This will populates the $user->_id - // The object that will contain the user - $post = Post::first('4af9f23d8ead0e1d32000000'); +// The object that will contain the user +$post = Post::first('4af9f23d8ead0e1d32000000'); - // This method will attach the $phoneObj _id into the phone attribute of the user - $post->attach('author', $userObj); +// This method will attach the $phone _id into the phone attribute of the user +$post->attach('author', $user); - // This is an alias to the method called above. - $post->attachToAuthor($userObj); +// This is an alias to the method called above. +$post->attachToAuthor($user); - // This will will also work - $post->author = $userObj->_id; +// This will will also work +$post->author = $user->_id; - $post->save(); +$post->save(); - $post->author(); // Will return a User object +$post->author(); // Will return a User object ``` > **Note:** When using Mongolid models you will need to call the `save()` method after embedding or attaching objects. The changes will only persists after you call the 'save()' method. @@ -258,30 +242,25 @@ In general, use references when embedding would result in duplication of data an **Defining A References Many Relation** ```php - // models/User.php - class User extends ActiveRecord { - - protected $collection = 'users'; - - public function questions() - { - return $this->referencesMany('Question', 'questions'); - } +class User extends \Mongolid\Model\AbstractModel { + protected $collection = 'users'; + public function questions() + { + return $this->referencesMany(Question::class, 'questions'); } - // models/Question.php - class Question extends ActiveRecord { - - protected $collection = 'questions'; +} - } +class Question extends \Mongolid\Model\AbstractModel { + protected $collection = 'questions'; +} ``` The first argument passed to the `referencesMany` method is the name of the related model, the second argument is the attribute where the `_id`s will be stored. Once the relationship is defined, we may retrieve it using the following method: ```php - $posts = User::find('4af9f23d8ead0e1d32000000')->posts(); +$posts = User::find('4af9f23d8ead0e1d32000000')->posts(); ``` This statement will perform the following: @@ -293,21 +272,21 @@ This statement will perform the following: In order to set a reference to a document use the attach method or it's alias. For example: ```php - $postA = new Post; - $postA->title = 'Nice post'; +$postA = new Post(); +$postA->title = 'Nice post'; - $postB = new Post; - $postB->title = 'Nicer post'; +$postB = new Post(); +$postB->title = 'Nicer post'; - $user = User::first('4af9f23d8ead0e1d32000000'); +$user = User::first('4af9f23d8ead0e1d32000000'); - // Both ways work - $user->attachToPosts($postA); - $user->attach('posts', $postB); +// Both ways work +$user->attachToPosts($postA); +$user->attach('posts', $postB); - $user->save(); +$user->save(); ``` > **Note:** When using Mongolid models you will need to call the `save()` method after embedding or attaching objects. The changes will only persists after you call the 'save()' method. -You can use `dettach` method with the referenced object or it's `_id` in order to remove a single reference. +You can use `detach` method with the referenced object or it's `_id` in order to remove a single reference. diff --git a/mkdocs.yml b/mkdocs.yml index 8bf0d0aa..ff4dd84e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,7 +8,6 @@ nav: - 'Home': 'index.md' - 'Basics': 'basics.md' - 'Relationships': 'relationships.md' - - 'DataMapper Pattern': 'datamapper.md' - 'API Documentation': 'api-docs.md' - 'Troubleshooting': 'troubleshooting.md' - 'Additional information': 'info.md' diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 00000000..2414ba82 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + src + tests + + + diff --git a/phpunit.xml b/phpunit.xml index 7c9faa18..ae4a836b 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -12,8 +12,11 @@ syntaxCheck="false" > - - ./tests/ + + tests/Unit + + + tests/Integration diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d386bae0..d2c88298 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -12,8 +12,11 @@ syntaxCheck="false" > - - ./tests/ + + tests/Unit + + + tests/Integration @@ -22,6 +25,7 @@ + diff --git a/src/Mongolid/Connection/Connection.php b/src/Connection/Connection.php similarity index 72% rename from src/Mongolid/Connection/Connection.php rename to src/Connection/Connection.php index 4e4184ea..59dccfa3 100644 --- a/src/Mongolid/Connection/Connection.php +++ b/src/Connection/Connection.php @@ -1,9 +1,7 @@ 'array', 'document' => 'array']; + $driverOptions['typeMap'] = ['array' => 'array']; $this->findDefaultDatabase($server); - $this->rawConnection = new Client($server, $options, $driverOptions); + $this->client = new Client($server, $options, $driverOptions); + } + + /** + * Getter for Client instance. + */ + public function getClient(): Client + { + return $this->client; } /** @@ -60,24 +66,4 @@ protected function findDefaultDatabase(string $connectionString) $this->defaultDatabase = $matches[1]; } } - - /** - * Getter for Client instance. - * - * @return Client - */ - public function getRawConnection() - { - return $this->rawConnection; - } - - /** - * Getter for Manager instance. - * - * @return Manager - */ - public function getRawManager() - { - return $this->getRawConnection()->getManager(); - } } diff --git a/src/Connection/Manager.php b/src/Connection/Manager.php new file mode 100644 index 00000000..4b508837 --- /dev/null +++ b/src/Connection/Manager.php @@ -0,0 +1,99 @@ +setConnection(new Connection()); + * // And then start persisting and querying your models. + */ +class Manager +{ + /** + * Singleton instance of the manager. + * + * @var Manager + */ + protected static $singleton; + + /** + * Container being used by Mongolid. + * + * @var \Illuminate\Contracts\Container\Container + */ + public $container; + + /** + * Mongolid connection object. + * + * @var Connection + */ + protected $connection; + + /** + * Main entry point to opening a connection and start using Mongolid in + * pure PHP. After adding a connection into the Manager you are ready to + * persist and query your models. + * + * @param IlluminateContainer $connection connection instance to be used in database interactions + */ + public function setConnection(IlluminateContainer $connection): bool + { + $this->init(); + $this->container->instance(IlluminateContainer::class, $this->connection); + + $this->connection = $connection; + + return true; + } + + /** + * Get MongoDB client. + */ + public function getClient(): Client + { + $this->init(); + + return $this->connection->getClient(); + } + + /** + * Sets the event trigger for Mongolid events. + * + * @param EventTriggerInterface $eventTrigger external event trigger + */ + public function setEventTrigger(EventTriggerInterface $eventTrigger) + { + $this->init(); + $eventService = new EventTriggerService(); + $eventService->registerEventDispatcher($eventTrigger); + + $this->container->instance(EventTriggerService::class, $eventService); + } + + /** + * Initializes the Mongolid manager. + */ + protected function init() + { + if ($this->container) { + return; + } + + $this->container = new IlluminateContainer(); + Container::setContainer($this->container); + + static::$singleton = $this; + } +} diff --git a/src/Mongolid/Container/Ioc.php b/src/Container/Container.php similarity index 94% rename from src/Mongolid/Container/Ioc.php rename to src/Container/Container.php index 3ad6de40..abeb3f57 100644 --- a/src/Mongolid/Container/Ioc.php +++ b/src/Container/Container.php @@ -1,5 +1,4 @@ cursor = null; - $this->entitySchema = $entitySchema; $this->collection = $collection; $this->command = $command; $this->params = $params; @@ -94,9 +74,9 @@ public function __construct( * * @param int $amount the number of results to return * - * @return Cursor returns this cursor + * @return static */ - public function limit(int $amount) + public function limit(int $amount): CursorInterface { $this->params[1]['limit'] = $amount; @@ -110,9 +90,9 @@ public function limit(int $amount) * Each element in the array has as key the field name, * and as value either 1 for ascending sort, or -1 for descending sort. * - * @return Cursor returns this cursor + * @return static */ - public function sort(array $fields) + public function sort(array $fields): CursorInterface { $this->params[1]['sort'] = $fields; @@ -124,9 +104,9 @@ public function sort(array $fields) * * @param int $amount the number of results to skip * - * @return Cursor returns this cursor + * @return static */ - public function skip(int $amount) + public function skip(int $amount): CursorInterface { $this->params[1]['skip'] = $amount; @@ -139,7 +119,7 @@ public function skip(int $amount) * * @param bool $flag toggle timeout on or off * - * @return Cursor returns this cursor + * @return static */ public function disableTimeout(bool $flag = true) { @@ -183,9 +163,9 @@ public function rewind() { try { $this->getCursor()->rewind(); - } catch (LogicException $e) { + } catch (LogicException | BaseLogicException $e) { $this->fresh(); - $this->getCursor()->rewind(); + $this->getCursor(); } $this->position = 0; @@ -199,19 +179,9 @@ public function rewind() */ public function current() { - $document = $this->getCursor()->current(); - - if ($document instanceof ActiveRecord) { - $documentToArray = $document->toArray(); - $this->entitySchema = $document->getSchema(); - } else { - $documentToArray = (array) $document; - } + $cursor = $this->getCursor(); - return $this->getAssembler()->assemble( - $documentToArray, - $this->entitySchema - ); + return $cursor->valid() ? $cursor->current() : null; } /** @@ -222,13 +192,8 @@ public function current() public function first() { $this->rewind(); - $document = $this->getCursor()->current(); - if (!$document) { - return; - } - - return $this->getAssembler()->assemble($document, $this->entitySchema); + return $this->current(); } /** @@ -262,8 +227,6 @@ public function next() /** * Iterator valid method (used in foreach). - * - * @return bool */ public function valid(): bool { @@ -298,38 +261,6 @@ public function toArray(): array return $result ?? []; } - /** - * Actually returns a Traversable object with the DriverCursor within. - * If it does not exists yet, create it using the $collection, $command and - * $params given. - * - * @return Traversable - */ - protected function getCursor(): Traversable - { - if (!$this->cursor) { - $driverCursor = $this->collection->{$this->command}(...$this->params); - $this->cursor = new IteratorIterator($driverCursor); - $this->cursor->rewind(); - } - - return $this->cursor; - } - - /** - * Retrieves an EntityAssembler instance. - * - * @return EntityAssembler - */ - protected function getAssembler() - { - if (!$this->assembler) { - $this->assembler = Ioc::make(EntityAssembler::class); - } - - return $this->assembler; - } - /** * Serializes this object storing the collection name instead of the actual * MongoDb\Collection (which is unserializable). @@ -340,6 +271,7 @@ public function serialize() { $properties = get_object_vars($this); $properties['collection'] = $this->collection->getCollectionName(); + unset($properties['cursor']); return serialize($properties); } @@ -353,9 +285,9 @@ public function unserialize($serialized) { $attributes = unserialize($serialized); - $conn = Ioc::make(Pool::class)->getConnection(); - $db = $conn->defaultDatabase; - $collectionObject = $conn->getRawConnection()->$db->{$attributes['collection']}; + $connection = Container::make(Connection::class); + $db = $connection->defaultDatabase; + $collectionObject = $connection->getClient()->$db->{$attributes['collection']}; foreach ($attributes as $key => $value) { $this->$key = $value; @@ -363,4 +295,19 @@ public function unserialize($serialized) $this->collection = $collectionObject; } + + /** + * Actually returns a Traversable object with the DriverCursor within. + * If it does not exists yet, create it using the $collection, $command and + * $params given. + */ + protected function getCursor(): Iterator + { + if (!$this->cursor) { + $driverCursor = $this->collection->{$this->command}(...$this->params); + $this->cursor = new CachingIterator($driverCursor); + } + + return $this->cursor; + } } diff --git a/src/Mongolid/Cursor/CursorInterface.php b/src/Cursor/CursorInterface.php similarity index 75% rename from src/Mongolid/Cursor/CursorInterface.php rename to src/Cursor/CursorInterface.php index aa979a8a..e0f43319 100644 --- a/src/Mongolid/Cursor/CursorInterface.php +++ b/src/Cursor/CursorInterface.php @@ -1,23 +1,23 @@ items = $items; - $this->entityClass = $entityClass; } /** @@ -54,7 +38,7 @@ public function __construct(string $entityClass, array $items) * * @return EmbeddedCursor returns this cursor */ - public function limit(int $amount) + public function limit(int $amount): CursorInterface { $this->items = array_slice($this->items, 0, $amount); @@ -70,11 +54,11 @@ public function limit(int $amount) * * @return EmbeddedCursor returns this cursor */ - public function sort(array $fields) + public function sort(array $fields): CursorInterface { foreach (array_reverse($fields) as $key => $direction) { // Uses usort with a function that will access the $key and sort in - // the $direction. It mimics how the mongodb does sorting internally. + // the $direction. It mimics how MongoDB does sorting internally. usort( $this->items, function ($a, $b) use ($key, $direction) { @@ -101,7 +85,7 @@ function ($a, $b) use ($key, $direction) { * * @return EmbeddedCursor returns this cursor */ - public function skip(int $amount) + public function skip(int $amount): CursorInterface { $this->items = array_slice($this->items, $amount); @@ -134,40 +118,7 @@ public function rewind() */ public function current() { - if (!$this->valid()) { - return; - } - - $document = $this->items[$this->position]; - - if ($document instanceof $this->entityClass) { - return $document; - } - - $schema = $this->getSchemaForEntity(); - $entityAssembler = Ioc::makeWith(EntityAssembler::class, compact('schema')); - - return $entityAssembler->assemble($document, $schema); - } - - /** - * Retrieve a schema based on Entity Class. - * - * @return Schema - */ - protected function getSchemaForEntity(): Schema - { - if ($this->entityClass instanceof Schema) { - return $this->entityClass; - } - - $model = new $this->entityClass(); - - if ($model instanceof ActiveRecord) { - return $model->getSchema(); - } - - return new DynamicSchema(); + return $this->items[$this->position] ?? null; } /** diff --git a/src/Mongolid/Event/EventTriggerInterface.php b/src/Event/EventTriggerInterface.php similarity index 99% rename from src/Mongolid/Event/EventTriggerInterface.php rename to src/Event/EventTriggerInterface.php index 8b6aaf0d..4265af7a 100644 --- a/src/Mongolid/Event/EventTriggerInterface.php +++ b/src/Event/EventTriggerInterface.php @@ -1,5 +1,4 @@ dispatcher = $dispatcher; + $this->dispatcher = $builder; } /** diff --git a/src/Model/AbstractModel.php b/src/Model/AbstractModel.php new file mode 100644 index 00000000..b6772d1d --- /dev/null +++ b/src/Model/AbstractModel.php @@ -0,0 +1,293 @@ +where(new static(), $query, $projection); + } + + /** + * Gets a cursor of this kind of entities from the database. + */ + public static function all(): CursorInterface + { + return self::getBuilderInstance()->all(new static()); + } + + /** + * Gets the first model of this kind that matches the query. + * + * @param mixed $query mongoDB selection criteria + * @param array $projection fields to project in Mongo query + * + * @return AbstractModel|null + */ + public static function first($query = [], array $projection = []) + { + return self::getBuilderInstance()->first(new static(), $query, $projection); + } + + /** + * Gets the first model of this kind that matches the query. If no + * document was found, throws ModelNotFoundException. + * + * @param mixed $query mongoDB selection criteria + * @param array $projection fields to project in Mongo query + * + * @throws ModelNotFoundException If no document was found + * + * @return AbstractModel|null + */ + public static function firstOrFail($query = [], array $projection = []) + { + return self::getBuilderInstance()->firstOrFail(new static(), $query, $projection); + } + + /** + * Gets the first model of this kind that matches the query. If no + * document was found, a new model will be returned with the + * _if field filled. + * + * @param mixed $id document id + * + * @return AbstractModel|null + */ + public static function firstOrNew($id) + { + if (!$model = self::first($id)) { + $model = new static(); + $model->_id = $id; + } + + return $model; + } + + /** + * Returns a valid instance from Ioc. + */ + private static function getBuilderInstance(): Builder + { + $instance = new static(); + + return $instance->getBuilder(); + } + + /** + * Saves this object into database. + */ + public function save(): bool + { + return $this->execute('save'); + } + + /** + * Insert this object into database. + */ + public function insert(): bool + { + return $this->execute('insert'); + } + + /** + * Updates this object in database. + */ + public function update(): bool + { + return $this->execute('update'); + } + + /** + * Deletes this object in database. + */ + public function delete(): bool + { + return $this->execute('delete'); + } + + /** + * Dynamically retrieve attributes on the model. + * + * @param string $key name of the attribute + * + * @return mixed + */ + public function &__get(string $key) + { + return $this->getDocumentAttribute($key); + } + + /** + * Dynamically set attributes on the model. + * + * @param string $key attribute name + * @param mixed $value value to be set + */ + public function __set(string $key, $value): void + { + $this->setDocumentAttribute($key, $value); + } + + /** + * Determine if an attribute exists on the model. + * + * @param string $key attribute name + */ + public function __isset(string $key): bool + { + return $this->hasDocumentAttribute($key); + } + + /** + * Unset an attribute on the model. + * + * @param string $key attribute name + */ + public function __unset(string $key): void + { + $this->cleanDocumentAttribute($key); + } + + /** + * {@inheritdoc} + */ + public function getCollectionName(): string + { + if (!$this->collection) { + throw new NoCollectionNameException(); + } + + return $this->collection; + } + + /** + * {@inheritdoc} + */ + public function getCollection(): Collection + { + $connection = Container::make(Connection::class); + + $database = $connection->defaultDatabase; + $collectionName = $this->getCollectionName(); + + return $connection->getClient()->$database->$collectionName; + } + + /** + * Getter for $writeConcern attribute. + */ + public function getWriteConcern(): int + { + return $this->writeConcern; + } + + /** + * Setter for $writeConcern attribute. + * + * @param int $writeConcern level of write concern for the transaction + */ + public function setWriteConcern(int $writeConcern): void + { + $this->writeConcern = $writeConcern; + } + + public function bsonSerialize() + { + return Container::make(ModelMapper::class) + ->map($this, array_merge($this->fillable, $this->guarded), $this->dynamic, $this->timestamps); + } + + public function bsonUnserialize(array $data) + { + unset($data['__pclass']); + static::fill($data, $this, true); + + $this->syncOriginalDocumentAttributes(); + } + + /** + * Performs the given action into database. + * + * @param string $action Builder function to execute + */ + protected function execute(string $action): bool + { + $options = [ + 'writeConcern' => new WriteConcern($this->getWriteConcern()), + ]; + + if ($result = $this->getBuilder()->$action($this, $options)) { + $this->syncOriginalDocumentAttributes(); + } + + return $result; + } + + /** + * Returns a Builder configured with the collection described + * in this model. + */ + private function getBuilder(): Builder + { + return Container::make(Builder::class); + } +} diff --git a/src/Model/Exception/InvalidFieldNameException.php b/src/Model/Exception/InvalidFieldNameException.php new file mode 100644 index 00000000..b459a8e7 --- /dev/null +++ b/src/Model/Exception/InvalidFieldNameException.php @@ -0,0 +1,8 @@ +polymorph(array_merge($object->getDocumentAttributes(), $input)); + + if ($class !== get_class($object)) { + $originalAttributes = $object->getDocumentAttributes(); + $object = new $class(); + + foreach ($originalAttributes as $key => $value) { + $object->setDocumentAttribute($key, $value); + } + } + } + + foreach ($input as $key => $value) { + if ($force + || ((!$object->fillable || in_array($key, $object->fillable)) && !in_array($key, $object->guarded))) { + if ($value instanceof stdClass) { + $value = json_decode(json_encode($value), true); // cast to array + } + + $object->setDocumentAttribute($key, $value); + } + } + + return $object; + } + + /** + * {@inheritdoc} + */ + public function hasDocumentAttribute(string $key): bool + { + return !is_null($this->getDocumentAttribute($key)); + } + + /** + * {@inheritdoc} + */ + public function &getDocumentAttribute(string $key) + { + if ($this->mutable && $this->hasMutatorMethod($key, 'get')) { + $this->mutableCache[$key] = $this->{$this->buildMutatorMethod($key, 'get')}(); + + return $this->mutableCache[$key]; + } + + if (array_key_exists($key, $this->attributes)) { + return $this->attributes[$key]; + } + + if (!method_exists(self::class, $key) && method_exists($this, $key)) { + return $this->getRelationResults($key); + } + + $this->attributes[$key] = null; + + return $this->attributes[$key]; + } + + /** + * {@inheritdoc} + */ + public function getDocumentAttributes(): array + { + foreach ($this->attributes as $field => $value) { + if (null === $value) { + $this->cleanDocumentAttribute($field); + } + } + + return $this->attributes ?? []; + } + + /** + * {@inheritdoc} + */ + public function cleanDocumentAttribute(string $key) + { + unset($this->attributes[$key]); + + if ($this->hasFieldRelation($key)) { + $this->unsetRelation($this->getFieldRelation($key)); + } + } + + /** + * {@inheritdoc} + */ + public function setDocumentAttribute(string $key, $value) + { + if ($this->mutable && $this->hasMutatorMethod($key, 'set')) { + $value = $this->{$this->buildMutatorMethod($key, 'set')}($value); + } + + if (null === $value) { + $this->cleanDocumentAttribute($key); + + return; + } + + $this->attributes[$key] = $value; + + if ($this->hasFieldRelation($key)) { + $this->unsetRelation($this->getFieldRelation($key)); + } + } + + /** + * {@inheritdoc} + */ + public function syncOriginalDocumentAttributes() + { + try { + $this->originalAttributes = unserialize(serialize($this->getDocumentAttributes())); + } catch (Exception $e) { + $this->originalAttributes = $this->getDocumentAttributes(); + } + } + + /** + * {@inheritdoc} + */ + public function getOriginalDocumentAttributes(): array + { + return $this->originalAttributes; + } + + /** + * {@inheritdoc} + */ + public function toArray(): array + { + return $this->getDocumentAttributes(); + } + + /** + * Verify if model has a mutator method defined. + * + * @param string $key attribute name + * @param string $prefix method prefix to be used (get, set) + */ + protected function hasMutatorMethod(string $key, $prefix): bool + { + $method = $this->buildMutatorMethod($key, $prefix); + + return method_exists($this, $method); + } + + /** + * Create mutator method pattern. + * + * @param string $key attribute name + * @param string $prefix method prefix to be used (get, set) + */ + protected function buildMutatorMethod(string $key, string $prefix): string + { + return $prefix.Str::studly($key).'DocumentAttribute'; + } +} diff --git a/src/Model/HasRelationsTrait.php b/src/Model/HasRelationsTrait.php new file mode 100644 index 00000000..e9484c4f --- /dev/null +++ b/src/Model/HasRelationsTrait.php @@ -0,0 +1,236 @@ +relations[$relation]; + } + + /** + * Determine if the given relation is loaded. + */ + public function relationLoaded(string $key): bool + { + return array_key_exists($key, $this->relations); + } + + /** + * Set the given relationship on the model. + */ + public function setRelation(string $relation, RelationInterface $value, string $field): void + { + $this->validateField($relation, $field); + $this->relations[$relation] = $value; + $this->fieldRelations[$field] = $relation; + } + + /** + * Unset a loaded relationship. + */ + public function unsetRelation(string $relation): void + { + unset($this->relations[$relation]); + } + + public function &getRelationResults(string $relation) + { + if (!$this->relationLoaded($relation) && !$this->$relation() instanceof RelationInterface) { + throw new NotARelationException("Called method \"{$relation}\" is not a Relation!"); + } + + return $this->getRelation($relation)->getResults(); + } + + public function hasFieldRelation(string $field): bool + { + return isset($this->fieldRelations[$field]); + } + + public function getFieldRelation(string $field): string + { + return $this->fieldRelations[$field]; + } + + /** + * Create a ReferencesOne Relation. + * + * @param string $modelClass class of the referenced model + * @param string|null $field the field where the $key is stored + * @param string $key the field that the document will be referenced by (usually _id) + */ + protected function referencesOne(string $modelClass, string $field = null, string $key = '_id'): ReferencesOne + { + $relationName = $this->guessRelationName(); + + if (!$this->relationLoaded($relationName)) { + $field = $field ?: $this->inferFieldForReference($relationName, $key, false); + + $relation = new ReferencesOne($this, $modelClass, $field, $key); + $this->setRelation($relationName, $relation, $field); + } + + return $this->getRelation($relationName); + } + + /** + * Create a ReferencesMany Relation. + * + * @param string $modelClass class of the referenced model + * @param string|null $field the field where the _ids are stored + * @param string $key the field that the document will be referenced by (usually _id) + */ + protected function referencesMany(string $modelClass, string $field = null, string $key = '_id'): ReferencesMany + { + $relationName = $this->guessRelationName(); + + if (!$this->relationLoaded($relationName)) { + $field = $field ?: $this->inferFieldForReference($relationName, $key, true); + + $relation = new ReferencesMany($this, $modelClass, $field, $key); + $this->setRelation($relationName, $relation, $field); + } + + return $this->getRelation($relationName); + } + + /** + * Create a EmbedsOne Relation. + * + * @param string $modelClass class of the embedded model + * @param string|null $field field where the embedded document is stored + */ + protected function embedsOne(string $modelClass, string $field = null): EmbedsOne + { + $relationName = $this->guessRelationName(); + + if (!$this->relationLoaded($relationName)) { + $field = $field ?: $this->inferFieldForEmbed($relationName); + + $relation = new EmbedsOne($this, $modelClass, $field); + $this->setRelation($relationName, $relation, $field); + } + + return $this->getRelation($relationName); + } + + /** + * Create a EmbedsMany Relation. + * + * @param string $modelClass class of the embedded model + * @param string|null $field field where the embedded documents are stored + */ + protected function embedsMany(string $modelClass, string $field = null): EmbedsMany + { + $relationName = $this->guessRelationName(); + + if (!$this->relationLoaded($relationName)) { + $field = $field ?: $this->inferFieldForEmbed($relationName); + + $relation = new EmbedsMany($this, $modelClass, $field); + $this->setRelation($relationName, $relation, $field); + } + + return $this->getRelation($relationName); + } + + /** + * Retrieve relation name. For example, if we have a code like this: + * + * ``` + * class User extends AbstractModel + * { + * public function brother() + * { + * return $this->referencesOne(User::class); + * } + * } + * ``` + * we will retrieve `brother` as the relation name. + * This is useful for storing the "Brother Reference" + * on a field called `brother_id`. + */ + private function guessRelationName(): string + { + [$method, $relationType, $relation] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); + + return $relation['function']; + } + + /** + * Infer field name for reference relations. + * This is useful for storing the relation on + * a field based on both the relation name and the + * referenced key used. + * + * @example a `parent` relation on a `code` field + * would be inferred as `parent_code`. + * @example a `addresses` relation on `_id` field + * would be inferred as `addresses_ids`. + */ + private function inferFieldForReference(string $relationName, string $key, bool $plural): string + { + $relationName = Str::snake($relationName); + $key = $plural ? Str::plural($key) : $key; + + return $relationName.'_'.ltrim($key, '_'); + } + + /** + * Infer field name for embed relations. + * This is useful for storing the relation on + * a field based on the relation name. + * + * @example a `comments` relation on would be inferred as `embedded_comments`. + * @example a `tag` relation on would be inferred as `embedded_tag`. + */ + private function inferFieldForEmbed(string $relationName): string + { + $relationName = Str::snake($relationName); + + return 'embedded_'.$relationName; + } + + /** + * Ensure that fieldName is not the same as the relationName. + * Otherwise, we would ran into trouble using magic accessors for relations. + */ + private function validateField(string $relationName, string $fieldName): void + { + if ($relationName === $fieldName) { + throw new InvalidFieldNameException( + "The field for relation \"{$relationName}\" cannot have the same name as the relation" + ); + } + } +} diff --git a/src/Model/ModelInterface.php b/src/Model/ModelInterface.php new file mode 100644 index 00000000..7a850d79 --- /dev/null +++ b/src/Model/ModelInterface.php @@ -0,0 +1,112 @@ +parent = $parent; + $this->model = $model; + $this->field = $field; + } + + /** + * Retrieve Relation Results. + * + * @return mixed + */ + abstract public function get(); + + /** + * Retrieve cached Relation Results. + * + * @return mixed + */ + public function &getResults() + { + if (!$this->pristine()) { + $this->results = $this->get(); + $this->pristine = true; + } + + return $this->results; + } + + protected function pristine(): bool + { + return $this->pristine; + } + + /** + * Gets the key of the given model. If there is no key, + * a new key will be generated and set on the model (while still returning it). + * + * @param ModelInterface $model the object that the key will be retrieved from + * + * @return ObjectId|mixed + */ + protected function getKey(ModelInterface $model) + { + if (!$model->{$this->key}) { + $model->{$this->key} = new ObjectId(); + } + + return $model->{$this->key}; + } +} diff --git a/src/Model/Relations/EmbedsMany.php b/src/Model/Relations/EmbedsMany.php new file mode 100644 index 00000000..fdfe015a --- /dev/null +++ b/src/Model/Relations/EmbedsMany.php @@ -0,0 +1,85 @@ +remove($model); + + $fieldValue = $this->parent->{$this->field}; + $fieldValue[] = $model; + $this->parent->{$this->field} = array_values($fieldValue); + $this->pristine = false; + } + + /** + * Embed many documents at once. + * + * @param array $entities model + */ + public function addMany(array $entities): void + { + foreach ($entities as $model) { + $this->add($model); + } + } + + /** + * Replace embedded documents. + * + * @param array $entities + */ + public function replace(array $entities): void + { + $this->removeAll(); + $this->addMany($entities); + } + + /** + * Removes an embedded document from the given field. It does that by using + * the _id of given $model. + * + * @param mixed $model model or _id + */ + public function remove(ModelInterface $model): void + { + $embeddedKey = $this->getKey($model); + + foreach ((array) $this->parent->{$this->field} as $arrayKey => $document) { + if ($embeddedKey == $this->getKey($document)) { + unset($this->parent->{$this->field}[$arrayKey]); + } + } + + $this->parent->{$this->field} = array_values((array) $this->parent->{$this->field}); + $this->pristine = false; + } + + public function removeAll(): void + { + unset($this->parent->{$this->field}); + $this->pristine = false; + } + + /** + * @return EmbeddedCursor + */ + public function get() + { + $items = $this->parent->{$this->field} ?? []; + + if (is_object($items)) { + $items = [$items]; + } + + return new EmbeddedCursor($items); + } +} diff --git a/src/Model/Relations/EmbedsOne.php b/src/Model/Relations/EmbedsOne.php new file mode 100644 index 00000000..1a18d864 --- /dev/null +++ b/src/Model/Relations/EmbedsOne.php @@ -0,0 +1,27 @@ +parent->{$this->field} = $model; + $this->pristine = false; + } + + public function remove(): void + { + unset($this->parent->{$this->field}); + $this->pristine = false; + } + + /** + * @return ModelInterface|null + */ + public function get() + { + return $this->parent->{$this->field}; + } +} diff --git a/src/Model/Relations/ReferencesMany.php b/src/Model/Relations/ReferencesMany.php new file mode 100644 index 00000000..12247b02 --- /dev/null +++ b/src/Model/Relations/ReferencesMany.php @@ -0,0 +1,104 @@ +key = $key; + $this->modelInstance = Container::make($this->model); + } + + /** + * Attach model reference. It will also generate an + * _id for the model if it's not present. + */ + public function attach(ModelInterface $model): void + { + $referencedKey = $this->getKey($model); + $fieldValue = (array) $this->parent->{$this->field}; + + foreach ($fieldValue as $key) { + if ($key == $referencedKey) { + return; + } + } + + $fieldValue[] = $referencedKey; + $this->parent->{$this->field} = array_values($fieldValue); + $this->pristine = false; + } + + /** + * Attach many models at once. + * + * @param ModelInterface[] $entities + */ + public function attachMany(array $entities): void + { + foreach ($entities as $model) { + $this->attach($model); + } + } + + /** + * Replace attached documents. + * + * @param ModelInterface[] $entities + */ + public function replace(array $entities): void + { + $this->detachAll(); + $this->attachMany($entities); + } + + /** + * Removes model reference from an attribute. + */ + public function detach(ModelInterface $model): void + { + $referencedKey = $this->getKey($model); + + foreach ((array) $this->parent->{$this->field} as $arrayKey => $documentKey) { + if ($documentKey == $referencedKey) { + unset($this->parent->{$this->field}[$arrayKey]); + $this->parent->{$this->field} = array_values((array) $this->parent->{$this->field}); + $this->pristine = false; + return; + } + } + } + + /** + * Removes all model references from relation. + */ + public function detachAll(): void + { + unset($this->parent->{$this->field}); + $this->pristine = false; + } + + public function get() + { + $referencedKeys = (array) $this->parent->{$this->field}; + + if (ObjectIdUtils::isObjectId($referencedKeys[0] ?? '')) { + foreach ($referencedKeys as $key => $value) { + $referencedKeys[$key] = new ObjectId((string) $value); + } + } + + return $this->modelInstance->where([$this->key => ['$in' => array_values($referencedKeys)]]); + } +} diff --git a/src/Model/Relations/ReferencesOne.php b/src/Model/Relations/ReferencesOne.php new file mode 100644 index 00000000..2fb017ee --- /dev/null +++ b/src/Model/Relations/ReferencesOne.php @@ -0,0 +1,47 @@ +key = $key; + $this->modelInstance = Container::make($this->model); + } + + public function attach(ModelInterface $model): void + { + $this->parent->{$this->field} = $this->getKey($model); + $this->pristine = false; + } + + public function detach(): void + { + unset($this->parent->{$this->field}); + $this->pristine = false; + } + + public function get() + { + if (!$referencedKey = $this->parent->{$this->field}) { + return null; + } + + if (is_string($referencedKey) && ObjectIdUtils::isObjectId($referencedKey)) { + $referencedKey = new ObjectId($referencedKey); + } + + return $this->modelInstance->first([$this->key => $referencedKey]); + } +} diff --git a/src/Model/Relations/RelationInterface.php b/src/Model/Relations/RelationInterface.php new file mode 100644 index 00000000..425e3ea9 --- /dev/null +++ b/src/Model/Relations/RelationInterface.php @@ -0,0 +1,12 @@ + 'objectId', - 'created_at' => 'createdAtTimestamp', - 'updated_at' => 'updatedAtTimestamp', - ]; - - /** - * The $dynamic property tells if the object will accept additional fields - * that are not specified in the $fields property. This is useful if you - * does not have a strict document format or if you want to take full - * advantage of the "schemaless" nature of MongoDB. - * - * @var bool - */ - public $dynamic = true; - - /** - * Saves this object into database. - * - * @return bool Success - */ - public function save() - { - return $this->execute('save'); - } - - /** - * Insert this object into database. - * - * @return bool Success - */ - public function insert() - { - return $this->execute('insert'); - } - - /** - * Updates this object in database. - * - * @return bool Success - */ - public function update() - { - return $this->execute('update'); - } - - /** - * Deletes this object in database. - * - * @return bool Success - */ - public function delete() - { - return $this->execute('delete'); - } - - /** - * Gets a cursor of this kind of entities that matches the query from the - * database. - * - * @param array $query mongoDB selection criteria - * @param array $projection fields to project in Mongo query - * @param bool $useCache retrieves a CacheableCursor instead - * - * @return \Mongolid\Cursor\Cursor - */ - public static function where( - array $query = [], - array $projection = [], - bool $useCache = false - ) { - return self::getDataMapperInstance()->where( - $query, - $projection, - $useCache - ); - } - - /** - * Gets a cursor of this kind of entities from the database. - * - * @return \Mongolid\Cursor\Cursor - */ - public static function all() - { - return self::getDataMapperInstance()->all(); - } - - /** - * Gets the first entity of this kind that matches the query. - * - * @param mixed $query mongoDB selection criteria - * @param array $projection fields to project in Mongo query - * @param bool $useCache retrieves the entity through a CacheableCursor - * - * @return ActiveRecord - */ - public static function first( - $query = [], - array $projection = [], - bool $useCache = false - ) { - return self::getDataMapperInstance()->first( - $query, - $projection, - $useCache - ); - } - - /** - * Gets the first entity of this kind that matches the query. If no - * document was found, throws ModelNotFoundException. - * - * @param mixed $query mongoDB selection criteria - * @param array $projection fields to project in Mongo query - * @param bool $useCache retrieves the entity through a CacheableCursor - * - * @throws ModelNotFoundException if no document was found - * - * @return ActiveRecord - */ - public static function firstOrFail( - $query = [], - array $projection = [], - bool $useCache = false - ) { - return self::getDataMapperInstance()->firstOrFail( - $query, - $projection, - $useCache - ); - } - - /** - * Gets the first entity of this kind that matches the query. If no - * document was found, a new entity will be returned with the - * _if field filled. - * - * @param mixed $id document id - * - * @return ActiveRecord - */ - public static function firstOrNew($id) - { - if ($entity = self::getDataMapperInstance()->first($id)) { - return $entity; - } - - $entity = new static(); - $entity->_id = $id; - - return $entity; - } - - /** - * Handle dynamic method calls into the model. - * - * @param mixed $method name of the method that is being called - * @param mixed $parameters parameters of $method - * - * @throws BadMethodCallException in case of invalid methods be called - * - * @return mixed - */ - public function __call($method, $parameters) - { - $value = $parameters[0] ?? null; - - // Alias to attach - if ('attachTo' == substr($method, 0, 8)) { - $field = lcfirst(substr($method, 8)); - - return $this->attach($field, $value); - } - - // Alias to embed - if ('embedTo' == substr($method, 0, 7)) { - $field = lcfirst(substr($method, 7)); - - return $this->embed($field, $value); - } - - throw new BadMethodCallException( - sprintf( - 'The following method can not be reached or does not exist: %s@%s', - get_class($this), - $method - ) - ); - } - - /** - * Returns a DataMapper configured with the Schema and collection described - * in this entity. - * - * @return DataMapper - */ - public function getDataMapper() - { - $dataMapper = Ioc::make(DataMapper::class); - $dataMapper->setSchema($this->getSchema()); - - return $dataMapper; - } - - /** - * Getter for the $collection attribute. - * - * @return string - */ - public function getCollectionName() - { - return $this->collection ? $this->collection : $this->getSchema()->collection; - } - - /** - * Getter for $writeConcern variable. - * - * @return mixed - */ - public function getWriteConcern() - { - return $this->writeConcern; - } - - /** - * Setter for $writeConcern variable. - * - * @param mixed $writeConcern level of write concern to the transation - */ - public function setWriteConcern($writeConcern) - { - $this->writeConcern = $writeConcern; - } - - /** - * {@inheritdoc} - */ - public function getSchema(): Schema - { - if ($schema = $this->instantiateSchemaInFields()) { - return $schema; - } - - $schema = new DynamicSchema(); - $schema->entityClass = get_class($this); - $schema->fields = $this->fields; - $schema->dynamic = $this->dynamic; - $schema->collection = $this->collection; - - return $schema; - } - - /** - * Will check if the current value of $fields property is the name of a - * Schema class and instantiate it if possible. - * - * @return Schema|null - */ - protected function instantiateSchemaInFields() - { - if (is_string($this->fields)) { - if (is_subclass_of($instance = Ioc::make($this->fields), Schema::class)) { - return $instance; - } - } - } - - /** - * Performs the given action into database. - * - * @param string $action datamapper function to execute - * - * @return bool - */ - protected function execute(string $action) - { - if (!$this->getCollectionName()) { - return false; - } - - $options = [ - 'writeConcern' => new WriteConcern($this->getWriteConcern()), - ]; - - if ($result = $this->getDataMapper()->$action($this, $options)) { - $this->syncOriginalAttributes(); - } - - return $result; - } - - /** - * Returns the a valid instance from Ioc. - * - * @throws NoCollectionNameException throws exception when has no collection filled - * - * @return mixed - */ - private static function getDataMapperInstance() - { - $instance = Ioc::make(get_called_class()); - - if (!$instance->getCollectionName()) { - throw new NoCollectionNameException(); - } - - return $instance->getDataMapper(); - } -} diff --git a/src/Mongolid/Connection/Pool.php b/src/Mongolid/Connection/Pool.php deleted file mode 100644 index 1e4c7837..00000000 --- a/src/Mongolid/Connection/Pool.php +++ /dev/null @@ -1,57 +0,0 @@ -connections = Ioc::make('SplQueue'); - } - - /** - * Gets a connection from the pool. It will cycle through the existent - * connections. - * - * @return Connection - */ - public function getConnection() - { - if ($chosenConn = $this->connections->pop()) { - $this->connections->push($chosenConn); - - return $chosenConn; - } - } - - /** - * Adds a new connection to the pool. - * - * @param Connection $conn the actual connection that will be added to the pool - * - * @return bool Success - */ - public function addConnection(Connection $conn) - { - $this->connections->push($conn); - - return true; - } -} diff --git a/src/Mongolid/Cursor/CacheableCursor.php b/src/Mongolid/Cursor/CacheableCursor.php deleted file mode 100644 index d8714c8f..00000000 --- a/src/Mongolid/Cursor/CacheableCursor.php +++ /dev/null @@ -1,178 +0,0 @@ -ignoreCache || $this->position >= self::DOCUMENT_LIMIT) { - return $this->getOriginalCursor(); - } - - // Returns cached set of documents - if ($this->documents) { - return $this->documents; - } - - // Check if there is a cached set of documents - $cacheComponent = Ioc::make(CacheComponentInterface::class); - $cacheKey = $this->generateCacheKey(); - - try { - $cachedDocuments = $cacheComponent->get($cacheKey, null); - } catch (ErrorException $error) { - $cachedDocuments = []; - } - - if ($cachedDocuments) { - return $this->documents = new ArrayIterator($cachedDocuments); - } - - // Stores the original "limit" clause of the query - $this->storeOriginalLimit(); - - // Stores the documents within the object and cache then for later use - $this->documents = []; - foreach (parent::getCursor() as $document) { - $this->documents[] = $document; - } - - $cacheComponent->put($cacheKey, $this->documents, 0.6); - - // Drops the unserializable DriverCursor. - $this->cursor = null; - - // Return the documents iterator - return $this->documents = new ArrayIterator($this->documents); - } - - /** - * Generates an unique cache key for the cursor in it's current state. - * - * @return string cache key to identify the query of the current cursor - */ - protected function generateCacheKey(): string - { - return sprintf( - '%s:%s:%s', - $this->command, - $this->collection->getNamespace(), - md5(serialize($this->params)) - ); - } - - /** - * Stores the original "limit" clause of the query. - */ - protected function storeOriginalLimit() - { - if (isset($this->params[1]['limit'])) { - $this->originalLimit = $this->params[1]['limit']; - } - - if ($this->originalLimit > self::DOCUMENT_LIMIT) { - $this->limit(self::DOCUMENT_LIMIT); - } - } - - /** - * Gets the limit clause of the query if any. - * - * @return mixed Int or null - */ - protected function getLimit() - { - return $this->originalLimit ?: ($this->params[1]['limit'] ?? null); - } - - /** - * Returns the DriverCursor considering the documents that have already - * been retrieved from cache. - * - * @return Traversable - */ - protected function getOriginalCursor(): Traversable - { - if ($this->ignoreCache) { - return parent::getCursor(); - } - - if ($this->getLimit()) { - $this->params[1]['limit'] = $this->getLimit() - $this->position; - } - - $skipped = $this->params[1]['skip'] ?? 0; - - $this->skip($skipped + $this->position - 1); - - $this->ignoreCache = true; - - return $this->getOriginalCursor(); - } - - /** - * Serializes this object. Drops the unserializable DriverCursor. In order - * to make the CacheableCursor object serializable. - * - * @return string serialized object - */ - public function serialize() - { - $this->documents = $this->cursor = null; - - return parent::serialize(); - } -} diff --git a/src/Mongolid/Cursor/CursorFactory.php b/src/Mongolid/Cursor/CursorFactory.php deleted file mode 100644 index 06b0b9c5..00000000 --- a/src/Mongolid/Cursor/CursorFactory.php +++ /dev/null @@ -1,48 +0,0 @@ -setBulkWrite(new MongoBulkWrite(['ordered' => false])); - $this->schema = $entity->getSchema(); - } - - /** - * Get the BulkWrite object to perform other operations - * not covered by this class. - * - * @return MongoBulkWrite - */ - public function getBulkWrite() - { - return $this->bulkWrite; - } - - /** - * Set BulkWrite object that will receive all operations - * and later be executed. - * - * @param MongoBulkWrite $bulkWrite - * - * @return $this - */ - public function setBulkWrite(MongoBulkWrite $bulkWrite) - { - $this->bulkWrite = $bulkWrite; - - return $this; - } - - /** - * Add an `update` operation to the Bulk, where only one record is updated, by `_id` or `query`. - * Be aware that working with multiple levels of nesting on `$dataToSet` may have - * an undesired behavior that could lead to data loss on a specific key. - * - * @see https://docs.mongodb.com/manual/reference/operator/update/set/#set-top-level-fields - * - * @param ObjectId|string|array $id - * @param array $dataToSet - * @param array $options - * @param string $operator - */ - public function updateOne( - $filter, - array $dataToSet, - array $options = ['upsert' => true], - string $operator = '$set' - ) { - $filter = is_array($filter) ? $filter : ['_id' => $filter]; - - return $this->getBulkWrite()->update( - $filter, - [$operator => $dataToSet], - $options - ); - } - - /** - * Execute the BulkWrite, using a connection from the Pool. - * The collection is inferred from entity's collection name. - * - * @param int $writeConcern - * - * @return \MongoDB\Driver\WriteResult - */ - public function execute($writeConcern = 1) - { - $connection = Ioc::make(Pool::class)->getConnection(); - $manager = $connection->getRawManager(); - - $namespace = $connection->defaultDatabase.'.'.$this->schema->collection; - - return $manager->executeBulkWrite( - $namespace, - $this->getBulkWrite(), - ['writeConcern' => new WriteConcern($writeConcern)] - ); - } -} diff --git a/src/Mongolid/DataMapper/DataMapper.php b/src/Mongolid/DataMapper/DataMapper.php deleted file mode 100644 index 68f22262..00000000 --- a/src/Mongolid/DataMapper/DataMapper.php +++ /dev/null @@ -1,573 +0,0 @@ -connPool = $connPool; - } - - /** - * Upserts the given object into database. Returns success if write concern - * is acknowledged. - * - * Notice: Saves with Unacknowledged WriteConcern will not fire `saved` event. - * - * @param mixed $entity the entity used in the operation - * @param array $options possible options to send to mongo driver - * - * @return bool Success (but always false if write concern is Unacknowledged) - */ - public function save($entity, array $options = []) - { - // If the "saving" event returns false we'll bail out of the save and return - // false, indicating that the save failed. This gives an opportunities to - // listeners to cancel save operations if validations fail or whatever. - if (false === $this->fireEvent('saving', $entity, true)) { - return false; - } - - $data = $this->parseToDocument($entity); - - $queryResult = $this->getCollection()->replaceOne( - ['_id' => $data['_id']], - $data, - $this->mergeOptions($options, ['upsert' => true]) - ); - - $result = $queryResult->isAcknowledged() && - ($queryResult->getModifiedCount() || $queryResult->getUpsertedCount()); - - if ($result) { - $this->afterSuccess($entity); - - $this->fireEvent('saved', $entity); - } - - return $result; - } - - /** - * Inserts the given object into database. Returns success if write concern - * is acknowledged. Since it's an insert, it will fail if the _id already - * exists. - * - * Notice: Inserts with Unacknowledged WriteConcern will not fire `inserted` event. - * - * @param mixed $entity the entity used in the operation - * @param array $options possible options to send to mongo driver - * @param bool $fireEvents whether events should be fired - * - * @return bool Success (but always false if write concern is Unacknowledged) - */ - public function insert($entity, array $options = [], bool $fireEvents = true): bool - { - if ($fireEvents && false === $this->fireEvent('inserting', $entity, true)) { - return false; - } - - $data = $this->parseToDocument($entity); - - $queryResult = $this->getCollection()->insertOne( - $data, - $this->mergeOptions($options) - ); - - $result = $queryResult->isAcknowledged() && $queryResult->getInsertedCount(); - - if ($result) { - $this->afterSuccess($entity); - - if ($fireEvents) { - $this->fireEvent('inserted', $entity); - } - } - - return $result; - } - - /** - * Updates the given object into database. Returns success if write concern - * is acknowledged. Since it's an update, it will fail if the document with - * the given _id did not exists. - * - * Notice: Updates with Unacknowledged WriteConcern will not fire `updated` event. - * - * @param mixed $entity the entity used in the operation - * @param array $options possible options to send to mongo driver - * - * @return bool Success (but always false if write concern is Unacknowledged) - */ - public function update($entity, array $options = []): bool - { - if (false === $this->fireEvent('updating', $entity, true)) { - return false; - } - - if (!$entity->_id) { - if ($result = $this->insert($entity, $options, false)) { - $this->afterSuccess($entity); - - $this->fireEvent('updated', $entity); - } - - return $result; - } - - $data = $this->parseToDocument($entity); - - $queryResult = $this->getCollection()->updateOne( - ['_id' => $data['_id']], - ['$set' => $data], - $this->mergeOptions($options) - ); - - $result = $queryResult->isAcknowledged() && $queryResult->getModifiedCount(); - - if ($result) { - $this->afterSuccess($entity); - - $this->fireEvent('updated', $entity); - } - - return $result; - } - - /** - * Removes the given document from the collection. - * - * Notice: Deletes with Unacknowledged WriteConcern will not fire `deleted` event. - * - * @param mixed $entity the entity used in the operation - * @param array $options possible options to send to mongo driver - * - * @return bool Success (but always false if write concern is Unacknowledged) - */ - public function delete($entity, array $options = []): bool - { - if (false === $this->fireEvent('deleting', $entity, true)) { - return false; - } - - $data = $this->parseToDocument($entity); - - $queryResult = $this->getCollection()->deleteOne( - ['_id' => $data['_id']], - $this->mergeOptions($options) - ); - - if ($queryResult->isAcknowledged() && - $queryResult->getDeletedCount() - ) { - $this->fireEvent('deleted', $entity); - - return true; - } - - return false; - } - - /** - * Retrieve a database cursor that will return $this->schema->entityClass - * objects that upon iteration. - * - * @param mixed $query mongoDB query to retrieve documents - * @param array $projection fields to project in Mongo query - * @param bool $cacheable retrieves a CacheableCursor instead - * - * @return \Mongolid\Cursor\Cursor - */ - public function where( - $query = [], - array $projection = [], - bool $cacheable = false - ): Cursor { - $cursorClass = $cacheable ? CacheableCursor::class : Cursor::class; - - $cursor = new $cursorClass( - $this->schema, - $this->getCollection(), - 'find', - [ - $this->prepareValueQuery($query), - ['projection' => $this->prepareProjection($projection)], - ] - ); - - return $cursor; - } - - /** - * Retrieve a database cursor that will return all documents as - * $this->schema->entityClass objects upon iteration. - * - * @return \Mongolid\Cursor\Cursor - */ - public function all(): Cursor - { - return $this->where([]); - } - - /** - * Retrieve one $this->schema->entityClass objects that matches the given - * query. - * - * @param mixed $query mongoDB query to retrieve the document - * @param array $projection fields to project in Mongo query - * @param bool $cacheable retrieves the first through a CacheableCursor - * - * @return mixed First document matching query as an $this->schema->entityClass object - */ - public function first( - $query = [], - array $projection = [], - bool $cacheable = false - ) { - if ($cacheable) { - return $this->where($query, $projection, true)->first(); - } - - $document = $this->getCollection()->findOne( - $this->prepareValueQuery($query), - ['projection' => $this->prepareProjection($projection)] - ); - - if (!$document) { - return; - } - - $model = $this->getAssembler()->assemble($document, $this->schema); - - return $model; - } - - /** - * Retrieve one $this->schema->entityClass objects that matches the given - * query. If no document was found, throws ModelNotFoundException. - * - * @param mixed $query mongoDB query to retrieve the document - * @param array $projection fields to project in Mongo query - * @param bool $cacheable retrieves the first through a CacheableCursor - * - * @throws ModelNotFoundException if no document was found - * - * @return mixed First document matching query as an $this->schema->entityClass object - */ - public function firstOrFail( - $query = [], - array $projection = [], - bool $cacheable = false - ) { - if ($result = $this->first($query, $projection, $cacheable)) { - return $result; - } - - throw (new ModelNotFoundException())->setModel($this->schema->entityClass); - } - - /** - * Parses an object with SchemaMapper and the given Schema. - * - * @param mixed $entity the object to be parsed - * - * @return array Document - */ - protected function parseToDocument($entity) - { - $schemaMapper = $this->getSchemaMapper(); - $parsedDocument = $schemaMapper->map($entity); - - if (is_object($entity)) { - foreach ($parsedDocument as $field => $value) { - $entity->$field = $value; - } - } - - return $parsedDocument; - } - - /** - * Returns a SchemaMapper with the $schema or $schemaClass instance. - * - * @return SchemaMapper - */ - protected function getSchemaMapper() - { - if (!$this->schema) { - $this->schema = Ioc::make($this->schemaClass); - } - - return Ioc::makeWith(SchemaMapper::class, ['schema' => $this->schema]); - } - - /** - * Retrieves the Collection object. - * - * @return Collection - */ - protected function getCollection(): Collection - { - $conn = $this->connPool->getConnection(); - $database = $conn->defaultDatabase; - $collection = $this->schema->collection; - - return $conn->getRawConnection()->$database->$collection; - } - - /** - * Transforms a value that is not an array into an MongoDB query (array). - * This method will take care of converting a single value into a query for - * an _id, including when a objectId is passed as a string. - * - * @param mixed $value the _id of the document - * - * @return array Query for the given _id - */ - protected function prepareValueQuery($value): array - { - if (!is_array($value)) { - $value = ['_id' => $value]; - } - - if (isset($value['_id']) && - is_string($value['_id']) && - ObjectIdUtils::isObjectId($value['_id']) - ) { - $value['_id'] = new ObjectId($value['_id']); - } - - if (isset($value['_id']) && - is_array($value['_id']) - ) { - $value['_id'] = $this->prepareArrayFieldOfQuery($value['_id']); - } - - return $value; - } - - /** - * Prepares an embedded array of an query. It will convert string ObjectIds - * in operators into actual objects. - * - * @param array $value array that will be treated - * - * @return array prepared array - */ - protected function prepareArrayFieldOfQuery(array $value): array - { - foreach (['$in', '$nin'] as $operator) { - if (isset($value[$operator]) && - is_array($value[$operator]) - ) { - foreach ($value[$operator] as $index => $id) { - if (ObjectIdUtils::isObjectId($id)) { - $value[$operator][$index] = new ObjectId($id); - } - } - } - } - - return $value; - } - - /** - * Retrieves an EntityAssembler instance. - * - * @return EntityAssembler - */ - protected function getAssembler() - { - if (!$this->assembler) { - $this->assembler = Ioc::make(EntityAssembler::class); - } - - return $this->assembler; - } - - /** - * Triggers an event. May return if that event had success. - * - * @param string $event identification of the event - * @param mixed $entity event payload - * @param bool $halt true if the return of the event handler will be used in a conditional - * - * @return mixed event handler return - */ - protected function fireEvent(string $event, $entity, bool $halt = false) - { - $event = "mongolid.{$event}: ".get_class($entity); - - $this->eventService ? $this->eventService : $this->eventService = Ioc::make(EventTriggerService::class); - - return $this->eventService->fire($event, $entity, $halt); - } - - /** - * Converts the given projection fields to Mongo driver format. - * - * How to use: - * As Mongo projection using boolean values: - * From: ['name' => true, '_id' => false] - * To: ['name' => true, '_id' => false] - * As Mongo projection using integer values - * From: ['name' => 1, '_id' => -1] - * To: ['name' => true, '_id' => false] - * As an array of string: - * From: ['name', '_id'] - * To: ['name' => true, '_id' => true] - * As an array of string to exclude some fields: - * From: ['name', '-_id'] - * To: ['name' => true, '_id' => false] - * - * @param array $fields fields to project - * - * @throws InvalidArgumentException if the given $fields are not a valid projection - * - * @return array - */ - protected function prepareProjection(array $fields) - { - $projection = []; - foreach ($fields as $key => $value) { - if (is_string($key)) { - if (is_bool($value)) { - $projection[$key] = $value; - - continue; - } - if (is_int($value)) { - $projection[$key] = ($value >= 1); - - continue; - } - } - - if (is_int($key) && is_string($value)) { - $key = $value; - if (0 === strpos($value, '-')) { - $key = substr($key, 1); - $value = false; - } else { - $value = true; - } - - $projection[$key] = $value; - - continue; - } - - throw new InvalidArgumentException( - sprintf( - "Invalid projection: '%s' => '%s'", - $key, - $value - ) - ); - } - - return $projection; - } - - /** - * Merge all options. - * - * @param array $defaultOptions default options array - * @param array $toMergeOptions to merge options array - * - * @return array - */ - private function mergeOptions(array $defaultOptions = [], array $toMergeOptions = []) - { - return array_merge($defaultOptions, $toMergeOptions); - } - - /** - * Perform actions on object before firing the after event. - * - * @param mixed $entity - */ - private function afterSuccess($entity) - { - if ($entity instanceof AttributesAccessInterface) { - $entity->syncOriginalAttributes(); - } - } - - /** - * {@inheritdoc} - */ - public function getSchema(): Schema - { - return $this->schema; - } - - /** - * Set a Schema object that describes an Entity in MongoDB. - * - * @param Schema $schema - */ - public function setSchema(Schema $schema) - { - $this->schema = $schema; - } -} diff --git a/src/Mongolid/DataMapper/EntityAssembler.php b/src/Mongolid/DataMapper/EntityAssembler.php deleted file mode 100644 index 70412293..00000000 --- a/src/Mongolid/DataMapper/EntityAssembler.php +++ /dev/null @@ -1,115 +0,0 @@ -entityClass; - $model = Ioc::make($entityClass); - - foreach ($document as $field => $value) { - $fieldType = $schema->fields[$field] ?? null; - - if ($fieldType && 'schema.' == substr($fieldType, 0, 7)) { - $value = $this->assembleDocumentsRecursively($value, substr($fieldType, 7)); - } - - $model->$field = $value; - } - - $entity = $this->morphingTime($model); - - return $this->prepareOriginalAttributes($entity); - } - - /** - * Returns the return of polymorph method of the given entity if available. - * - * @see \Mongolid\Model\PolymorphableInterface::polymorph - * @see https://i.ytimg.com/vi/TFGN9kAjdis/maxresdefault.jpg - * - * @param mixed $entity the entity that may or may not have a polymorph method - * - * @return mixed the result of $entity->polymorph or the $entity itself - */ - protected function morphingTime($entity) - { - if ($entity instanceof PolymorphableInterface) { - return $entity->polymorph(); - } - - return $entity; - } - - /** - * Stores original attributes from Entity if needed. - * - * @param mixed $entity the entity that may have the attributes stored - * - * @return mixed the entity with original attributes - */ - protected function prepareOriginalAttributes($entity) - { - if ($entity instanceof AttributesAccessInterface) { - $entity->syncOriginalAttributes(); - } - - return $entity; - } - - /** - * Assembly multiple documents for the given $schemaClass recursively. - * - * @param mixed $value a value of an embeded field containing entity data to be assembled - * @param string $schemaClass the schemaClass to be used when assembling the entities within $value - * - * @return mixed - */ - protected function assembleDocumentsRecursively($value, string $schemaClass) - { - $value = (array) $value; - - if (empty($value)) { - return; - } - - $schema = Ioc::make($schemaClass); - $assembler = Ioc::make(self::class); - - if (!isset($value[0])) { - $value = [$value]; - } - - foreach ($value as $key => $subValue) { - $value[$key] = $assembler->assemble($subValue, $schema); - } - - return $value; - } -} diff --git a/src/Mongolid/DataMapper/SchemaMapper.php b/src/Mongolid/DataMapper/SchemaMapper.php deleted file mode 100644 index 6e074fe1..00000000 --- a/src/Mongolid/DataMapper/SchemaMapper.php +++ /dev/null @@ -1,170 +0,0 @@ -schema = $schema; - } - - /** - * Maps the input $data to the schema specified in the $schema property. - * - * @param array|object $data array or object with the fields that should - * be mapped to $this->schema specifications - * - * @return array - */ - public function map($data) - { - $data = $this->parseToArray($data); - $this->clearDynamic($data); - - // Parse each specified field - foreach ($this->schema->fields as $key => $fieldType) { - $data[$key] = $this->parseField($data[$key] ?? null, $fieldType); - } - - return $data; - } - - /** - * If the schema is not dynamic, remove all non specified fields. - * - * @param array $data Reference of the fields. The passed array will be modified. - */ - protected function clearDynamic(array &$data) - { - if (!$this->schema->dynamic) { - $data = array_intersect_key($data, $this->schema->fields); - } - } - - /** - * Parse a value based on a field yype of the schema. - * - * @param mixed $value value to be parsed - * @param string $fieldType description of how the field should be treated - * - * @return mixed $value Value parsed to match $type - */ - public function parseField($value, string $fieldType) - { - // Uses $fieldType method of the schema to parse the value - if (method_exists($this->schema, $fieldType)) { - return $this->schema->$fieldType($value); - } - - // Returns null or an empty array - if (null === $value || is_array($value) && empty($value)) { - return $value; - } - - // If fieldType is castable (Ex: 'int') - if (in_array($fieldType, $this->castableTypes)) { - return $this->cast($value, $fieldType); - } - - // If the field type points to another schema. - if ('schema.' == substr($fieldType, 0, 7)) { - return $this->mapToSchema($value, substr($fieldType, 7)); - } - - return $value; - } - - /** - * Uses PHP's settype to cast a value to a type. - * - * @see http://php.net/manual/pt_BR/function.settype.php - * - * @param mixed $value value to be casted - * @param string $type type to which the $value should be casted to - * - * @return mixed - */ - protected function cast($value, string $type) - { - settype($value, $type); - - return $value; - } - - /** - * Instantiate another SchemaMapper with the given $schemaClass and maps - * the given $value. - * - * @param mixed $value value that will be mapped - * @param string $schemaClass class that will be passed to the new SchemaMapper constructor - * - * @return mixed - */ - protected function mapToSchema($value, string $schemaClass) - { - $value = (array) $value; - $schema = Ioc::make($schemaClass); - $mapper = Ioc::makeWith(self::class, compact('schema')); - - if (!isset($value[0])) { - $value = [$value]; - } - - foreach ($value as $key => $subValue) { - $value[$key] = $mapper->map($subValue); - } - - return $value; - } - - /** - * Parses an object to an array before sending it to the SchemaMapper. - * - * @param mixed $object the object that will be transformed into an array - * - * @return array - */ - protected function parseToArray($object): array - { - if (!is_array($object)) { - $attributes = method_exists($object, 'getAttributes') - ? $object->getAttributes() - : get_object_vars($object); - - return $attributes; - } - - return $object; - } -} diff --git a/src/Mongolid/Manager.php b/src/Mongolid/Manager.php deleted file mode 100644 index 68aad958..00000000 --- a/src/Mongolid/Manager.php +++ /dev/null @@ -1,156 +0,0 @@ -addConnection(new Connection); - * // And then start persisting and querying your models. - */ -class Manager -{ - /** - * Singleton instance of the manager. - * - * @var Manager - */ - protected static $singleton; - - /** - * Container being used by Mongolid. - * - * @var \Illuminate\Contracts\Container\Container - */ - public $container; - - /** - * Mongolid connection pool being object. - * - * @var Pool - */ - public $connectionPool; - - /** - * Mongolid cache component object. - * - * @var CacheComponent - */ - public $cacheComponent; - - /** - * Stores the schemas that have been registered for later use. This may be - * useful when using Mongolid DataMapper pattern. - * - * @var array - */ - protected $schemas = []; - - /** - * Main entry point to openning a connection and start using Mongolid in - * pure PHP. After adding a connection into the Manager you are ready to - * persist and query your models. - * - * @param Connection $connection connection instance to be used in database interactions - * - * @return bool Success - */ - public function addConnection(Connection $connection): bool - { - $this->init(); - $this->connectionPool->addConnection($connection); - - return true; - } - - /** - * Get the raw MongoDB connection. - * - * @return \MongoDB\Client - */ - public function getConnection() - { - $this->init(); - - return $this->connectionPool->getConnection()->getRawConnection(); - } - - /** - * Sets the event trigger for Mongolid events. - * - * @param EventTriggerInterface $eventTrigger external event trigger - */ - public function setEventTrigger(EventTriggerInterface $eventTrigger) - { - $this->init(); - $eventService = new EventTriggerService(); - $eventService->registerEventDispatcher($eventTrigger); - - $this->container->instance(EventTriggerService::class, $eventService); - } - - /** - * Allow document Schemas to be registered for later use. - * - * @param Schema $schema schema being registered - */ - public function registerSchema(Schema $schema) - { - $this->schemas[$schema->entityClass] = $schema; - } - - /** - * Retrieves a DataMapper for the given $entityClass. This can only be done - * if the Schema for that entity has been previously registered with - * registerSchema() method. - * - * @param string $entityClass class of the entity that needs to be mapped - * - * @return DataMapper|null dataMapper configured for the $entityClass - */ - public function getMapper(string $entityClass) - { - if (isset($this->schemas[$entityClass])) { - $dataMapper = Ioc::make(DataMapper::class); - $dataMapper->setSchema($this->schemas[$entityClass] ?? null); - - return $dataMapper; - } - } - - /** - * Initializes the Mongolid manager. - */ - protected function init() - { - if ($this->container) { - return; - } - - $this->container = new Container(); - $this->connectionPool = new Pool(); - $this->cacheComponent = new CacheComponent(); - - $this->container->instance(Pool::class, $this->connectionPool); - $this->container->instance(CacheComponentInterface::class, $this->cacheComponent); - Ioc::setContainer($this->container); - - static::$singleton = $this; - } -} diff --git a/src/Mongolid/Model/Attributes.php b/src/Mongolid/Model/Attributes.php deleted file mode 100644 index 2a492cef..00000000 --- a/src/Mongolid/Model/Attributes.php +++ /dev/null @@ -1,227 +0,0 @@ -attributes); - - if ($inAttributes) { - return $this->attributes[$key]; - } elseif ('attributes' == $key) { - return $this->attributes; - } - } - - /** - * Get all attributes from the model. - * - * @return mixed - */ - public function getAttributes() - { - return $this->attributes; - } - - /** - * Set the model attributes using an array. - * - * @param array $input the data that will be used to fill the attributes - * @param bool $force force fill - */ - public function fill(array $input, bool $force = false) - { - foreach ($input as $key => $value) { - if ($force) { - $this->setAttribute($key, $value); - - continue; - } - - if ((empty($this->fillable) || in_array($key, $this->fillable)) && !in_array($key, $this->guarded)) { - $this->setAttribute($key, $value); - } - } - } - - /** - * Set a given attribute on the model. - * - * @param string $key name of the attribute to be unset - */ - public function cleanAttribute(string $key) - { - unset($this->attributes[$key]); - } - - /** - * Set a given attribute on the model. - * - * @param string $key name of the attribute to be set - * @param mixed $value value to be set - */ - public function setAttribute(string $key, $value) - { - $this->attributes[$key] = $value; - } - - /** - * Stores original attributes from actual data from attributes - * to be used in future comparisons about changes. - * - * Ideally should be called once right after retrieving data from - * the database. - */ - public function syncOriginalAttributes() - { - $this->original = $this->attributes; - } - - /** - * Verify if model has a mutator method defined. - * - * @param mixed $key attribute name - * @param mixed $prefix method prefix to be used - * - * @return bool - */ - protected function hasMutatorMethod($key, $prefix) - { - $method = $this->buildMutatorMethod($key, $prefix); - - return method_exists($this, $method); - } - - /** - * Create mutator method pattern. - * - * @param mixed $key attribute name - * @param mixed $prefix method prefix to be used - * - * @return string - */ - protected function buildMutatorMethod($key, $prefix) - { - return $prefix.ucfirst($key).'Attribute'; - } - - /** - * Returns the model instance as an Array. - * - * @return array - */ - public function toArray() - { - return $this->getAttributes(); - } - - /** - * Dynamically retrieve attributes on the model. - * - * @param mixed $key name of the attribute - * - * @return mixed - */ - public function __get($key) - { - if ($this->mutable && $this->hasMutatorMethod($key, 'get')) { - return $this->{$this->buildMutatorMethod($key, 'get')}(); - } - - return $this->getAttribute($key); - } - - /** - * Dynamically set attributes on the model. - * - * @param mixed $key attribute name - * @param mixed $value value to be set - */ - public function __set($key, $value) - { - if ($this->mutable && $this->hasMutatorMethod($key, 'set')) { - $value = $this->{$this->buildMutatorMethod($key, 'set')}($value); - } - - $this->setAttribute($key, $value); - } - - /** - * Determine if an attribute exists on the model. - * - * @param mixed $key attribute name - * - * @return bool - */ - public function __isset($key) - { - return !is_null($this->{$key}); - } - - /** - * Unset an attribute on the model. - * - * @param mixed $key attribute name - */ - public function __unset($key) - { - unset($this->attributes[$key]); - } -} diff --git a/src/Mongolid/Model/AttributesAccessInterface.php b/src/Mongolid/Model/AttributesAccessInterface.php deleted file mode 100644 index 475eed5d..00000000 --- a/src/Mongolid/Model/AttributesAccessInterface.php +++ /dev/null @@ -1,62 +0,0 @@ -unembed($parent, $field, $entity); - - $fieldValue = $parent->$field; - $fieldValue[] = $entity; - $parent->$field = array_values($fieldValue); - - return true; - } - - /** - * Removes the given $entity from $field of $parent. This method will - * consider the _id of the $entity in order to remove it. - * - * @param mixed $parent the object where the $entity will be removed - * @param string $field name of the field of the object where the document is - * @param mixed $entity entity that will be removed from $parent - * - * @return bool Success - */ - public function unembed($parent, string $field, &$entity): bool - { - $fieldValue = (array) $parent->$field; - $id = $this->getId($entity); - - foreach ($fieldValue as $key => $document) { - if ($id == $this->getId($document)) { - unset($fieldValue[$key]); - } - } - - $parent->$field = array_values($fieldValue); - - return true; - } - - /** - * Attach a new _id reference into $field of $parent. - * - * @param mixed $parent the object where $entity will be referenced - * @param string $field the field where the _id reference of $entity will be stored - * @param object|array $entity the object that is being attached - * - * @return bool Success - */ - public function attach($parent, string $field, &$entity): bool - { - $fieldValue = (array) $parent->$field; - $newId = $this->getId($entity); - - foreach ($fieldValue as $id) { - if ($id == $newId) { - return true; - } - } - - $fieldValue[] = $newId; - $parent->$field = $fieldValue; - - return true; - } - - /** - * Removes an _id reference from $field of $parent. - * - * @param mixed $parent the object where $entity reference will be removed - * @param string $field the field where the _id reference of $entity is stored - * @param mixed $entity the object being detached or its _id - * - * @return bool Success - */ - public function detach($parent, string $field, &$entity): bool - { - $fieldValue = (array) $parent->$field; - $newId = $this->getId($entity); - - foreach ($fieldValue as $key => $id) { - if ($id == $newId) { - unset($fieldValue[$key]); - } - } - - $parent->$field = array_values($fieldValue); - - return true; - } - - /** - * Gets the _id of the given object or array. If there is no _id in it a new - * _id will be generated and set on the object (while still returning it). - * - * @param mixed $object the object|array that the _id will be retrieved from - * - * @return ObjectId|mixed - */ - protected function getId(&$object) - { - if (is_array($object)) { - if (isset($object['_id']) && $object['_id']) { - return $object['_id']; - } - - return $object['_id'] = new ObjectId(); - } - - if (is_object($object) && !$object instanceof ObjectId) { - if (isset($object->_id) && $object->_id) { - return $object->_id; - } - - return $object->_id = new ObjectId(); - } - - return $object; - } -} diff --git a/src/Mongolid/Model/PolymorphableInterface.php b/src/Mongolid/Model/PolymorphableInterface.php deleted file mode 100644 index db42a607..00000000 --- a/src/Mongolid/Model/PolymorphableInterface.php +++ /dev/null @@ -1,44 +0,0 @@ -video != null) { - * $obj = new VideoContent; - * $obj->fill($this->attributes); - * - * return $obj; - * } else { - * return $this; - * } - * } - * - * In the example above, if you call Content::first() and the content - * returned have the key video set, then the object returned will be - * a VideoContent instead of a Content. - * - * @return mixed - */ - public function polymorph(); -} diff --git a/src/Mongolid/Model/Relations.php b/src/Mongolid/Model/Relations.php deleted file mode 100644 index ea079348..00000000 --- a/src/Mongolid/Model/Relations.php +++ /dev/null @@ -1,177 +0,0 @@ -$field; - - if (is_array($referenced_id) && isset($referenced_id[0])) { - $referenced_id = $referenced_id[0]; - } - - $entityInstance = Ioc::make($entity); - - if ($entityInstance instanceof Schema) { - $dataMapper = Ioc::make(DataMapper::class); - $dataMapper->setSchema($entityInstance); - - return $dataMapper->first(['_id' => $referenced_id], [], $cacheable); - } - - return $entityInstance::first(['_id' => $referenced_id], [], $cacheable); - } - - /** - * Returns the cursor for the referenced documents as objects. - * - * @param string $entity class of the entity or of the schema of the entity - * @param string $field the field where the _ids are stored - * @param bool $cacheable retrieves a CacheableCursor instead - * - * @return array - */ - protected function referencesMany(string $entity, string $field, bool $cacheable = true) - { - $referencedIds = (array) $this->$field; - - if (ObjectIdUtils::isObjectId($referencedIds[0] ?? '')) { - foreach ($referencedIds as $key => $value) { - $referencedIds[$key] = new ObjectId($value); - } - } - - $query = ['_id' => ['$in' => array_values($referencedIds)]]; - - $entityInstance = Ioc::make($entity); - - if ($entityInstance instanceof Schema) { - $dataMapper = Ioc::make(DataMapper::class); - $dataMapper->setSchema($entityInstance); - - return $dataMapper->where($query, [], $cacheable); - } - - return $entityInstance::where($query, [], $cacheable); - } - - /** - * Return a embedded documents as object. - * - * @param string $entity class of the entity or of the schema of the entity - * @param string $field field where the embedded document is stored - * - * @return Model|null - */ - protected function embedsOne(string $entity, string $field) - { - if (is_subclass_of($entity, Schema::class)) { - $entity = (new $entity())->entityClass; - } - - $items = (array) $this->$field; - if (false === empty($items) && false === array_key_exists(0, $items)) { - $items = [$items]; - } - - return Ioc::make(CursorFactory::class) - ->createEmbeddedCursor($entity, $items)->first(); - } - - /** - * Return array of embedded documents as objects. - * - * @param string $entity class of the entity or of the schema of the entity - * @param string $field field where the embedded documents are stored - * - * @return EmbeddedCursor Array with the embedded documents - */ - protected function embedsMany(string $entity, string $field) - { - if (is_subclass_of($entity, Schema::class)) { - $entity = (new $entity())->entityClass; - } - - $items = (array) $this->$field; - if (false === empty($items) && false === array_key_exists(0, $items)) { - $items = [$items]; - } - - return Ioc::make(CursorFactory::class) - ->createEmbeddedCursor($entity, $items); - } - - /** - * Embed a new document to an attribute. It will also generate an - * _id for the document if it's not present. - * - * @param string $field field to where the $obj will be embedded - * @param mixed $obj document or model instance - */ - public function embed(string $field, &$obj) - { - $embedder = Ioc::make(DocumentEmbedder::class); - $embedder->embed($this, $field, $obj); - } - - /** - * Removes an embedded document from the given field. It does that by using - * the _id of the given $obj. - * - * @param string $field name of the field where the $obj is embeded - * @param mixed $obj document, model instance or _id - */ - public function unembed(string $field, &$obj) - { - $embedder = Ioc::make(DocumentEmbedder::class); - $embedder->unembed($this, $field, $obj); - } - - /** - * Attach document _id reference to an attribute. It will also generate an - * _id for the document if it's not present. - * - * @param string $field name of the field where the reference will be stored - * @param mixed $obj document, model instance or _id to be referenced - */ - public function attach(string $field, &$obj) - { - $embedder = Ioc::make(DocumentEmbedder::class); - $embedder->attach($this, $field, $obj); - } - - /** - * Removes a document _id reference from an attribute. It will remove the - * _id of the given $obj from inside the given $field. - * - * @param string $field field where the reference is stored - * @param mixed $obj document, model instance or _id that have been referenced by $field - */ - public function detach(string $field, &$obj) - { - $embedder = Ioc::make(DocumentEmbedder::class); - $embedder->detach($this, $field, $obj); - } -} diff --git a/src/Mongolid/Schema/DynamicSchema.php b/src/Mongolid/Schema/DynamicSchema.php deleted file mode 100644 index 43c3ee45..00000000 --- a/src/Mongolid/Schema/DynamicSchema.php +++ /dev/null @@ -1,16 +0,0 @@ -' This represents an embedded document (or - * sub-document). - * - * @var string[] - */ - public $fields = [ - '_id' => 'objectId', // Means that the _id will pass trough the `objectId` method - 'created_at' => 'createdAtTimestamp', // Generates an automatic timestamp - 'updated_at' => 'updatedAtTimestamp', - ]; - - /** - * Name of the class that will be used to represent a document of this - * Schema when retrieve from the database. - * - * @var string - */ - public $entityClass = 'stdClass'; - - /** - * Filters any field in the $fields that has it's value specified as a - * 'objectId'. It will wraps the $value, if any, into a ObjectId object. - * - * @param mixed $value value that may be converted to ObjectId - * - * @return ObjectId|mixed - */ - public function objectId($value = null) - { - if (null === $value) { - return new ObjectId(); - } - - if (is_string($value) && ObjectIdUtils::isObjectId($value)) { - $value = new ObjectId($value); - } - - return $value; - } - - /** - * Prepares the field to have a sequence. If $value is zero or not defined - * a new auto-increment number will be "generated" for the collection of - * the schema. The sequence generation is done by the SequenceService. - * - * @param int|null $value value that will be evaluated - * - * @return int - */ - public function sequence(int $value = null) - { - if ($value) { - return $value; - } - - return Ioc::make(SequenceService::class) - ->getNextValue($this->collection ?: $this->entityClass); - } - - /** - * Prepares the field to be the datetime that the document has been created. - * - * @param mixed|null $value value that will be evaluated - * - * @return UTCDateTime - */ - public function createdAtTimestamp($value) - { - if ($value instanceof UTCDateTime) { - return $value; - } - - return $this->updatedAtTimestamp(); - } - - /** - * Prepares the field to be now. - * - * @return UTCDateTime - */ - public function updatedAtTimestamp() - { - return new UTCDateTime(); - } -} diff --git a/src/Mongolid/Util/CacheComponent.php b/src/Mongolid/Util/CacheComponent.php deleted file mode 100644 index 9f0326e9..00000000 --- a/src/Mongolid/Util/CacheComponent.php +++ /dev/null @@ -1,86 +0,0 @@ -has($key)) { - return $this->storage[$key]; - } - } - - /** - * Store an item in the cache for a given number of minutes. - * - * @param string $key cache key of the item - * @param mixed $value value being stored in cache - * @param float $minutes cache ttl - */ - public function put(string $key, $value, float $minutes) - { - $this->storage[$key] = $value; - $this->ttl[$key] = $this->time() + 60 * $minutes; - } - - /** - * Determine if an item exists in the cache. This method will also check - * if the ttl of the given cache key has been expired and will free the - * memory if so. - * - * @param string $key cache key of the item - * - * @return bool has cache key - */ - public function has(string $key): bool - { - if (array_key_exists($key, $this->ttl) && - $this->time() - $this->ttl[$key] > 0 - ) { - unset($this->ttl[$key]); - unset($this->storage[$key]); - - return false; - } - - return array_key_exists($key, $this->storage); - } - - /** - * Return the current time in order to check ttl. - * - * @codeCoverageIgnore - * - * @return int return current Unix timestamp - */ - protected function time() - { - return time(); - } -} diff --git a/src/Mongolid/Util/CacheComponentInterface.php b/src/Mongolid/Util/CacheComponentInterface.php deleted file mode 100644 index e895e04f..00000000 --- a/src/Mongolid/Util/CacheComponentInterface.php +++ /dev/null @@ -1,39 +0,0 @@ -connection = $connection; + } + + /** + * Upserts the given object into database. Returns success if write concern + * is acknowledged. + * + * Notice: Saves with Unacknowledged WriteConcern will not fire `saved` event. + * Return is always false if write concern is Unacknowledged. + * + * @param ModelInterface $model the model used in the operation + * @param array $options possible options to send to mongo driver + */ + public function save(ModelInterface $model, array $options = []): bool + { + // If the "saving" event returns false we'll bail out of the save and return + // false, indicating that the save failed. This gives an opportunities to + // listeners to cancel save operations if validations fail or whatever. + if (false === $this->fireEvent('saving', $model, true)) { + return false; + } + + $model->bsonSerialize(); + + $queryResult = $model->getCollection()->replaceOne( + ['_id' => $model->_id], + $model, + $this->mergeOptions($options, ['upsert' => true]) + ); + + $result = $queryResult->isAcknowledged() && + ($queryResult->getModifiedCount() || $queryResult->getUpsertedCount()); + + if ($result) { + $this->afterSuccess($model); + + $this->fireEvent('saved', $model); + } + + return $result; + } + + /** + * Inserts the given object into database. Returns success if write concern + * is acknowledged. Since it's an insert, it will fail if the _id already + * exists. + * + * Notice: Inserts with Unacknowledged WriteConcern will not fire `inserted` event. + * Return is always false if write concern is Unacknowledged. + * + * @param ModelInterface $model the model used in the operation + * @param array $options possible options to send to mongo driver + * @param bool $fireEvents whether events should be fired + */ + public function insert(ModelInterface $model, array $options = [], bool $fireEvents = true): bool + { + if ($fireEvents && false === $this->fireEvent('inserting', $model, true)) { + return false; + } + + $queryResult = $model->getCollection()->insertOne( + $model, + $this->mergeOptions($options) + ); + + $result = $queryResult->isAcknowledged() && $queryResult->getInsertedCount(); + + if ($result) { + $this->afterSuccess($model); + + if ($fireEvents) { + $this->fireEvent('inserted', $model); + } + } + + return $result; + } + + /** + * Updates the given object into database. Returns success if write concern + * is acknowledged. Since it's an update, it will fail if the model with + * the given _id did not exists. + * + * Notice: Updates with Unacknowledged WriteConcern will not fire `updated` event. + * Return is always false if write concern is Unacknowledged. + * + * @param ModelInterface $model the model used in the operation + * @param array $options possible options to send to mongo driver + */ + public function update(ModelInterface $model, array $options = []): bool + { + if (false === $this->fireEvent('updating', $model, true)) { + return false; + } + + if (!$model->_id) { + if ($result = $this->insert($model, $options, false)) { + $this->afterSuccess($model); + + $this->fireEvent('updated', $model); + } + + return $result; + } + + $updateData = $this->getUpdateData($model, $model->bsonSerialize()); + + $queryResult = $model->getCollection()->updateOne( + ['_id' => $model->_id], + $updateData, + $this->mergeOptions($options) + ); + + $result = $queryResult->isAcknowledged() && $queryResult->getModifiedCount(); + + if ($result) { + $this->afterSuccess($model); + + $this->fireEvent('updated', $model); + } + + return $result; + } + + /** + * Removes the given document from the collection. + * + * Notice: Deletes with Unacknowledged WriteConcern will not fire `deleted` event. + * Return is always false if write concern is Unacknowledged. + * + * @param ModelInterface $model the model used in the operation + * @param array $options possible options to send to mongo driver + */ + public function delete(ModelInterface $model, array $options = []): bool + { + if (false === $this->fireEvent('deleting', $model, true)) { + return false; + } + + $queryResult = $model->getCollection()->deleteOne( + ['_id' => $model->_id], + $this->mergeOptions($options) + ); + + if ($queryResult->isAcknowledged() && + $queryResult->getDeletedCount() + ) { + $this->fireEvent('deleted', $model); + + return true; + } + + return false; + } + + /** + * Retrieve a database cursor that will return models that upon iteration. + * + * @param ModelInterface $model Model to query from collection + * @param mixed $query MongoDB query to retrieve documents + * @param array $projection fields to project in MongoDB query + */ + public function where(ModelInterface $model, $query = [], array $projection = []): CursorInterface + { + return new Cursor( + $model->getCollection(), + 'find', + [ + $this->prepareValueQuery($query), + ['projection' => $this->prepareProjection($projection)], + ] + ); + } + + /** + * Retrieve a database cursor that will return all models upon iteration. + * + * @param ModelInterface $model Model to query from collection + */ + public function all(ModelInterface $model): CursorInterface + { + return $this->where($model, []); + } + + /** + * Retrieve first model that matches given query. + * + * @param ModelInterface $model Model to query from collection + * @param mixed $query MongoDB query to retrieve the model + * @param array $projection fields to project in MongoDB query + * + * @return ModelInterface|array|null + */ + public function first(ModelInterface $model, $query = [], array $projection = []) + { + if (null === $query) { + return null; + } + + return $model->getCollection()->findOne( + $this->prepareValueQuery($query), + ['projection' => $this->prepareProjection($projection)] + ); + } + + /** + * Retrieve one model that matches given query. + * If no model was found, throws an exception. + * + * @param ModelInterface $model Model to query from collection + * @param mixed $query MongoDB query to retrieve the model + * @param array $projection fields to project in MongoDB query + * + * @throws ModelNotFoundException If no model was found + * + * @return ModelInterface|null + */ + public function firstOrFail(ModelInterface $model, $query = [], array $projection = []) + { + if ($result = $this->first($model, $query, $projection)) { + return $result; + } + + throw (new ModelNotFoundException())->setModel(get_class($model)); + } + + /** + * Transforms a value that is not an array into an MongoDB query (array). + * This method will take care of converting a single value into a query for + * an _id, including when a objectId is passed as a string. + * + * @param mixed $value the _id of the model + * + * @return array Query for the given _id + */ + protected function prepareValueQuery($value): array + { + if (!is_array($value)) { + $value = ['_id' => $value]; + } + + if (isset($value['_id']) && + is_string($value['_id']) && + ObjectIdUtils::isObjectId($value['_id']) + ) { + $value['_id'] = new ObjectId($value['_id']); + } + + if (isset($value['_id']) && + is_array($value['_id']) + ) { + $value['_id'] = $this->prepareArrayFieldOfQuery($value['_id']); + } + + return $value; + } + + /** + * Prepares an embedded array of an query. It will convert string ObjectIds + * in operators into actual objects. + * + * @param array $value array that will be treated + * + * @return array prepared array + */ + protected function prepareArrayFieldOfQuery(array $value): array + { + foreach (['$in', '$nin'] as $operator) { + if (isset($value[$operator]) && + is_array($value[$operator]) + ) { + foreach ($value[$operator] as $index => $id) { + if (ObjectIdUtils::isObjectId($id)) { + $value[$operator][$index] = new ObjectId($id); + } + } + } + } + + return $value; + } + + /** + * Triggers an event. May return if that event had success. + * + * @param string $event identification of the event + * @param mixed $model event payload + * @param bool $halt true if the return of the event handler will be used in a conditional + * + * @return mixed event handler return + */ + protected function fireEvent(string $event, ModelInterface $model, bool $halt = false) + { + $event = "mongolid.{$event}: ".get_class($model); + + $this->eventService ?: $this->eventService = Container::make(EventTriggerService::class); + + return $this->eventService->fire($event, $model, $halt); + } + + /** + * Converts the given projection fields to Mongo driver format. + * + * How to use: + * As Mongo projection using boolean values: + * From: ['name' => true, '_id' => false] + * To: ['name' => true, '_id' => false] + * As Mongo projection using integer values + * From: ['name' => 1, '_id' => -1] + * To: ['name' => true, '_id' => false] + * As an array of string: + * From: ['name', '_id'] + * To: ['name' => true, '_id' => true] + * As an array of string to exclude some fields: + * From: ['name', '-_id'] + * To: ['name' => true, '_id' => false] + * + * @param array $fields fields to project + * + * @throws InvalidArgumentException If the given $fields are not a valid projection + * + * @return array + */ + protected function prepareProjection(array $fields): array + { + $projection = []; + foreach ($fields as $key => $value) { + if (is_string($key)) { + if (is_bool($value)) { + $projection[$key] = $value; + + continue; + } + if (is_int($value)) { + $projection[$key] = ($value >= 1); + + continue; + } + } + + if (is_int($key) && is_string($value)) { + $key = $value; + if (0 === strpos($value, '-')) { + $key = substr($key, 1); + $value = false; + } else { + $value = true; + } + + $projection[$key] = $value; + + continue; + } + + throw new InvalidArgumentException( + sprintf( + "Invalid projection: '%s' => '%s'", + $key, + $value + ) + ); + } + + if ($projection) { + $projection['__pclass'] = true; + } + + return $projection; + } + + /** + * Based on the work of "bjori/mongo-php-transistor". + * Calculate `$set` and `$unset` arrays for update operation and store them on $changes. + * + * @see https://github.com/bjori/mongo-php-transistor/blob/70f5af00795d67f4d5a8c397e831435814df9937/src/Transistor.php#L108 + */ + private function calculateChanges(array &$changes, array $newData, array $oldData, string $keyfix = ''): void + { + foreach ($newData as $k => $v) { + if (!isset($oldData[$k])) { // new field + $changes['$set']["{$keyfix}{$k}"] = $v; + } elseif ($oldData[$k] != $v) { // changed value + if (is_array($v) && is_array($oldData[$k]) && $v) { // check array recursively for changes + $this->calculateChanges($changes, $v, $oldData[$k], "{$keyfix}{$k}."); + } else { + // overwrite normal changes in keys + // this applies to previously empty arrays/documents too + $changes['$set']["{$keyfix}{$k}"] = $v; + } + } + } + + foreach ($oldData as $k => $v) { // data that used to exist, but now doesn't + if (!isset($newData[$k])) { // removed field + $changes['$unset']["{$keyfix}{$k}"] = ''; + continue; + } + } + } + + /** + * Merge all options. + * + * @param array $defaultOptions default options array + * @param array $toMergeOptions to merge options array + * + * @return array + */ + private function mergeOptions(array $defaultOptions = [], array $toMergeOptions = []): array + { + return array_merge($defaultOptions, $toMergeOptions); + } + + /** + * Perform actions on object before firing the after event. + */ + private function afterSuccess(ModelInterface $model): void + { + $model->syncOriginalDocumentAttributes(); + } + + private function getUpdateData($model, array $data): array + { + $changes = []; + $this->calculateChanges($changes, $data, $model->getOriginalDocumentAttributes()); + + return $changes; + } +} diff --git a/src/Query/BulkWrite.php b/src/Query/BulkWrite.php new file mode 100644 index 00000000..359b9d44 --- /dev/null +++ b/src/Query/BulkWrite.php @@ -0,0 +1,82 @@ +model = $model; + } + + public function isEmpty(): bool + { + return !$this->operations; + } + + /** + * Add an `update` operation to the Bulk, where only one record is updated, by `_id` or `query`. + * Be aware that working with multiple levels of nesting on `$dataToSet` may have + * an undesired behavior that could lead to data loss on a specific key. + * + * @see https://docs.mongodb.com/manual/reference/operator/update/set/#set-top-level-fields + * + * @param ObjectId|string|array $filter + * @param array $dataToSet + * @param array $options + */ + public function updateOne( + $filter, + array $dataToSet, + array $options = ['upsert' => true], + string $operator = '$set' + ): void { + $filter = is_array($filter) ? $filter : ['_id' => $filter]; + + $update = [$operator => $dataToSet]; + + $this->operations[] = ['updateOne' => [$filter, $update, $options]]; + } + + /** + * Execute the BulkWrite, using connection. + * The collection is inferred from model's collection name. + * + * @throws \Mongolid\Model\Exception\NoCollectionNameException + */ + public function execute(int $writeConcern = 1): BulkWriteResult + { + $collection = $this->model->getCollection(); + + $result = $collection->bulkWrite( + $this->operations, + ['writeConcern' => new WriteConcern($writeConcern)] + ); + + $this->operations = []; + + return $result; + } +} diff --git a/src/Query/ModelMapper.php b/src/Query/ModelMapper.php new file mode 100644 index 00000000..80b843da --- /dev/null +++ b/src/Query/ModelMapper.php @@ -0,0 +1,89 @@ +clearNullFields($model); + $this->clearDynamicFields($model, $allowedFields, $dynamic, $timestamps); + $this->manageTimestamps($model, $timestamps); + $this->manageId($model); + + return $model->getDocumentAttributes(); + } + + /** + * If the model is not dynamic, remove all non specified fields. + */ + protected function clearDynamicFields( + ModelInterface $model, + array $allowedFields, + bool $dynamic, + bool $timestamps + ): void { + if ($dynamic) { + return; + } + + $merge = ['_id']; + + if ($timestamps) { + $merge[] = 'created_at'; + $merge[] = 'updated_at'; + } + + $allowedFields = array_unique(array_merge($allowedFields, $merge)); + + foreach ($model->getDocumentAttributes() as $field => $value) { + if (!in_array($field, $allowedFields)) { + unset($model->{$field}); + } + } + } + + private function clearNullFields(ModelInterface $model): void + { + foreach ($model->getDocumentAttributes() as $field => $value) { + if (null === $value) { + unset($model->{$field}); + } + } + } + + private function manageTimestamps(ModelInterface $model, bool $timestamps): void + { + if (!$timestamps) { + return; + } + $model->updated_at = Container::make(UTCDateTime::class, ['milliseconds' => null]); + + if (!$model->created_at instanceof UTCDateTime) { + $model->created_at = $model->updated_at; + } + } + + private function manageId(ModelInterface $model) + { + $value = $model->_id; + + if (is_null($value) || (is_string($value) && ObjectIdUtils::isObjectId($value))) { + $value = Container::make(ObjectId::class, ['id' => $value]); + } + + $model->_id = $value; + } +} diff --git a/src/Mongolid/Util/LocalDateTime.php b/src/Util/LocalDateTime.php similarity index 78% rename from src/Mongolid/Util/LocalDateTime.php rename to src/Util/LocalDateTime.php index aa8b061f..a525ec8f 100644 --- a/src/Mongolid/Util/LocalDateTime.php +++ b/src/Util/LocalDateTime.php @@ -1,5 +1,4 @@ connPool = $connPool; + $this->connection = $connection; $this->collection = $collection; } @@ -40,8 +35,6 @@ public function __construct(Pool $connPool, string $collection = 'mongolid_seque * Get next value for the sequence. * * @param string $sequenceName sequence identifier string - * - * @return int */ public function getNextValue(string $sequenceName): int { @@ -60,14 +53,13 @@ public function getNextValue(string $sequenceName): int /** * Get the actual MongoDB Collection object. - * - * @return Collection */ protected function rawCollection(): Collection { - $conn = $this->connPool->getConnection(); - $database = $conn->defaultDatabase; + $database = $this->connection->defaultDatabase; - return $conn->getRawConnection()->$database->{$this->collection}; + return $this->connection->getClient() + ->$database + ->{$this->collection}; } } diff --git a/tests/Integration/AttributesKeyTest.php b/tests/Integration/AttributesKeyTest.php new file mode 100644 index 00000000..720b1000 --- /dev/null +++ b/tests/Integration/AttributesKeyTest.php @@ -0,0 +1,62 @@ +name = 'John'; + $user->email = 'john@doe.com'; + + // attributes that used to be "reserved" + $user->attributes = ['my', 'attributes']; + $user->originalAttributes = ['my', 'original', 'attributes']; + + $this->assertSame('John', $user->name); + $this->assertSame('john@doe.com', $user->email); + $this->assertSame(['my', 'attributes'], $user->attributes); + $this->assertSame(['my', 'original', 'attributes'], $user->originalAttributes); + $this->assertSame( + [ + 'name' => 'John', + 'email' => 'john@doe.com', + 'attributes' => ['my', 'attributes'], + 'originalAttributes' => ['my', 'original', 'attributes'], + ], + $user->getDocumentAttributes() + ); + $this->assertSame([], $user->getOriginalDocumentAttributes()); + + // Save and refetch from database + $this->assertTrue($user->save()); + $user = $user->first(); + + $this->assertSame('John', $user->name); + $this->assertSame('john@doe.com', $user->email); + $this->assertSame(['my', 'attributes'], $user->attributes); + $this->assertSame(['my', 'original', 'attributes'], $user->originalAttributes); + $this->assertSame( + [ + '_id' => $user->_id, + 'name' => 'John', + 'email' => 'john@doe.com', + 'attributes' => ['my', 'attributes'], + 'originalAttributes' => ['my', 'original', 'attributes'], + ], + $user->getDocumentAttributes() + ); + $this->assertEquals( + [ + '_id' => $user->_id, + 'name' => 'John', + 'email' => 'john@doe.com', + 'attributes' => ['my', 'attributes'], + 'originalAttributes' => ['my', 'original', 'attributes'], + ], + $user->getOriginalDocumentAttributes() + ); + } +} diff --git a/tests/Integration/BulkWriteTest.php b/tests/Integration/BulkWriteTest.php new file mode 100644 index 00000000..86e24834 --- /dev/null +++ b/tests/Integration/BulkWriteTest.php @@ -0,0 +1,97 @@ +createUser('Bob'); + $john = $this->createUser('John'); + $mary = $this->createUser('Mary'); + + $bulkWrite = new BulkWrite(new ReferencedUser()); + + $this->assertTrue($bulkWrite->isEmpty()); + + $bulkWrite->updateOne( + ['_id' => $bob->_id], + ['name' => 'Bulk Updated Bob!'] + ); + $bulkWrite->updateOne( + ['_id' => $john->_id], + ['name' => 'Bulk Updated John!'] + ); + $bulkWrite->updateOne( + ['_id' => $mary->_id], + ['name' => 'Bulk Updated Mary!'] + ); + $bulkWrite->updateOne( + ['_id' => $bob->_id], + ['delete_this' => ''], + [], + '$unset' + ); + $bulkWrite->updateOne( + ['_id' => $john->_id], + ['delete_this' => ''], + [], + '$unset' + ); + $bulkWrite->updateOne( + ['_id' => $mary->_id], + ['delete_this' => ''], + [], + '$unset' + ); + + $this->assertFalse($bulkWrite->isEmpty()); + + // Before running + $this->assertSame('Bob', $bob->name); + $this->assertSame('John', $john->name); + $this->assertSame('Mary', $mary->name); + + $this->assertSame('xxxxx', $bob->delete_this); + $this->assertSame('xxxxx', $john->delete_this); + $this->assertSame('xxxxx', $mary->delete_this); + + // Runs it + $result = $bulkWrite->execute(); + + $this->assertTrue($bulkWrite->isEmpty()); + + $this->assertInstanceOf(BulkWriteResult::class, $result); + $this->assertTrue($result->isAcknowledged()); + $this->assertSame(6, $result->getModifiedCount()); + + // Refresh models + $bob = $bob->first($bob->_id); + $john = $john->first($john->_id); + $mary = $mary->first($mary->_id); + + // After running + $this->assertSame('Bulk Updated Bob!', $bob->name); + $this->assertSame('Bulk Updated John!', $john->name); + $this->assertSame('Bulk Updated Mary!', $mary->name); + + $this->assertNull($bob->delete_this); + $this->assertNull($john->delete_this); + $this->assertNull($mary->delete_this); + } + + private function createUser(string $name): ReferencedUser + { + $user = new ReferencedUser(); + $user->_id = new ObjectId(); + $user->name = $name; + $user->delete_this = 'xxxxx'; + $this->assertTrue($user->save()); + + return $user; + } +} diff --git a/tests/Integration/DateQueriesTest.php b/tests/Integration/DateQueriesTest.php new file mode 100644 index 00000000..4fbd5c00 --- /dev/null +++ b/tests/Integration/DateQueriesTest.php @@ -0,0 +1,90 @@ +some_date = new UTCDateTime(new DateTime('2018-10-10 00:00:00')); + + $this->assertTrue($user->save()); + + $greaterEqualResult = ReferencedUser::where( + [ + 'some_date' => [ + '$gte' => new UTCDateTime(new DateTime('2018-10-10 00:00:00')), + ], + ] + ); + + $this->assertCount(1, $greaterEqualResult); + $this->assertEquals($user, $greaterEqualResult->first()); + + $greaterResult = ReferencedUser::where( + [ + 'some_date' => [ + '$gt' => new UTCDateTime(new DateTime('2018-10-10 00:00:00')), + ], + ] + ); + + $this->assertCount(0, $greaterResult); + + $emptyResult = ReferencedUser::where( + [ + 'some_date' => [ + '$gte' => new UTCDateTime(new DateTime('2018-10-10 00:00:01')), + ], + ] + ); + + $this->assertCount(0, $emptyResult); + } + + public function testShouldRetrieveDocumentsUsingDateFiltersWithRelativeDates() + { + // Set + $user = new ReferencedUser(); + $user->some_date = new UTCDateTime(new DateTime('+10 days')); + + $this->assertTrue($user->save()); + + $greaterEqualResult = ReferencedUser::where( + [ + 'some_date' => [ + '$gte' => new UTCDateTime(), + ], + ] + ); + + $this->assertCount(1, $greaterEqualResult); + $this->assertEquals($user, $greaterEqualResult->first()); + + $greaterResult = ReferencedUser::where( + [ + 'some_date' => [ + '$gt' => new UTCDateTime(), + ], + ] + ); + + $this->assertCount(1, $greaterResult); + $this->assertEquals($user, $greaterResult->first()); + + $emptyResult = ReferencedUser::where( + [ + 'some_date' => [ + '$gte' => new UTCDateTime(new DateTime('+10 days +1 second')), + ], + ] + ); + + $this->assertCount(0, $emptyResult); + } +} diff --git a/tests/Integration/EmbedsManyRelationTest.php b/tests/Integration/EmbedsManyRelationTest.php new file mode 100644 index 00000000..dfd5ee72 --- /dev/null +++ b/tests/Integration/EmbedsManyRelationTest.php @@ -0,0 +1,185 @@ +createUser('Chuck'); + $john = $this->createUser('John'); + $john->siblings()->add($chuck); + + $this->assertSiblings([$chuck], $john); + + $mary = $this->createUser('Mary'); + $john->siblings()->addMany([$mary]); + + $this->assertSiblings([$chuck, $mary], $john); + + // remove one sibling + $john->siblings()->remove($chuck); + $this->assertSiblings([$mary], $john); + + // replace siblings + $bob = $this->createUser('Bob'); + + // unset + $john->siblings()->replace([$bob]); + $this->assertSiblings([$bob], $john); + unset($john->embedded_siblings); + $this->assertEmpty($john->siblings->all()); + $this->assertEmpty($john->embedded_siblings); + + // remove all + $john->siblings()->add($bob); + $this->assertSiblings([$bob], $john); + $john->siblings()->removeAll(); + $this->assertEmpty($john->siblings->all()); + $this->assertEmpty($john->embedded_siblings); + + // remove + $john->siblings()->add($bob); + $this->assertSiblings([$bob], $john); + $john->siblings()->remove($bob); + $this->assertEmpty($john->embedded_siblings); + $this->assertEmpty($john->siblings->all()); + + // changing the field directly + $john->siblings()->add($bob); + $this->assertSiblings([$bob], $john); + $john->embedded_siblings = [$chuck]; + $this->assertSiblings([$chuck], $john); + + $john->siblings()->removeAll(); + + // changing the field with fillable + $john->siblings()->add($bob); + $this->assertSiblings([$bob], $john); + $john = EmbeddedUser::fill(['embedded_siblings' => [$chuck]], $john, true); + $this->assertSiblings([$chuck], $john); + } + + public function testShouldRetrieveGrandsonsOfUserUsingCustomKey() + { + // create grandson + $chuck = $this->createUser('Chuck'); + $john = $this->createUser('John'); + $john->grandsons()->add($chuck); + + $this->assertGrandsons([$chuck], $john); + + $mary = $this->createUser('Mary'); + $john->grandsons()->add($mary); + + $this->assertGrandsons([$chuck, $mary], $john); + + // remove one grandson + $john->grandsons()->remove($chuck); + $this->assertGrandsons([$mary], $john); + + // replace grandsons + $john->grandsons()->remove($mary); + $bob = $this->createUser('Bob'); + + // unset + $john->grandsons()->add($bob); + $this->assertGrandsons([$bob], $john); + unset($john->other_arbitrary_field); + $this->assertEmpty($john->other_arbitrary_field); + $this->assertEmpty($john->grandsons->all()); + + // removeAll + $john->grandsons()->add($bob); + $this->assertGrandsons([$bob], $john); + $john->grandsons()->removeAll(); + $this->assertEmpty($john->other_arbitrary_field); + $this->assertEmpty($john->grandsons->all()); + + // remove + $john->grandsons()->add($bob); + $this->assertGrandsons([$bob], $john); + $john->grandsons()->remove($bob); + $this->assertEmpty($john->other_arbitrary_field); + $this->assertEmpty($john->grandsons->all()); + + // changing the field directly + $john->grandsons()->add($bob); + $this->assertGrandsons([$bob], $john); + $john->other_arbitrary_field = [$chuck]; + $this->assertGrandsons([$chuck], $john); + + $john->grandsons()->removeAll(); + + // changing the field with fillable + $john->grandsons()->add($bob); + $this->assertGrandsons([$bob], $john); + $john = EmbeddedUser::fill(['other_arbitrary_field' => [$chuck]], $john, true); + $this->assertGrandsons([$chuck], $john); + + // save and retrieve object + $this->assertTrue($john->save()); + $john = $john->first($john->_id); + + $this->assertInstanceOf(EmbeddedUser::class, $john->grandsons->first()); + $this->assertEquals( + array_except($chuck->toArray(), 'updated_at'), + array_except($john->grandsons->first()->toArray(), 'updated_at') + ); + } + + private function createUser(string $name): EmbeddedUser + { + $user = new EmbeddedUser(); + $user->_id = new ObjectId(); + $user->name = $name; + $this->assertTrue($user->save()); + + return $user; + } + + private function assertSiblings($expectedSiblings, EmbeddedUser $model) + { + $expected = []; + foreach ($expectedSiblings as $sibling) { + $expected[] = $sibling; + $this->assertInstanceOf(UTCDateTime::class, $sibling->created_at); + } + + $siblings = $model->siblings; + $this->assertInstanceOf(CursorInterface::class, $siblings); + $this->assertEquals($expectedSiblings, $siblings->all()); + $this->assertSame($expected, $model->embedded_siblings); + + // hit cache + $siblings = $model->siblings; + $this->assertInstanceOf(CursorInterface::class, $siblings); + $this->assertEquals($expectedSiblings, $siblings->all()); + $this->assertSame($expected, $model->embedded_siblings); + } + + private function assertGrandsons($expectedGrandsons, EmbeddedUser $model) + { + $expected = []; + foreach ($expectedGrandsons as $grandson) { + $expected[] = $grandson; + $this->assertInstanceOf(UTCDateTime::class, $grandson->created_at); + } + + $grandsons = $model->grandsons; + $this->assertInstanceOf(CursorInterface::class, $grandsons); + $this->assertEquals($expectedGrandsons, $grandsons->all()); + $this->assertSame($expected, $model->other_arbitrary_field); + + // hit cache + $grandsons = $model->grandsons; + $this->assertInstanceOf(CursorInterface::class, $grandsons); + $this->assertEquals($expectedGrandsons, $grandsons->all()); + $this->assertSame($expected, $model->other_arbitrary_field); + } +} diff --git a/tests/Integration/EmbedsOneRelationTest.php b/tests/Integration/EmbedsOneRelationTest.php new file mode 100644 index 00000000..fc2b150c --- /dev/null +++ b/tests/Integration/EmbedsOneRelationTest.php @@ -0,0 +1,162 @@ +createUser('Chuck'); + $john = $this->createUser('John'); + $john->parent()->add($chuck); + + $this->assertParent($chuck, $john); + + // replace parent + $bob = $this->createUser('Bob'); + + // unset + $john->parent()->add($bob); + $this->assertParent($bob, $john); + unset($john->embedded_parent); + + $this->assertNull($john->embedded_parent); + $this->assertNull($john->parent); + + // remove all + $john->parent()->add($bob); + $this->assertParent($bob, $john); + $john->parent()->remove(); + $this->assertNull($john->embedded_parent); + $this->assertNull($john->parent); + + // remove + $john->parent()->add($bob); + $this->assertParent($bob, $john); + $john->parent()->remove(); + $this->assertNull($john->embedded_parent); + $this->assertNull($john->parent); + + // changing the field directly + $john->parent()->add($bob); + $this->assertParent($bob, $john); + $john->embedded_parent = $chuck; + $this->assertParent($chuck, $john); + + $john->parent()->remove(); + + // changing the field with fillable + $john->parent()->add($bob); + $this->assertParent($bob, $john); + $john = EmbeddedUser::fill(['embedded_parent' => $chuck], $john, true); + $this->assertParent($chuck, $john); + } + + public function testShouldRetrieveSonOfUserUsingCustomKey() + { + // create parent + $chuck = $this->createUser('Chuck'); + $john = $this->createUser('John'); + $john->son()->add($chuck); + + $this->assertSon($chuck, $john); + + // replace son + $bob = $this->createUser('Bob'); + + // unset + $john->son()->add($bob); + $this->assertSon($bob, $john); + unset($john->arbitrary_field); + $this->assertNull($john->arbitrary_field); + $this->assertNull($john->son); + + // remove all + $john->son()->add($bob); + $this->assertSon($bob, $john); + $john->son()->remove(); + $this->assertNull($john->arbitrary_field); + $this->assertNull($john->son); + + // remove + $john->son()->add($bob); + $this->assertSon($bob, $john); + $john->son()->remove(); + $this->assertNull($john->arbitrary_field); + $this->assertNull($john->son); + + // changing the field directly + $john->son()->add($bob); + $this->assertSon($bob, $john); + $john->arbitrary_field = $chuck; + $this->assertSon($chuck, $john); + + $john->son()->remove(); + + // changing the field with fillable + $john->son()->add($bob); + $this->assertSon($bob, $john); + $john = EmbeddedUser::fill(['arbitrary_field' => $chuck], $john, true); + $this->assertSon($chuck, $john); + } + + public function testShouldCatchInvalidFieldNameOnRelations() + { + // Set + $user = new EmbeddedUser(); + + // Expectations + $this->expectException(InvalidFieldNameException::class); + $this->expectExceptionMessage('The field for relation "sameName" cannot have the same name as the relation'); + + // Actions + $user->sameName; + } + + private function createUser(string $name): EmbeddedUser + { + $user = new EmbeddedUser(); + $user->_id = new ObjectId(); + $user->name = $name; + $this->assertTrue($user->save()); + + return $user; + } + + private function assertParent($expected, EmbeddedUser $model) + { + $parent = $model->parent; + $this->assertInstanceOf(EmbeddedUser::class, $parent); + $this->assertInstanceOf(UTCDateTime::class, $parent->created_at); + $this->assertEquals($expected, $parent); + $this->assertSame($expected, $model->embedded_parent); + + // hit cache + $parent = $model->parent; + $this->assertInstanceOf(EmbeddedUser::class, $parent); + $this->assertInstanceOf(UTCDateTime::class, $parent->created_at); + $this->assertEquals($expected, $parent); + $this->assertSame($expected, $model->embedded_parent); + } + + private function assertSon($expected, EmbeddedUser $model) + { + $son = $model->son; + $this->assertInstanceOf(EmbeddedUser::class, $son); + $this->assertInstanceOf(UTCDateTime::class, $son->created_at); + $this->assertEquals($expected, $son); + $this->assertSame($expected, $model->arbitrary_field); + + // hit cache + $son = $model->son; + $this->assertInstanceOf(EmbeddedUser::class, $son); + $this->assertInstanceOf(UTCDateTime::class, $son->created_at); + $this->assertEquals($expected, $son); + $this->assertSame($expected, $model->arbitrary_field); + } +} diff --git a/tests/Integration/IntegrationTestCase.php b/tests/Integration/IntegrationTestCase.php new file mode 100644 index 00000000..15227958 --- /dev/null +++ b/tests/Integration/IntegrationTestCase.php @@ -0,0 +1,28 @@ +setupConnection($host, $database); + $this->dropDatabase(); + } + + protected function tearDown() + { + $this->dropDatabase(); + parent::tearDown(); + } +} diff --git a/tests/Integration/PersistedDataTest.php b/tests/Integration/PersistedDataTest.php new file mode 100644 index 00000000..ef10218d --- /dev/null +++ b/tests/Integration/PersistedDataTest.php @@ -0,0 +1,168 @@ +_id = new ObjectId('5bcb310783a7fcdf1bf1a672'); + } + + public function testSaveInsertingData() + { + // Set + $user = $this->getUser(); + + $expected = [ + '_id' => (string) $this->_id, + 'name' => 'John Doe', + 'age' => 25, + 'height' => 1.80, + 'preferences' => [ + 'email' => 'never', + ], + 'friends' => [], + 'skills' => [ + 'PHP' => ['percentage' => '100%', 'version' => '7.0'], + 'JavaScript' => ['percentage' => '80%', 'version' => 'ES6'], + 'CSS' => ['percentage' => '45%', 'version' => 'CSS3'], + ], + 'photos' => ['profile' => '/user-photo', 'icon' => '/user-icon'], + ]; + + // Actions + $saveResult = $user->save(); + $result = $user->getCollection()->findOne(['_id' => $this->_id]); + $result->_id = (string) $result->_id; + + // Assertions + $this->assertTrue($saveResult); + $this->assertInstanceOf(ReferencedUser::class, $result); + $this->assertSame($expected, $result->toArray()); + } + + public function testSaveUpdatingData() + { + // Set + $user = $this->getUser(true); + + $user->name = 'Jane Doe'; + unset($user->age); + $user->height = null; + $user->email = 'jane@doe.com'; + $user->preferences = []; + $user->friends = ['Mary']; + $user->address = '123 Blue Street'; + $user->skills->HTML = ['percentage' => '89%', 'version' => 'HTML5']; + $user->skills->PHP['version'] = '7.1'; + + $expected = [ + '_id' => (string) $user->_id, + 'name' => 'Jane Doe', + 'preferences' => [], + 'friends' => ['Mary'], + 'skills' => [ + 'PHP' => ['percentage' => '100%', 'version' => '7.1'], + 'JavaScript' => ['percentage' => '80%', 'version' => 'ES6'], + 'CSS' => ['percentage' => '45%', 'version' => 'CSS3'], + 'HTML' => ['percentage' => '89%', 'version' => 'HTML5'], + ], + 'photos' => ['profile' => '/user-photo', 'icon' => '/user-icon'], + 'email' => 'jane@doe.com', + 'address' => '123 Blue Street', + ]; + + // Actions + $updateResult = $user->save(); + $result = $user->getCollection()->findOne(['_id' => $user->_id]); + $result->_id = (string) $result->_id; + + // Assertions + $this->assertTrue($updateResult); + $this->assertInstanceOf(ReferencedUser::class, $result); + $this->assertEquals($expected, $result->toArray()); + } + + public function testUpdateData() + { + // Set + $user = $this->getUser(true); + + $user->name = 'Jane Doe'; + unset($user->age); + $user->height = null; + $user->email = 'jane@doe.com'; + $user->preferences = []; + $user->friends = ['Mary']; + $user->address = '123 Blue Street'; + $user->skills->HTML = ['percentage' => '89%', 'version' => 'HTML5']; + $user->skills->PHP['version'] = '7.1'; + + $expected = [ + '_id' => (string) $user->_id, + 'name' => 'Jane Doe', + 'preferences' => [], + 'friends' => ['Mary'], + 'skills' => [ + 'PHP' => ['percentage' => '100%', 'version' => '7.1'], + 'JavaScript' => ['percentage' => '80%', 'version' => 'ES6'], + 'CSS' => ['percentage' => '45%', 'version' => 'CSS3'], + 'HTML' => ['percentage' => '89%', 'version' => 'HTML5'], + ], + 'photos' => ['profile' => '/user-photo', 'icon' => '/user-icon'], + 'address' => '123 Blue Street', + 'email' => 'jane@doe.com', + ]; + + // Actions + $updateResult = $user->update(); + $result = $user->getCollection()->findOne(['_id' => $user->_id]); + $result->_id = (string) $result->_id; + + // Assertions + $this->assertTrue($updateResult); + $this->assertInstanceOf(ReferencedUser::class, $result); + $this->assertEquals($expected, $result->toArray()); + } + + private function getUser(bool $save = false): ReferencedUser + { + $user = new ReferencedUser(); + $user->_id = $this->_id; + $user->name = 'John Doe'; + $user->age = 25; + $user->height = 1.80; + $user->preferences = [ + 'email' => 'never', + ]; + $user->friends = []; + $user->address = null; + $user->skills = (object) [ + 'PHP' => ['percentage' => '100%', 'version' => '7.0'], + 'JavaScript' => ['percentage' => '80%', 'version' => 'ES6'], + 'CSS' => ['percentage' => '45%', 'version' => 'CSS3'], + ]; + + // dynamically set array + $user->photos['profile'] = '/user-photo'; + $user->photos['icon'] = '/user-icon'; + + // access unknown field and don't find it saved later. + $user->unknown; + + if ($save) { + $this->assertTrue($user->save(), 'Failed to save user!'); + } + + return $user; + } +} diff --git a/tests/Integration/ReferencesManyRelationTest.php b/tests/Integration/ReferencesManyRelationTest.php new file mode 100644 index 00000000..28878b2d --- /dev/null +++ b/tests/Integration/ReferencesManyRelationTest.php @@ -0,0 +1,198 @@ +createUser('Chuck'); + $john = $this->createUser('John'); + $john->siblings()->attach($chuck); + + $this->assertSiblings([$chuck], $john); + + $mary = $this->createUser('Mary'); + $john->siblings()->attachMany([$mary]); + + $this->assertSiblings([$chuck, $mary], $john); + + // remove one sibling + $john->siblings()->detach($chuck); + $this->assertSiblings([$mary], $john); + + // replace siblings + $bob = $this->createUser('Bob'); + + // unset + $john->siblings()->replace([$bob]); + $this->assertSiblings([$bob], $john); + unset($john->siblings_ids); + $this->assertEmpty($john->siblings_ids); + $this->assertEmpty($john->siblings->all()); + + // detachAll + $john->siblings()->attach($bob); + $this->assertSiblings([$bob], $john); + $john->siblings()->detachAll(); + $this->assertEmpty($john->siblings_ids); + $this->assertEmpty($john->siblings->all()); + + // detach + $john->siblings()->attach($bob); + $this->assertSiblings([$bob], $john); + $john->siblings()->detach($bob); + $this->assertEmpty($john->siblings_ids); + $this->assertEmpty($john->siblings->all()); + + // changing the field directly + $john->siblings()->attach($bob); + $this->assertSiblings([$bob], $john); + $john->siblings_ids = [$chuck->_id]; + $this->assertSiblings([$chuck], $john); + + $john->siblings()->detachAll(); + + // changing the field with fillable + $john->siblings()->attach($bob); + $this->assertSiblings([$bob], $john); + $john = ReferencedUser::fill(['siblings_ids' => [$chuck->_id]], $john, true); + $this->assertSiblings([$chuck], $john); + + // detach not attached has no problems + $john->siblings()->detach(new ReferencedUser()); + $this->assertSiblings([$chuck], $john); + } + + public function testShouldRetrieveGrandsonsOfUserUsingCustomKey() + { + // create sibling + $chuck = $this->createUser('Chuck', '010'); + $john = $this->createUser('John', '369'); + $john->grandsons()->attach($chuck); + + $this->assertSame(['010'], $john->grandsons_codes); + $this->assertGrandsons([$chuck], $john); + + $mary = $this->createUser('Mary', '222'); + $john->grandsons()->attach($mary); + + $this->assertSame(['010', '222'], $john->grandsons_codes); + $this->assertGrandsons([$chuck, $mary], $john); + + // remove one sibling + $john->grandsons()->detach($chuck); + + $this->assertSame(['222'], $john->grandsons_codes); + $this->assertGrandsons([$mary], $john); + + // replace grandsons + $john->grandsons()->detach($mary); + $bob = $this->createUser('Bob', '987'); + + // unset + $john->grandsons()->attach($bob); + $this->assertGrandsons([$bob], $john); + unset($john->grandsons_codes); + $this->assertEmpty($john->grandsons_codes); + $this->assertEmpty($john->grandsons->all()); + + // detachAll + $john->grandsons()->attach($bob); + $this->assertGrandsons([$bob], $john); + $john->grandsons()->detachAll(); + $this->assertEmpty($john->grandsons_codes); + $this->assertEmpty($john->grandsons->all()); + + // detach + $john->grandsons()->attach($bob); + $this->assertGrandsons([$bob], $john); + $john->grandsons()->detach($bob); + $this->assertEmpty($john->grandsons_codes); + $this->assertEmpty($john->grandsons->all()); + + // changing the field directly + $john->grandsons()->attach($bob); + $this->assertGrandsons([$bob], $john); + $john->grandsons_codes = [$chuck->code]; + $this->assertGrandsons([$chuck], $john); + + $john->grandsons()->detachAll(); + + // changing the field with fillable + $john->grandsons()->attach($bob); + $this->assertGrandsons([$bob], $john); + $john = ReferencedUser::fill(['grandsons_codes' => [$chuck->code]], $john, true); + $this->assertGrandsons([$chuck], $john); + } + + private function createUser(string $name, string $code = null): ReferencedUser + { + $user = new ReferencedUser(); + $user->_id = new ObjectId(); + $user->name = $name; + if ($code) { + $user->code = $code; + } + $this->assertTrue($user->save()); + + return $user; + } + + private function assertSiblings($expected, ReferencedUser $model) + { + $siblings = $model->siblings; + $this->assertInstanceOf(CursorInterface::class, $siblings); + $this->assertEquals($expected, $siblings->all()); + + $ids = []; + + foreach ($expected as $expectedModel) { + $ids[] = $expectedModel->_id; + } + + $this->assertSame($ids, $model->siblings_ids); + + // hit cache + $siblings = $model->siblings; + $this->assertInstanceOf(CursorInterface::class, $siblings); + $this->assertEquals($expected, $siblings->all()); + + $ids = []; + foreach ($expected as $expectedModel) { + $ids[] = $expectedModel->_id; + } + + $this->assertSame($ids, $model->siblings_ids); + } + + private function assertGrandsons($expected, ReferencedUser $model) + { + $grandsons = $model->grandsons; + $this->assertInstanceOf(CursorInterface::class, $grandsons); + $this->assertEquals($expected, $grandsons->all()); + + $codes = []; + foreach ($expected as $expectedModel) { + $codes[] = $expectedModel->code; + } + + $this->assertSame($codes, $model->grandsons_codes); + + // hit cache + $grandsons = $model->grandsons; + $this->assertInstanceOf(CursorInterface::class, $grandsons); + $this->assertEquals($expected, $grandsons->all()); + + $codes = []; + foreach ($expected as $expectedModel) { + $codes[] = $expectedModel->code; + } + + $this->assertSame($codes, $model->grandsons_codes); + } +} diff --git a/tests/Integration/ReferencesOneRelationTest.php b/tests/Integration/ReferencesOneRelationTest.php new file mode 100644 index 00000000..977a0817 --- /dev/null +++ b/tests/Integration/ReferencesOneRelationTest.php @@ -0,0 +1,159 @@ +createUser('Chuck'); + $john = $this->createUser('John'); + $john->parent()->attach($chuck); + + $this->assertParent($chuck, $john); + + // replace parent + $bob = $this->createUser('Bob'); + + // unset + $john->parent()->attach($bob); + $this->assertParent($bob, $john); + unset($john->parent_id); + $this->assertNull($john->parent_id); + $this->assertNull($john->parent); + + // detach all + $john->parent()->attach($bob); + $this->assertParent($bob, $john); + $john->parent()->detach(); + $this->assertNull($john->parent_id); + $this->assertNull($john->parent); + + // detach + $john->parent()->attach($bob); + $this->assertParent($bob, $john); + $john->parent()->detach(); + $this->assertNull($john->parent_id); + $this->assertNull($john->parent); + + // changing the field directly + $john->parent()->attach($bob); + $this->assertParent($bob, $john); + $john->parent_id = $chuck->_id; + $this->assertParent($chuck, $john); + + $john->parent()->detach(); + + // changing the field with fillable + $john->parent()->attach($bob); + $this->assertParent($bob, $john); + $john = ReferencedUser::fill(['parent_id' => $chuck->_id], $john, true); + $this->assertParent($chuck, $john); + } + + public function testShouldRetrieveSonOfUserUsingCustomKey() + { + // create parent + $chuck = $this->createUser('Chuck', '010'); + $john = $this->createUser('John', '369'); + $john->son()->attach($chuck); + + $this->assertSon($chuck, $john); + + // replace son + $bob = $this->createUser('Bob', '987'); + + // unset + $john->son()->attach($bob); + $this->assertSon($bob, $john); + unset($john->arbitrary_field); + $this->assertNull($john->arbitrary_field); + $this->assertNull($john->son); + + // detach + $john->son()->attach($bob); + $this->assertSon($bob, $john); + $john->son()->detach(); + $this->assertNull($john->arbitrary_field); + $this->assertNull($john->son); + + // detach + $john->son()->attach($bob); + $this->assertSon($bob, $john); + $john->son()->detach(); + $this->assertNull($john->arbitrary_field); + $this->assertNull($john->son); + + // changing the field directly + $john->son()->attach($bob); + $this->assertSon($bob, $john); + $john->arbitrary_field = $chuck->code; + $this->assertSon($chuck, $john); + + $john->son()->detach(); + + // changing the field with fillable + $john->son()->attach($bob); + $this->assertSon($bob, $john); + $john = ReferencedUser::fill(['arbitrary_field' => $chuck->code], $john, true); + $this->assertSon($chuck, $john); + } + + public function testShouldCatchInvalidRelations() + { + // Set + $user = new ReferencedUser(); + + // Expectations + $this->expectException(NotARelationException::class); + $this->expectExceptionMessage('Called method "invalid" is not a Relation!'); + + // Actions + $user->invalid; + } + + private function createUser(string $name, string $code = null): ReferencedUser + { + $user = new ReferencedUser(); + $user->_id = new ObjectId(); + $user->name = $name; + if ($code) { + $user->code = $code; + } + $this->assertTrue($user->save()); + + return $user; + } + + private function assertParent($expected, ReferencedUser $model) + { + $parent = $model->parent; + $this->assertInstanceOf(ReferencedUser::class, $parent); + $this->assertEquals($expected, $parent); + $this->assertSame($expected->_id, $model->parent_id); + + // hit cache + $parent = $model->parent; + $this->assertInstanceOf(ReferencedUser::class, $parent); + $this->assertEquals($expected, $parent); + $this->assertSame($expected->_id, $model->parent_id); + } + + private function assertSon($expected, ReferencedUser $model) + { + $son = $model->son; + $this->assertInstanceOf(ReferencedUser::class, $son); + $this->assertEquals($expected, $son); + $this->assertSame($expected->code, $model->arbitrary_field); + + // hit cache + $son = $model->son; + $this->assertInstanceOf(ReferencedUser::class, $son); + $this->assertEquals($expected, $son); + $this->assertSame($expected->code, $model->arbitrary_field); + } +} diff --git a/tests/Integration/RewindableCursorTest.php b/tests/Integration/RewindableCursorTest.php new file mode 100644 index 00000000..2b02f0b4 --- /dev/null +++ b/tests/Integration/RewindableCursorTest.php @@ -0,0 +1,50 @@ +createUser('Bob'); + $this->createUser('Mary'); + $this->createUser('John'); + $this->createUser('Jane'); + + $cursor = ReferencedUser::all(); + + // exhaust cursor + foreach ($cursor as $user) { + $this->assertInstanceOf(ReferencedUser::class, $user); + } + + // try again + foreach ($cursor as $user) { + $this->assertInstanceOf(ReferencedUser::class, $user); + } + + // rewind and try again + $cursor->rewind(); + foreach ($cursor as $user) { + $this->assertInstanceOf(ReferencedUser::class, $user); + } + + // serializing + $newCursor = unserialize(serialize($cursor)); + foreach ($newCursor as $user) { + $this->assertInstanceOf(ReferencedUser::class, $user); + } + } + + private function createUser(string $name): ReferencedUser + { + $user = new ReferencedUser(); + $user->_id = new ObjectId(); + $user->name = $name; + $this->assertTrue($user->save()); + + return $user; + } +} diff --git a/tests/Mongolid/ActiveRecordTest.php b/tests/Mongolid/ActiveRecordTest.php deleted file mode 100644 index 02a40ee4..00000000 --- a/tests/Mongolid/ActiveRecordTest.php +++ /dev/null @@ -1,476 +0,0 @@ -entity = new class() extends ActiveRecord { - }; - } - - /** - * {@inheritdoc} - */ - public function tearDown() - { - parent::tearDown(); - m::close(); - unset($this->entity); - } - - public function testShouldHaveCorrectPropertiesByDefault() - { - // Assert - $this->assertAttributeEquals( - [ - '_id' => 'objectId', - 'created_at' => 'createdAtTimestamp', - 'updated_at' => 'updatedAtTimestamp', - ], - 'fields', - $this->entity - ); - $this->assertTrue($this->entity->dynamic); - } - - public function testShouldImplementModelTraits() - { - // Assert - $this->assertEquals( - [Attributes::class, Relations::class], - array_keys(class_uses(ActiveRecord::class)) - ); - } - - public function testShouldSave() - { - // Arrage - $entity = m::mock(ActiveRecord::class.'[getDataMapper,getCollectionName,syncOriginalAttributes]'); - $dataMapper = m::mock(); - - // Act - $entity->shouldReceive('getDataMapper') - ->andReturn($dataMapper); - - $entity->shouldReceive('getCollectionName') - ->andReturn('mongolid'); - - $entity->shouldReceive('syncOriginalAttributes') - ->once(); - - $dataMapper->shouldReceive('save') - ->once() - ->with($entity, ['writeConcern' => new WriteConcern(1)]) - ->andReturn(true); - - // Assert - $this->assertTrue($entity->save()); - } - - public function testShouldInsert() - { - // Arrage - $entity = m::mock(ActiveRecord::class.'[getDataMapper,getCollectionName,syncOriginalAttributes]'); - $dataMapper = m::mock(); - - // Act - $entity->shouldReceive('getDataMapper') - ->andReturn($dataMapper); - - $entity->shouldReceive('getCollectionName') - ->andReturn('mongolid'); - - $entity->shouldReceive('syncOriginalAttributes') - ->once(); - - $dataMapper->shouldReceive('insert') - ->once() - ->with($entity, ['writeConcern' => new WriteConcern(1)]) - ->andReturn(true); - - // Assert - $this->assertTrue($entity->insert()); - } - - public function testShouldUpdate() - { - // Arrage - $entity = m::mock(ActiveRecord::class.'[getDataMapper,getCollectionName,syncOriginalAttributes]'); - $dataMapper = m::mock(); - - // Act - $entity->shouldReceive('getDataMapper') - ->andReturn($dataMapper); - - $entity->shouldReceive('getCollectionName') - ->andReturn('mongolid'); - - $entity->shouldReceive('syncOriginalAttributes') - ->once(); - - $dataMapper->shouldReceive('update') - ->once() - ->with($entity, ['writeConcern' => new WriteConcern(1)]) - ->andReturn(true); - - // Assert - $this->assertTrue($entity->update()); - } - - public function testShouldDelete() - { - // Arrage - $entity = m::mock(ActiveRecord::class.'[getDataMapper,getCollectionName]'); - $dataMapper = m::mock(); - - // Act - $entity->shouldReceive('getDataMapper') - ->andReturn($dataMapper); - - $entity->shouldReceive('getCollectionName') - ->andReturn('mongolid'); - - $dataMapper->shouldReceive('delete') - ->once() - ->with($entity, ['writeConcern' => new WriteConcern(1)]) - ->andReturn(true); - - // Assert - $this->assertTrue($entity->delete()); - } - - public function testSaveShouldReturnFalseIfCollectionIsNull() - { - $this->assertFalse($this->entity->save()); - } - - public function testUpdateShouldReturnFalseIfCollectionIsNull() - { - $this->assertFalse($this->entity->update()); - } - - public function testInsertShouldReturnFalseIfCollectionIsNull() - { - $this->assertFalse($this->entity->insert()); - } - - public function testDeleteShouldReturnFalseIfCollectionIsNull() - { - $this->assertFalse($this->entity->delete()); - } - - public function testShouldGetWithWhereQuery() - { - // Arrage - $entity = m::mock(ActiveRecord::class.'[getDataMapper]'); - $this->setProtected($entity, 'collection', 'mongolid'); - $query = ['foo' => 'bar']; - $projection = ['some', 'fields']; - $dataMapper = m::mock(); - $cursor = m::mock(); - - // Act - Ioc::instance(get_class($entity), $entity); - - $entity->shouldReceive('getDataMapper') - ->andReturn($dataMapper); - - $dataMapper->shouldReceive('where') - ->once() - ->with($query, $projection, true) - ->andReturn($cursor); - - // Assert - $this->assertEquals($cursor, $entity->where($query, $projection, true)); - } - - public function testShouldGetAll() - { - // Arrage - $entity = m::mock(ActiveRecord::class.'[getDataMapper]'); - $this->setProtected($entity, 'collection', 'mongolid'); - $dataMapper = m::mock(); - $cursor = m::mock(); - - // Act - Ioc::instance(get_class($entity), $entity); - - $entity->shouldReceive('getDataMapper') - ->andReturn($dataMapper); - - $dataMapper->shouldReceive('all') - ->once() - ->andReturn($cursor); - - // Assert - $this->assertEquals($cursor, $entity->all()); - } - - public function testShouldGetFirstWithQuery() - { - // Arrage - $entity = m::mock(ActiveRecord::class.'[getDataMapper]'); - $this->setProtected($entity, 'collection', 'mongolid'); - $query = ['foo' => 'bar']; - $projection = ['some', 'fields']; - $dataMapper = m::mock(); - - // Act - Ioc::instance(get_class($entity), $entity); - - $entity->shouldReceive('getDataMapper') - ->andReturn($dataMapper); - - $dataMapper->shouldReceive('first') - ->once() - ->with($query, $projection, true) - ->andReturn($entity); - - // Assert - $this->assertEquals($entity, $entity->first($query, $projection, true)); - } - - public function testShouldGetFirstOrFail() - { - // Arrage - $entity = m::mock(ActiveRecord::class.'[getDataMapper]'); - $this->setProtected($entity, 'collection', 'mongolid'); - $query = ['foo' => 'bar']; - $projection = ['some', 'fields']; - $dataMapper = m::mock(); - - // Act - Ioc::instance(get_class($entity), $entity); - - $entity->shouldReceive('getDataMapper') - ->andReturn($dataMapper); - - $dataMapper->shouldReceive('firstOrFail') - ->once() - ->with($query, $projection, true) - ->andReturn($entity); - - // Assert - $this->assertEquals($entity, $entity->firstOrFail($query, $projection, true)); - } - - public function testShouldGetFirstOrNewAndReturnExistingModel() - { - // Arrage - $entity = m::mock(ActiveRecord::class.'[getDataMapper]'); - $this->setProtected($entity, 'collection', 'mongolid'); - $id = 123; - $dataMapper = m::mock(); - - // Act - Ioc::instance(get_class($entity), $entity); - - $entity->shouldReceive('getDataMapper') - ->andReturn($dataMapper); - - $dataMapper->shouldReceive('first') - ->once() - ->with($id) - ->andReturn($entity); - - // Assert - $this->assertEquals($entity, $entity->firstOrNew($id)); - } - - public function testShouldGetFirstOrNewAndReturnNewModel() - { - // Arrage - $entity = m::mock(ActiveRecord::class.'[getDataMapper]'); - $this->setProtected($entity, 'collection', 'mongolid'); - $id = 123; - $dataMapper = m::mock(); - - // Act - Ioc::instance(get_class($entity), $entity); - - $entity->shouldReceive('getDataMapper') - ->andReturn($dataMapper); - - $dataMapper->shouldReceive('first') - ->once() - ->with($id) - ->andReturn(null); - - // Assert - $this->assertNotEquals($entity, $entity->firstOrNew($id)); - } - - public function testShouldGetSchemaIfFieldsIsTheClassName() - { - // Arrage - $this->setProtected($this->entity, 'fields', 'MySchemaClass'); - $schema = m::mock(Schema::class); - - // Act - Ioc::instance('MySchemaClass', $schema); - - // Assert - $this->assertEquals( - $schema, - $this->entity->getSchema() - ); - } - - public function testShouldGetSchemaIfFieldsDescribesSchemaFields() - { - // Arrage - $fields = ['name' => 'string', 'age' => 'int']; - $this->setProtected($this->entity, 'fields', $fields); - - // Assert - $result = $this->entity->getSchema(); - $this->assertInstanceOf(Schema::class, $result); - $this->assertEquals($fields, $result->fields); - $this->assertEquals($this->entity->dynamic, $result->dynamic); - $this->assertEquals($this->entity->getCollectionName(), $result->collection); - $this->assertEquals(get_class($this->entity), $result->entityClass); - } - - public function testShouldGetDataMapper() - { - // Arrage - $entity = m::mock(ActiveRecord::class.'[getSchema]'); - $schema = m::mock(Schema::class.'[]'); - - // Act - $entity->shouldAllowMockingProtectedMethods(); - - $entity->shouldReceive('getSchema') - ->once() - ->andReturn($schema); - - // Assert - $result = $this->callProtected($entity, 'getDataMapper'); - $this->assertInstanceOf(DataMapper\DataMapper::class, $result); - $this->assertEquals($schema, $result->getSchema()); - } - - /** - * @expectedException \Mongolid\Exception\NoCollectionNameException - */ - public function testShouldRaiseExceptionWhenHasNoCollectionAndTryToCallAllFunction() - { - $entity = new class() extends ActiveRecord { - }; - - $this->assertNull($entity->getCollectionName()); - - $entity->all(); - } - - /** - * @expectedException \Mongolid\Exception\NoCollectionNameException - */ - public function testShouldRaiseExceptionWhenHasNoCollectionAndTryToCallFirstFunction() - { - $entity = new class() extends ActiveRecord { - }; - - $this->assertNull($entity->getCollectionName()); - - $entity->first(); - } - - /** - * @expectedException \Mongolid\Exception\NoCollectionNameException - */ - public function testShouldRaiseExceptionWhenHasNoCollectionAndTryToCallWhereFunction() - { - $entity = new class() extends ActiveRecord { - }; - - $this->assertNull($entity->getCollectionName()); - - $entity->where(); - } - - public function testShouldGetCollectionName() - { - $entity = new class() extends ActiveRecord { - protected $collection = 'collection_name'; - }; - - $this->assertEquals('collection_name', $entity->getCollectionName()); - } - - public function testShouldAttachToAttribute() - { - $entity = new class() extends ActiveRecord { - protected $collection = 'collection_name'; - - public function class() - { - return $this->referencesOne(stdClass::class, 'courseClass'); - } - }; - $embedded = new stdClass(); - $embedded->_id = new ObjectID(); - $embedded->name = 'Course Class #1'; - $entity->attachToCourseClass($embedded); - - $this->assertEquals([$embedded->_id], $entity->courseClass); - } - - public function testShouldEmbedToAttribute() - { - $entity = new class() extends ActiveRecord { - protected $collection = 'collection_name'; - - public function classes() - { - return $this->embedsMany(stdClass::class, 'courseClasses'); - } - }; - $embedded = new stdClass(); - $embedded->name = 'Course Class #1'; - $entity->embedToCourseClasses($embedded); - - $this->assertEquals('Course Class #1', $entity->classes()->first()->name); - } - - public function testShouldThrowBadMethodCallExceptionWhenCallingInvalidMethod() - { - $entity = new class() extends ActiveRecord { - protected $collection = 'collection_name'; - }; - - $this->expectException(BadMethodCallException::class); - - $entity->foobar(); - } - - public function testShouldGetSetWriteConcernInActiveRecordClass() - { - $this->assertEquals(1, $this->entity->getWriteConcern()); - $this->assertEquals(1, $this->entity->getWriteConcern()); - $this->entity->setWriteConcern(0); - $this->assertEquals(0, $this->entity->getWriteConcern()); - } -} diff --git a/tests/Mongolid/Connection/ConnectionTest.php b/tests/Mongolid/Connection/ConnectionTest.php deleted file mode 100644 index fb96d8fa..00000000 --- a/tests/Mongolid/Connection/ConnectionTest.php +++ /dev/null @@ -1,78 +0,0 @@ -assertAttributeInstanceOf(Client::class, 'rawConnection', $connection); - $this->assertAttributeEquals('my_db', 'defaultDatabase', $connection); - } - - public function testShouldDetermineDatabaseFromACluster() - { - // Arrange - $server = 'mongodb://my-server,other-server/my_db?replicaSet=someReplica'; - $options = ['some', 'uri', 'options']; - $driverOptions = ['some', 'driver', 'options']; - - // Act - $connection = new Connection($server, $options, $driverOptions); - - // Assert - $this->assertAttributeInstanceOf(Client::class, 'rawConnection', $connection); - $this->assertAttributeEquals('my_db', 'defaultDatabase', $connection); - } - - public function testShouldGetRawConnection() - { - // Arrange - $server = 'mongodb://my-server/my_db'; - $options = ['some', 'uri', 'options']; - $driverOptions = ['some', 'driver', 'options']; - $expectedParameters = [ - 'uri' => $server, - 'typeMap' => [ - 'array' => 'array', - 'document' => 'array', - ], - ]; - - // Act - $connection = new Connection($server, $options, $driverOptions); - $rawConnection = $connection->getRawConnection(); - - // Assert - $this->assertAttributeEquals($expectedParameters['uri'], 'uri', $rawConnection); - $this->assertAttributeEquals($expectedParameters['typeMap'], 'typeMap', $rawConnection); - } - - public function testShouldGetRawManager() - { - // Arrange - $server = 'mongodb://my-server/my_db'; - $options = ['some', 'uri', 'options']; - $driverOptions = ['some', 'driver', 'options']; - - // Act - $connection = new Connection($server, $options, $driverOptions); - $rawManager = $connection->getRawManager(); - - // Assert - $this->assertInstanceOf(Manager::class, $rawManager); - } -} diff --git a/tests/Mongolid/Connection/PoolTest.php b/tests/Mongolid/Connection/PoolTest.php deleted file mode 100644 index 2ffc83be..00000000 --- a/tests/Mongolid/Connection/PoolTest.php +++ /dev/null @@ -1,72 +0,0 @@ -setProtected($pool, 'connections', $connQueue); - - // Act - $connQueue->shouldReceive('pop') - ->once() - ->andReturn($connection); - - $connQueue->shouldReceive('push') - ->once() - ->with($connection); - - // Assert - $this->assertEquals($connection, $pool->getConnection()); - } - - public function testShouldGetNullConnectionFromPoolIfItsEmpty() - { - // Arrange - $pool = new Pool(); - $connQueue = m::mock(); - $this->setProtected($pool, 'connections', $connQueue); - - // Act - $connQueue->shouldReceive('pop') - ->once() - ->andReturn(null); - - $connQueue->shouldReceive('push') - ->never(); - - // Assert - $this->assertNull($pool->getConnection()); - } - - public function testShouldAddConnectionToPool() - { - // Arrange - $pool = new Pool(); - $connQueue = m::mock(); - $connection = m::mock(Connection::class); - $this->setProtected($pool, 'connections', $connQueue); - - // Act - $connQueue->shouldReceive('push') - ->once() - ->with($connection); - - // Assert - $this->assertTrue($pool->addConnection($connection)); - } -} diff --git a/tests/Mongolid/Container/IocTest.php b/tests/Mongolid/Container/IocTest.php deleted file mode 100644 index 1a5f8097..00000000 --- a/tests/Mongolid/Container/IocTest.php +++ /dev/null @@ -1,100 +0,0 @@ -shouldReceive('method') - ->once() - ->with() - ->andReturn(true); - - Ioc::setContainer($container); - - Ioc::method(); - } - - public function testShouldCallMethodsPropertlywithOneArgument() - { - $container = m::mock(Container::class); - - $container->shouldReceive('method') - ->once() - ->with(1) - ->andReturn(true); - - Ioc::setContainer($container); - - Ioc::method(1); - } - - public function testShouldCallMethodsPropertlywithTwoArgument() - { - $container = m::mock(Container::class); - - $container->shouldReceive('method') - ->once() - ->with(1, 2) - ->andReturn(true); - - Ioc::setContainer($container); - - Ioc::method(1, 2); - } - - public function testShouldCallMethodsPropertlywithThreeArgument() - { - $container = m::mock(Container::class); - - $container->shouldReceive('method') - ->once() - ->with(1, 2, 3) - ->andReturn(true); - - Ioc::setContainer($container); - - Ioc::method(1, 2, 3); - } - - public function testShouldCallMethodsPropertlywithFourArgument() - { - $container = m::mock(Container::class); - - $container->shouldReceive('method') - ->once() - ->with(1, 2, 3, 4) - ->andReturn(true); - - Ioc::setContainer($container); - - Ioc::method(1, 2, 3, 4); - } - - public function testShouldCallMethodsPropertlywithFiveOrMoreArgument() - { - $container = m::mock(Container::class); - - $container->shouldReceive('method') - ->once() - ->with(1, 2, 3, 4, 5) - ->andReturn(true); - - Ioc::setContainer($container); - - Ioc::method(1, 2, 3, 4, 5); - } -} diff --git a/tests/Mongolid/Cursor/CacheableCursorTest.php b/tests/Mongolid/Cursor/CacheableCursorTest.php deleted file mode 100644 index cca8eaa8..00000000 --- a/tests/Mongolid/Cursor/CacheableCursorTest.php +++ /dev/null @@ -1,249 +0,0 @@ - 'joe'], ['name' => 'doe']]); - $cursor = $this->getCachableCursor(); - $this->setProtected( - $cursor, - 'documents', - $documentsFromDb - ); - - // Assert - $this->assertEquals( - new ArrayIterator($documentsFromDb), - $this->callProtected($cursor, 'getCursor') - ); - } - - public function testShouldGetCursorFromCache() - { - // Arrange - $documentsFromCache = [['name' => 'joe'], ['name' => 'doe']]; - $cursor = $this->getCachableCursor(); - $cacheComponent = m::mock(CacheComponentInterface::class); - - // Act - $cursor->shouldReceive('generateCacheKey') - ->andReturn('find:collection:123'); - - Ioc::instance(CacheComponentInterface::class, $cacheComponent); - - $cacheComponent->shouldReceive('get') - ->once() - ->with('find:collection:123', null) - ->andReturn($documentsFromCache); - - // Assert - $this->assertEquals( - new ArrayIterator($documentsFromCache), - $this->callProtected($cursor, 'getCursor') - ); - } - - public function testShouldGetFromDatabaseWhenCacheFails() - { - // Arrange - $documentsFromDb = [['name' => 'joe'], ['name' => 'doe']]; - $cursor = $this->getCachableCursor()->limit(150); - $cacheComponent = m::mock(CacheComponentInterface::class); - $rawCollection = m::mock(); - $cacheKey = 'find:collection:123'; - - $this->setProtected( - $cursor, - 'collection', - $rawCollection - ); - - // Act - $cursor->shouldReceive('generateCacheKey') - ->andReturn($cacheKey); - - Ioc::instance(CacheComponentInterface::class, $cacheComponent); - - $cacheComponent->shouldReceive('get') - ->with($cacheKey, null) - ->andThrow( - new ErrorException( - sprintf('Unable to unserialize cache %s', $cacheKey) - ) - ); - - $rawCollection->shouldReceive('find') - ->with([], ['limit' => 100]) - ->andReturn(new ArrayIterator($documentsFromDb)); - - $cacheComponent->shouldReceive('put') - ->once() - ->with($cacheKey, $documentsFromDb, m::any()); - - // Assert - $this->assertEquals( - new ArrayIterator($documentsFromDb), - $this->callProtected($cursor, 'getCursor') - ); - } - - public function testShouldGetCursorFromDatabaseAndCacheForLater() - { - // Arrange - $documentsFromDb = [['name' => 'joe'], ['name' => 'doe']]; - $cursor = $this->getCachableCursor()->limit(150); - $cacheComponent = m::mock(CacheComponentInterface::class); - $rawCollection = m::mock(); - - $this->setProtected( - $cursor, - 'collection', - $rawCollection - ); - - // Act - $cursor->shouldReceive('generateCacheKey') - ->andReturn('find:collection:123'); - - Ioc::instance(CacheComponentInterface::class, $cacheComponent); - - $cacheComponent->shouldReceive('get') - ->with('find:collection:123', null) - ->andReturn(null); - - $rawCollection->shouldReceive('find') - ->with([], ['limit' => 100]) - ->andReturn(new ArrayIterator($documentsFromDb)); - - $cacheComponent->shouldReceive('put') - ->once() - ->with('find:collection:123', $documentsFromDb, m::any()); - - // Assert - $this->assertEquals( - new ArrayIterator($documentsFromDb), - $this->callProtected($cursor, 'getCursor') - ); - } - - public function testShouldGetOriginalCursorFromDatabaseAfterTheDocumentLimit() - { - // Arrange - $documentsFromDb = [['name' => 'joe'], ['name' => 'doe']]; - $cursor = $this->getCachableCursor()->limit(150); - $cacheComponent = m::mock(CacheComponentInterface::class); - $rawCollection = m::mock(); - - $this->setProtected( - $cursor, - 'position', - CacheableCursor::DOCUMENT_LIMIT + 1 - ); - - $this->setProtected( - $cursor, - 'collection', - $rawCollection - ); - - // Act - $cursor->shouldReceive('generateCacheKey') - ->never(); - - Ioc::instance(CacheComponentInterface::class, $cacheComponent); - - $cacheComponent->shouldReceive('get') - ->with('find:collection:123', null) - ->never(); - - $rawCollection->shouldReceive('find') - ->with([], ['skip' => CacheableCursor::DOCUMENT_LIMIT, 'limit' => 49]) - ->andReturn(new ArrayIterator($documentsFromDb)); - - $cacheComponent->shouldReceive('put') - ->never(); - - // Assert - $this->assertEquals( - new IteratorIterator(new ArrayIterator($documentsFromDb)), - $this->callProtected($cursor, 'getCursor') - ); - } - - public function testShouldGenerateUniqueCacheKey() - { - // Arrange - $cursor = $this->getCachableCursor(null, null, 'find', [['color' => 'red']]); - - // Act - $cursor->shouldReceive('generateCacheKey') - ->passthru(); - - $expectedCacheKey = sprintf( - '%s:%s:%s', - 'find', - 'my_db.my_collection', - md5(serialize([['color' => 'red']])) - ); - - // Assert - - $this->assertEquals( - $expectedCacheKey, - $cursor->generateCacheKey() - ); - } - - protected function getCachableCursor( - $entitySchema = null, - $collection = null, - $command = 'find', - $params = [[]], - $driverCursor = null - ) { - if (!$entitySchema) { - $entitySchema = m::mock(Schema::class.'[]'); - } - - if (!$collection) { - $collection = m::mock(Collection::class); - $collection->shouldReceive('getNamespace') - ->andReturn('my_db.my_collection'); - $collection->shouldReceive('getCollectionName') - ->andReturn('my_collection'); - } - - $mock = m::mock( - CacheableCursor::class.'[generateCacheKey]', - [$entitySchema, $collection, $command, $params] - ); - $mock->shouldAllowMockingProtectedMethods(); - - if ($driverCursor) { - $mock->shouldReceive('getCursor') - ->andReturn($driverCursor); - } - - return $mock; - } -} diff --git a/tests/Mongolid/Cursor/CursorFactoryTest.php b/tests/Mongolid/Cursor/CursorFactoryTest.php deleted file mode 100644 index e902a45c..00000000 --- a/tests/Mongolid/Cursor/CursorFactoryTest.php +++ /dev/null @@ -1,76 +0,0 @@ -createCursor( - $schema, - $collection, - 'find', - $params = ['age' => ['$gr' => 25]] - ); - - $this->assertInstanceOf(Cursor::class, $result); - $this->assertNotInstanceOf(CacheableCursor::class, $result); - $this->assertNotInstanceOf(EmbeddedCursor::class, $result); - $this->assertAttributeEquals($schema, 'entitySchema', $result); - $this->assertAttributeEquals($collection, 'collection', $result); - $this->assertAttributeEquals('find', 'command', $result); - $this->assertAttributeEquals($params, 'params', $result); - } - - public function testShouldCreateACacheableCursor() - { - // Set - $factory = new CursorFactory(); - $schema = m::mock(Schema::class); - $collection = m::mock(Collection::class); - - // Assert - $result = $factory->createCursor( - $schema, - $collection, - 'find', - $params = ['age' => ['$gr' => 25]], - true // $cacheable - ); - - $this->assertInstanceOf(Cursor::class, $result); - $this->assertInstanceOf(CacheableCursor::class, $result); - $this->assertNotInstanceOf(EmbeddedCursor::class, $result); - $this->assertAttributeEquals($schema, 'entitySchema', $result); - $this->assertAttributeEquals($collection, 'collection', $result); - $this->assertAttributeEquals('find', 'command', $result); - $this->assertAttributeEquals($params, 'params', $result); - } - - public function testShouldCreateAEmbeddedCursor() - { - // Set - $factory = new CursorFactory(); - $entityClass = 'MyModelClass'; - - // Assert - $result = $factory->createEmbeddedCursor($entityClass, [['foo' => 'bar']]); - - $this->assertInstanceOf(EmbeddedCursor::class, $result); - $this->assertNotInstanceOf(Cursor::class, $result); - $this->assertNotInstanceOf(CacheableCursor::class, $result); - $this->assertAttributeEquals($entityClass, 'entityClass', $result); - $this->assertAttributeEquals([['foo' => 'bar']], 'items', $result); - } -} diff --git a/tests/Mongolid/Cursor/CursorTest.php b/tests/Mongolid/Cursor/CursorTest.php deleted file mode 100644 index 6ff26933..00000000 --- a/tests/Mongolid/Cursor/CursorTest.php +++ /dev/null @@ -1,483 +0,0 @@ -getCursor(); - - // Assert - $cursor->limit(10); - $this->assertAttributeEquals( - [[], ['limit' => 10]], - 'params', - $cursor - ); - } - - public function testShouldSortDocumentsOfCursor() - { - // Arrange - $cursor = $this->getCursor(); - - // Assert - $cursor->sort(['name' => 1]); - $this->assertAttributeEquals( - [[], ['sort' => ['name' => 1]]], - 'params', - $cursor - ); - } - - public function testShouldSkipDocuments() - { - // Arrange - $cursor = $this->getCursor(); - - // Assert - $cursor->skip(5); - $this->assertAttributeEquals( - [[], ['skip' => 5]], - 'params', - $cursor - ); - } - - public function testShouldSetNoCursorTimeoutToTrue() - { - // Arrange - $cursor = $this->getCursor(); - - // Assert - $cursor->disableTimeout(); - $this->assertAttributeEquals( - [[], ['noCursorTimeout' => true]], - 'params', - $cursor - ); - } - - public function testShouldSetReadPreferenceParameterAccordingly() - { - // Arrange - $cursor = $this->getCursor(); - $mode = ReadPreference::RP_SECONDARY; - $cursor->setReadPreference($mode); - $readPreferenceParameter = $this->getProtected($cursor, 'params')[1]['readPreference']; - - // Assert - $this->assertInstanceOf(ReadPreference::class, $readPreferenceParameter); - $this->assertSame($readPreferenceParameter->getMode(), $mode); - } - - public function testShouldCountDocuments() - { - // Arrange - $collection = m::mock(Collection::class); - $cursor = $this->getCursor(null, $collection); - - // Act - $collection->shouldReceive('count') - ->once() - ->with([]) - ->andReturn(5); - - // Assert - $this->assertEquals(5, $cursor->count()); - } - - public function testShouldCountDocumentsWithCountFunction() - { - // Arrange - $collection = m::mock(Collection::class); - $cursor = $this->getCursor(null, $collection); - - // Act - $collection->shouldReceive('count') - ->once() - ->with([]) - ->andReturn(5); - - // Assert - $this->assertEquals(5, count($cursor)); - } - - public function testShouldRewind() - { - // Arrange - $collection = m::mock(Collection::class); - $driverCursor = m::mock(IteratorIterator::class); - $cursor = $this->getCursor(null, $collection, 'find', [[]], $driverCursor); - - $this->setProtected($cursor, 'position', 10); - - // Act - $driverCursor->shouldReceive('rewind') - ->once(); - - // Assert - $cursor->rewind(); - $this->assertAttributeEquals(0, 'position', $cursor); - } - - public function testShouldRewindACursorThatHasAlreadyBeenInitialized() - { - // Arrange - $collection = m::mock(Collection::class); - $driverCursor = m::mock(IteratorIterator::class); - $cursor = $this->getCursor(null, $collection, 'find', [[]], $driverCursor); - - $this->setProtected($cursor, 'position', 10); - - // Act - $driverCursor->shouldReceive('rewind') - ->twice() - ->andReturnUsing(function () use ($cursor) { - if ($this->getProtected($cursor, 'cursor')) { - throw new LogicException('Cursor already initialized', 1); - } - }); - - // Assert - $cursor->rewind(); - $this->assertAttributeEquals(0, 'position', $cursor); - } - - public function testShouldGetCurrent() - { - // Arrange - $collection = m::mock(Collection::class); - $driverCursor = m::mock(IteratorIterator::class); - $cursor = $this->getCursor(null, $collection, 'find', [[]], $driverCursor); - - // Act - $driverCursor->shouldReceive('current') - ->once() - ->andReturn(['name' => 'John Doe']); - - // Assert - $entity = $cursor->current(); - $this->assertInstanceOf(stdClass::class, $entity); - $this->assertAttributeEquals('John Doe', 'name', $entity); - } - - public function testShouldGetCurrentUsingActiveRecordClasses() - { - // Arrange - $collection = m::mock(Collection::class); - $entity = m::mock(ActiveRecord::class.'[]'); - $entity->name = 'John Doe'; - $driverCursor = new ArrayIterator([$entity]); - $cursor = $this->getCursor(null, $collection, 'find', [[]], $driverCursor); - - // Assert - $entity = $cursor->current(); - $this->assertInstanceOf(ActiveRecord::class, $entity); - $this->assertEquals('John Doe', $entity->name); - } - - public function testShouldGetFirst() - { - // Arrange - $collection = m::mock(Collection::class); - $driverCursor = m::mock(IteratorIterator::class); - $cursor = $this->getCursor(null, $collection, 'find', [[]], $driverCursor); - - // Act - $driverCursor->shouldReceive('rewind') - ->once(); - - $driverCursor->shouldReceive('current') - ->once() - ->andReturn(['name' => 'John Doe']); - - // Assert - $entity = $cursor->first(); - $this->assertInstanceOf(stdClass::class, $entity); - $this->assertAttributeEquals('John Doe', 'name', $entity); - } - - public function testShouldGetFirstWhenEmpty() - { - // Arrange - $collection = m::mock(Collection::class); - $driverCursor = m::mock(IteratorIterator::class); - $cursor = $this->getCursor(null, $collection, 'find', [[]], $driverCursor); - - // Act - $driverCursor->shouldReceive('rewind') - ->once(); - - $driverCursor->shouldReceive('current') - ->once() - ->andReturn(null); - - // Assert - $result = $cursor->first(); - $this->assertNull($result); - } - - public function testShouldRefreshTheCursor() - { - // Arrange - $driverCursor = m::mock(IteratorIterator::class); - $cursor = $this->getCursor(); - $this->setProtected($cursor, 'cursor', $driverCursor); - - // Assert - $cursor->fresh(); - $this->assertAttributeEquals(null, 'cursor', $cursor); - } - - public function testShouldImplementKeyMethodFromIterator() - { - // Arrange - $cursor = $this->getCursor(); - - $this->setProtected($cursor, 'position', 7); - - // Assertion - $this->assertEquals(7, $cursor->key()); - } - - public function testShouldImplementNextMethodFromIterator() - { - // Arrange - $collection = m::mock(Collection::class); - $driverCursor = m::mock(IteratorIterator::class); - $cursor = $this->getCursor(null, $collection, 'find', [[]], $driverCursor); - - $this->setProtected($cursor, 'position', 7); - - // Act - $driverCursor->shouldReceive('next') - ->once(); - - // Assert - $cursor->next(); - $this->assertEquals(8, $cursor->key()); - } - - public function testShouldImplementValidMethodFromIterator() - { - // Arrange - $collection = m::mock(Collection::class); - $driverCursor = m::mock(IteratorIterator::class); - $cursor = $this->getCursor(null, $collection, 'find', [[]], $driverCursor); - - // Act - $driverCursor->shouldReceive('valid') - ->andReturn(true); - - // Assert - $this->assertTrue($cursor->valid()); - } - - public function testShouldWrapMongoDriverCursorWithIteratoriterator() - { - // Arrange - $collection = m::mock(Collection::class); - $cursor = $this->getCursor(null, $collection, 'find', [['bacon' => true]]); - $driverCursor = m::mock(Traversable::class); - $driverIterator = m::mock(Iterator::class); - - // Act - $collection->shouldReceive('find') - ->once() - ->with(['bacon' => true]) - ->andReturn($driverCursor); - - $driverCursor->shouldReceive('getIterator') - ->andReturn($driverIterator); - - // Because when creating an IteratorIterator with the driverCursor - // this methods will be called once to initialize the iterable object. - $driverIterator->shouldReceive('rewind', 'valid', 'current', 'key') - ->once() - ->andReturn(true); - - // Assert - $result = $this->callProtected($cursor, 'getCursor'); - $this->assertInstanceOf(IteratorIterator::class, $result); - } - - public function testShouldReturnAllResults() - { - // Arrange - $collection = m::mock(Collection::class); - $driverCursor = m::mock(IteratorIterator::class); - $cursor = $this->getCursor(null, $collection, 'find', [[]], $driverCursor); - - // Act - $driverCursor->shouldReceive('rewind', 'valid', 'key') - ->andReturn(true, true, false); - - $driverCursor->shouldReceive('next') - ->andReturn(true, false); - - $driverCursor->shouldReceive('current') - ->twice() - ->andReturn( - ['name' => 'bob', 'occupation' => 'coder'], - ['name' => 'jef', 'occupation' => 'tester'] - ); - - $result = $cursor->all(); - - // Assert - $this->assertEquals( - [ - (object) ['name' => 'bob', 'occupation' => 'coder'], - (object) ['name' => 'jef', 'occupation' => 'tester'], - ], - $result - ); - } - - public function testShouldReturnResultsToArray() - { - // Arrange - $collection = m::mock(Collection::class); - $driverCursor = m::mock(IteratorIterator::class); - $cursor = $this->getCursor(null, $collection, 'find', [[]], $driverCursor); - - // Act - $driverCursor->shouldReceive('rewind', 'valid', 'key') - ->andReturn(true, true, false); - - $driverCursor->shouldReceive('next') - ->andReturn(true, false); - - $driverCursor->shouldReceive('current') - ->twice() - ->andReturn( - ['name' => 'bob', 'occupation' => 'coder'], - ['name' => 'jef', 'occupation' => 'tester'] - ); - - $result = $cursor->toArray(); - - // Assert - $this->assertEquals( - [ - ['name' => 'bob', 'occupation' => 'coder'], - ['name' => 'jef', 'occupation' => 'tester'], - ], - $result - ); - } - - public function testShouldSerializeAnActiveCursor() - { - // Arrange - $pool = m::mock(Pool::class); - $conn = m::mock(Connection::class); - $schema = new DynamicSchema(); - $cursor = $this->getCursor($schema, null, 'find', [[]]); - $driverCollection = $this->getDriverCollection(); - - $this->setProtected($cursor, 'collection', $driverCollection); - - // Act - Ioc::instance(Pool::class, $pool); - - $pool->shouldReceive('getConnection') - ->andReturn($conn); - - $conn->shouldReceive('getRawConnection') - ->andReturn($conn); - - $conn->defaultDatabase = 'db'; - $conn->db = $conn; - $conn->my_collection = $driverCollection; // Return the same driver Collection - - // Assert - $result = unserialize(serialize($cursor)); - $this->assertEquals($cursor, $result); - } - - protected function getCursor( - $entitySchema = null, - $collection = null, - $command = 'find', - $params = [[]], - $driverCursor = null - ) { - if (!$entitySchema) { - $entitySchema = m::mock(Schema::class.'[]'); - } - - if (!$collection) { - $collection = m::mock(Collection::class); - } - - if (!$driverCursor) { - return new Cursor($entitySchema, $collection, $command, $params); - } - - $mock = m::mock( - Cursor::class.'[getCursor]', - [$entitySchema, $collection, $command, $params] - ); - - $mock->shouldAllowMockingProtectedMethods(); - $mock->shouldReceive('getCursor') - ->andReturn($driverCursor); - - $this->setProtected($mock, 'cursor', $driverCursor); - - return $mock; - } - - /** - * Since the MongoDB\Collection is not serializable. This method will - * emulate an unserializable collection from mongoDb driver. - */ - protected function getDriverCollection() - { - /* - * Emulates a MongoDB\Collection non serializable behavior. - */ - return new class() implements \Serializable { - public function serialize() - { - throw new Exception('Unable to serialize', 1); - } - - public function unserialize($serialized) - { - } - - public function getCollectionName() - { - return 'my_collection'; - } - }; - } -} diff --git a/tests/Mongolid/DataMapper/BulkWriteTest.php b/tests/Mongolid/DataMapper/BulkWriteTest.php deleted file mode 100644 index febf1467..00000000 --- a/tests/Mongolid/DataMapper/BulkWriteTest.php +++ /dev/null @@ -1,210 +0,0 @@ -shouldReceive('getSchema') - ->once(); - - // Act - $bulkWrite = new BulkWrite($entity); - - // Assert - $this->assertInstanceOf(BulkWrite::class, $bulkWrite); - } - - public function testShouldSetAndGetMongoBulkWrite() - { - // Arrange - $entity = m::mock(HasSchemaInterface::class); - $mongoBulkWrite = new MongoBulkWrite(); - - // Expect - $entity->shouldReceive('getSchema') - ->once(); - - // Act - $bulkWrite = new BulkWrite($entity); - $bulkWrite->setBulkWrite($mongoBulkWrite); - - // Assert - $this->assertSame($mongoBulkWrite, $bulkWrite->getBulkWrite()); - } - - public function testShouldAddUpdateOneOperationToBulkWrite() - { - // Arrange - $entity = m::mock(HasSchemaInterface::class); - $mongoBulkWrite = m::mock(new MongoBulkWrite()); - - $id = '123'; - $data = ['name' => 'John']; - - // Expect - $entity->shouldReceive('getSchema') - ->once(); - - $mongoBulkWrite->shouldReceive('update') - ->once() - ->with(['_id' => $id], ['$set' => $data], ['upsert' => true]); - - $bulkWrite = m::mock(BulkWrite::class.'[getBulkWrite]', [$entity]); - - $bulkWrite->shouldReceive('getBulkWrite') - ->once() - ->with() - ->andReturn($mongoBulkWrite); - - // Act - $bulkWrite->updateOne($id, $data); - } - - public function testShouldUpdateOneWithUnsetOperationToBulkWrite() - { - // Arrange - $entity = m::mock(HasSchemaInterface::class); - $mongoBulkWrite = m::mock(new MongoBulkWrite()); - - $id = '123'; - $data = ['name' => 'John']; - - // Expect - $entity->shouldReceive('getSchema') - ->withNoArgs() - ->once(); - - $mongoBulkWrite->expects() - ->update( - m::on( - function ($actual) { - $this->assertSame(['_id' => '123'], $actual); - - return true; - } - ), - ['$unset' => $data], - ['upsert' => true] - ); - - - $bulkWrite = m::mock(BulkWrite::class.'[getBulkWrite]', [$entity]); - - $bulkWrite->shouldReceive('getBulkWrite') - ->with() - ->once() - ->andReturn($mongoBulkWrite); - - // Act - $bulkWrite->updateOne($id, $data, ['upsert' => true], '$unset'); - } - - public function testShouldUpdateOneWithQueryOnFilterToBulkWrite() - { - // Arrange - $entity = m::mock(HasSchemaInterface::class); - $mongoBulkWrite = m::mock(new MongoBulkWrite()); - - $query = ['_id' => '123', 'grades.grade' => 85]; - $data = ['grades.std' => 6]; - - // Expect - $entity->shouldReceive('getSchema') - ->withNoArgs() - ->once(); - - $mongoBulkWrite->expects() - ->update( - m::on( - function ($actual) use ($query) { - $this->assertSame($query, $actual); - - return true; - } - ), - ['$unset' => $data], - ['upsert' => true] - ); - - $bulkWrite = m::mock(BulkWrite::class.'[getBulkWrite]', [$entity]); - - $bulkWrite->shouldReceive('getBulkWrite') - ->with() - ->once() - ->andReturn($mongoBulkWrite); - - // Act - $bulkWrite->updateOne($query, $data, ['upsert' => true], '$unset'); - } - - public function testShouldExecuteBulkWrite() - { - $entity = m::mock(HasSchemaInterface::class); - $schema = m::mock(Schema::class); - $entity->schema = $schema; - $mongoBulkWrite = m::mock(new MongoBulkWrite()); - $pool = m::mock(Pool::class); - $connection = m::mock(Connection::class); - $manager = m::mock(Manager::class); - - $connection->defaultDatabase = 'foo'; - $schema->collection = 'bar'; - $namespace = 'foo.bar'; - - Ioc::instance(Pool::class, $pool); - - // Expect - $entity->shouldReceive('getSchema') - ->once() - ->with() - ->andReturn($schema); - - $pool->shouldReceive('getConnection') - ->once() - ->with() - ->andReturn($connection); - - $connection->shouldReceive('getRawManager') - ->once() - ->with() - ->andReturn($manager); - - $manager->shouldReceive('executeBulkWrite') - ->once() - ->with($namespace, $mongoBulkWrite, ['writeConcern' => new WriteConcern(1)]) - ->andReturn(true); - - $bulkWrite = m::mock(BulkWrite::class.'[getBulkWrite]', [$entity]); - - $bulkWrite->shouldReceive('getBulkWrite') - ->once() - ->with() - ->andReturn($mongoBulkWrite); - - // Act - $bulkWrite->execute(); - } -} diff --git a/tests/Mongolid/DataMapper/DataMapperTest.php b/tests/Mongolid/DataMapper/DataMapperTest.php deleted file mode 100644 index e157e2c8..00000000 --- a/tests/Mongolid/DataMapper/DataMapperTest.php +++ /dev/null @@ -1,963 +0,0 @@ -eventService); - parent::tearDown(); - m::close(); - } - - public function testShouldBeAbleToConstructWithSchema() - { - // Arrange - $connPool = m::mock(Pool::class); - - // Act - $mapper = new DataMapper($connPool); - - // Assert - $this->assertAttributeEquals($connPool, 'connPool', $mapper); - } - - /** - * @dataProvider getWriteConcernVariations - */ - public function testShouldSave($entity, $writeConcern, $shouldFireEventAfter, $expected) - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = m::mock(DataMapper::class.'[parseToDocument,getCollection]', [$connPool]); - $options = ['writeConcern' => new WriteConcern($writeConcern)]; - - $collection = m::mock(Collection::class); - $parsedObject = ['_id' => 123]; - $operationResult = m::mock(); - - $entity->_id = null; - - // Act - $mapper->shouldAllowMockingProtectedMethods(); - - $mapper->shouldReceive('parseToDocument') - ->once() - ->with($entity) - ->andReturn($parsedObject); - - $mapper->shouldReceive('getCollection') - ->once() - ->andReturn($collection); - - $collection->shouldReceive('replaceOne') - ->once() - ->with( - ['_id' => 123], - $parsedObject, - ['upsert' => true, 'writeConcern' => new WriteConcern($writeConcern)] - )->andReturn($operationResult); - - $operationResult->shouldReceive('isAcknowledged') - ->once() - ->andReturn((bool) $writeConcern); - - $operationResult->shouldReceive('getModifiedCount', 'getUpsertedCount') - ->andReturn(1); - - if ($entity instanceof AttributesAccessInterface) { - $entity->shouldReceive('syncOriginalAttributes') - ->once() - ->with(); - } - - $this->expectEventToBeFired('saving', $entity, true); - - if ($shouldFireEventAfter) { - $this->expectEventToBeFired('saved', $entity, false); - } else { - $this->expectEventNotToBeFired('saved', $entity); - } - - // Assert - $this->assertEquals($expected, $mapper->save($entity, $options)); - } - - /** - * @dataProvider getWriteConcernVariations - */ - public function testShouldInsert($entity, $writeConcern, $shouldFireEventAfter, $expected) - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = m::mock(DataMapper::class.'[parseToDocument,getCollection]', [$connPool]); - $options = ['writeConcern' => new WriteConcern($writeConcern)]; - - $collection = m::mock(Collection::class); - $parsedObject = ['_id' => 123]; - $operationResult = m::mock(); - - $entity->_id = null; - - // Act - $mapper->shouldAllowMockingProtectedMethods(); - - $mapper->shouldReceive('parseToDocument') - ->once() - ->with($entity) - ->andReturn($parsedObject); - - $mapper->shouldReceive('getCollection') - ->once() - ->andReturn($collection); - - $collection->shouldReceive('insertOne') - ->once() - ->with($parsedObject, ['writeConcern' => new WriteConcern($writeConcern)]) - ->andReturn($operationResult); - - $operationResult->shouldReceive('isAcknowledged') - ->once() - ->andReturn((bool) $writeConcern); - - $operationResult->shouldReceive('getInsertedCount') - ->andReturn(1); - - if ($entity instanceof AttributesAccessInterface) { - $entity->shouldReceive('syncOriginalAttributes') - ->once() - ->with(); - } - - $this->expectEventToBeFired('inserting', $entity, true); - - if ($shouldFireEventAfter) { - $this->expectEventToBeFired('inserted', $entity, false); - } else { - $this->expectEventNotToBeFired('inserted', $entity); - } - - // Assert - $this->assertEquals($expected, $mapper->insert($entity, $options)); - } - - /** - * @dataProvider getWriteConcernVariations - */ - public function testShouldInsertWithoutFiringEvents($entity, $writeConcern, $shouldFireEventAfter, $expected) - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = m::mock(DataMapper::class.'[parseToDocument,getCollection]', [$connPool]); - $options = ['writeConcern' => new WriteConcern($writeConcern)]; - - $collection = m::mock(Collection::class); - $parsedObject = ['_id' => 123]; - $operationResult = m::mock(); - - $entity->_id = null; - - // Act - $mapper->shouldAllowMockingProtectedMethods(); - - $mapper->shouldReceive('parseToDocument') - ->once() - ->with($entity) - ->andReturn($parsedObject); - - $mapper->shouldReceive('getCollection') - ->once() - ->andReturn($collection); - - $collection->shouldReceive('insertOne') - ->once() - ->with($parsedObject, ['writeConcern' => new WriteConcern($writeConcern)]) - ->andReturn($operationResult); - - $operationResult->shouldReceive('isAcknowledged') - ->once() - ->andReturn((bool) $writeConcern); - - $operationResult->shouldReceive('getInsertedCount') - ->andReturn(1); - - if ($entity instanceof AttributesAccessInterface) { - $entity->shouldReceive('syncOriginalAttributes') - ->once() - ->with(); - } - - $this->expectEventNotToBeFired('inserting', $entity); - $this->expectEventNotToBeFired('inserted', $entity); - - // Assert - $this->assertEquals($expected, $mapper->insert($entity, $options, false)); - } - - /** - * @dataProvider getWriteConcernVariations - */ - public function testShouldUpdate($entity, $writeConcern, $shouldFireEventAfter, $expected) - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = m::mock(DataMapper::class.'[parseToDocument,getCollection]', [$connPool]); - - $collection = m::mock(Collection::class); - $parsedObject = ['_id' => 123]; - $operationResult = m::mock(); - $options = ['writeConcern' => new WriteConcern($writeConcern)]; - - $entity->_id = 123; - - // Act - $mapper->shouldAllowMockingProtectedMethods(); - - $mapper->shouldReceive('parseToDocument') - ->once() - ->with($entity) - ->andReturn($parsedObject); - - $mapper->shouldReceive('getCollection') - ->once() - ->andReturn($collection); - - $collection->shouldReceive('updateOne') - ->once() - ->with( - ['_id' => 123], - ['$set' => $parsedObject], - ['writeConcern' => new WriteConcern($writeConcern)] - )->andReturn($operationResult); - - $operationResult->shouldReceive('isAcknowledged') - ->once() - ->andReturn((bool) $writeConcern); - - $operationResult->shouldReceive('getModifiedCount') - ->andReturn(1); - - if ($entity instanceof AttributesAccessInterface) { - $entity->shouldReceive('syncOriginalAttributes') - ->once() - ->with(); - } - - $this->expectEventToBeFired('updating', $entity, true); - - if ($shouldFireEventAfter) { - $this->expectEventToBeFired('updated', $entity, false); - } else { - $this->expectEventNotToBeFired('updated', $entity); - } - - // Assert - $this->assertEquals($expected, $mapper->update($entity, $options)); - } - - /** - * @dataProvider getWriteConcernVariations - */ - public function testUpdateShouldCallInsertWhenObjectHasNoId( - $entity, - $writeConcern, - $shouldFireEventAfter, - $expected - ) { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = m::mock(DataMapper::class.'[parseToDocument,getCollection]', [$connPool]); - - $collection = m::mock(Collection::class); - $parsedObject = ['_id' => 123]; - $operationResult = m::mock(); - $options = ['writeConcern' => new WriteConcern($writeConcern)]; - - $entity->_id = null; - - // Act - $mapper->shouldAllowMockingProtectedMethods(); - - $mapper->shouldReceive('parseToDocument') - ->once() - ->with($entity) - ->andReturn($parsedObject); - - $mapper->shouldReceive('getCollection') - ->once() - ->andReturn($collection); - - $collection->shouldReceive('insertOne') - ->once() - ->with( - $parsedObject, - ['writeConcern' => new WriteConcern($writeConcern)] - )->andReturn($operationResult); - - $operationResult->shouldReceive('isAcknowledged') - ->once() - ->andReturn((bool) $writeConcern); - - $operationResult->shouldReceive('getInsertedCount') - ->andReturn(1); - - if ($entity instanceof AttributesAccessInterface) { - $entity->shouldReceive('syncOriginalAttributes') - ->once() - ->with(); - } - - $this->expectEventToBeFired('updating', $entity, true); - - if ($shouldFireEventAfter) { - $this->expectEventToBeFired('updated', $entity, false); - } else { - $this->expectEventNotToBeFired('updated', $entity); - } - - $this->expectEventNotToBeFired('inserting', $entity); - $this->expectEventNotToBeFired('inserted', $entity); - - // Assert - $this->assertEquals($expected, $mapper->update($entity, $options)); - } - - /** - * @dataProvider getWriteConcernVariations - */ - public function testShouldDelete($entity, $writeConcern, $shouldFireEventAfter, $expected) - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = m::mock(DataMapper::class.'[parseToDocument,getCollection]', [$connPool]); - - $collection = m::mock(Collection::class); - $parsedObject = ['_id' => 123]; - $operationResult = m::mock(); - $options = ['writeConcern' => new WriteConcern($writeConcern)]; - - $entity->_id = null; - - // Act - $mapper->shouldAllowMockingProtectedMethods(); - - $mapper->shouldReceive('parseToDocument') - ->once() - ->with($entity) - ->andReturn($parsedObject); - - $mapper->shouldReceive('getCollection') - ->once() - ->andReturn($collection); - - $collection->shouldReceive('deleteOne') - ->once() - ->with(['_id' => 123], ['writeConcern' => new WriteConcern($writeConcern)]) - ->andReturn($operationResult); - - $operationResult->shouldReceive('isAcknowledged') - ->once() - ->andReturn((bool) $writeConcern); - - $operationResult->shouldReceive('getDeletedCount') - ->andReturn(1); - - if ($entity instanceof AttributesAccessInterface) { - $entity->shouldReceive('syncOriginalAttributes') - ->once() - ->with(); - } - - $this->expectEventToBeFired('deleting', $entity, true); - - if ($shouldFireEventAfter) { - $this->expectEventToBeFired('deleted', $entity, false); - } else { - $this->expectEventNotToBeFired('deleted', $entity); - } - - // Assert - $this->assertEquals($expected, $mapper->delete($entity, $options)); - } - - /** - * @dataProvider eventsToBailOperations - */ - public function testDatabaseOperationsShouldBailOutIfTheEventHandlerReturnsFalse( - $operation, - $dbOperation, - $eventName - ) { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = m::mock(DataMapper::class.'[parseToDocument,getCollection]', [$connPool]); - $collection = m::mock(Collection::class); - $entity = m::mock(); - - $mapper->shouldAllowMockingProtectedMethods(); - - // Expect - $mapper->shouldReceive('parseToDocument') - ->with($entity) - ->never(); - - $mapper->shouldReceive('getCollection') - ->andReturn($collection); - - $collection->shouldReceive($dbOperation) - ->never(); - - /* "Mocks" the fireEvent to return false and bail the operation */ - $this->expectEventToBeFired($eventName, $entity, true, false); - - // Act - $result = $mapper->$operation($entity); - - // Assert - $this->assertFalse($result); - } - - public function testShouldGetWithWhereQuery() - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = m::mock(DataMapper::class.'[prepareValueQuery,getCollection]', [$connPool]); - $schema = m::mock(Schema::class); - - $collection = m::mock(Collection::class); - $query = 123; - $preparedQuery = ['_id' => 123]; - $projection = ['project' => true, '_id' => false]; - - $schema->entityClass = 'stdClass'; - $mapper->setSchema($schema); - - $mapper->shouldAllowMockingProtectedMethods(); - - // Expect - $mapper->shouldReceive('prepareValueQuery') - ->with($query) - ->andReturn($preparedQuery); - - $mapper->shouldReceive('getCollection') - ->andReturn($collection); - - // Act - $result = $mapper->where($query, $projection); - $cacheableResult = $mapper->where($query, [], true); - - // Assert - $this->assertInstanceOf(Cursor::class, $result); - $this->assertNotInstanceOf(CacheableCursor::class, $result); - $this->assertAttributeEquals($schema, 'entitySchema', $result); - $this->assertAttributeEquals($collection, 'collection', $result); - $this->assertAttributeEquals('find', 'command', $result); - $this->assertAttributeEquals( - [$preparedQuery, ['projection' => $projection]], - 'params', - $result - ); - - $this->assertInstanceOf(CacheableCursor::class, $cacheableResult); - $this->assertAttributeEquals($schema, 'entitySchema', $cacheableResult); - $this->assertAttributeEquals($collection, 'collection', $cacheableResult); - $this->assertAttributeEquals( - [$preparedQuery, ['projection' => []]], - 'params', - $cacheableResult - ); - } - - public function testShouldGetAll() - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = m::mock(DataMapper::class.'[where]', [$connPool]); - $mongolidCursor = m::mock(Cursor::class); - - // Expect - $mapper->shouldReceive('where') - ->once() - ->with([]) - ->andReturn($mongolidCursor); - - // Act - $result = $mapper->all(); - - // Assert - $this->assertEquals($mongolidCursor, $result); - } - - public function testShouldGetFirstWithQuery() - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = m::mock(DataMapper::class.'[prepareValueQuery,getCollection]', [$connPool]); - $schema = m::mock(Schema::class); - $collection = m::mock(Collection::class); - $query = 123; - $preparedQuery = ['_id' => 123]; - - $schema->entityClass = 'stdClass'; - $mapper->setSchema($schema); - - $mapper->shouldAllowMockingProtectedMethods(); - - // Act - $mapper->shouldReceive('prepareValueQuery') - ->once() - ->with($query) - ->andReturn($preparedQuery); - - $mapper->shouldReceive('getCollection') - ->once() - ->andReturn($collection); - - $collection->shouldReceive('findOne') - ->once() - ->with($preparedQuery, ['projection' => []]) - ->andReturn(['name' => 'John Doe']); - - $result = $mapper->first($query); - - // Assert - $this->assertInstanceOf(stdClass::class, $result); - $this->assertAttributeEquals('John Doe', 'name', $result); - } - - public function testShouldGetNullIfFirstCantFindAnything() - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = m::mock(DataMapper::class.'[prepareValueQuery,getCollection]', [$connPool]); - $schema = m::mock(Schema::class); - - $collection = m::mock(Collection::class); - $query = 123; - $preparedQuery = ['_id' => 123]; - - $schema->entityClass = 'stdClass'; - $mapper->setSchema($schema); - - $mapper->shouldAllowMockingProtectedMethods(); - - // Expect - $mapper->shouldReceive('prepareValueQuery') - ->once() - ->with($query) - ->andReturn($preparedQuery); - - $mapper->shouldReceive('getCollection') - ->once() - ->andReturn($collection); - - $collection->shouldReceive('findOne') - ->once() - ->with($preparedQuery, ['projection' => []]) - ->andReturn(null); - - // Act - $result = $mapper->first($query); - - // Assert - $this->assertNull($result); - } - - public function testShouldGetFirstProjectingFields() - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = m::mock( - DataMapper::class.'[prepareValueQuery,getCollection]', - [$connPool] - ); - $schema = m::mock(Schema::class); - - $collection = m::mock(Collection::class); - $query = 123; - $preparedQuery = ['_id' => 123]; - $projection = ['project' => true, 'fields' => false]; - - $schema->entityClass = 'stdClass'; - $mapper->setSchema($schema); - - $mapper->shouldAllowMockingProtectedMethods(); - - // Expect - $mapper->shouldReceive('prepareValueQuery') - ->once() - ->with($query) - ->andReturn($preparedQuery); - - $mapper->shouldReceive('getCollection') - ->once() - ->andReturn($collection); - - $collection->shouldReceive('findOne') - ->once() - ->with($preparedQuery, ['projection' => $projection]) - ->andReturn(null); - - // Act - $result = $mapper->first($query, $projection); - - // Assert - $this->assertNull($result); - } - - public function testShouldGetFirstTroughACacheableCursor() - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = m::mock(DataMapper::class.'[where]', [$connPool]); - $query = 123; - $entity = new stdClass(); - $cursor = m::mock(CacheableCursor::class); - - // Expect - $mapper->shouldReceive('where') - ->once() - ->with($query, [], true) - ->andReturn($cursor); - - $cursor->shouldReceive('first') - ->once() - ->andReturn($entity); - - // Act - $result = $mapper->first($query, [], true); - - // Assert - $this->assertEquals($entity, $result); - } - - public function testShouldGetFirstTroughACacheableCursorProjectingFields() - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = m::mock(DataMapper::class.'[where]', [$connPool]); - $query = 123; - $entity = new stdClass(); - $cursor = m::mock(CacheableCursor::class); - $projection = ['project' => true, '_id' => false]; - - // Expect - $mapper->shouldReceive('where') - ->once() - ->with($query, $projection, true) - ->andReturn($cursor); - - $cursor->shouldReceive('first') - ->once() - ->andReturn($entity); - - // Act - $result = $mapper->first($query, $projection, true); - - // Assert - $this->assertEquals($entity, $result); - } - - public function testShouldParseObjectToDocumentAndPutResultingIdIntoTheGivenObject() - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = m::mock(DataMapper::class.'[getSchemaMapper]', [$connPool]); - $entity = m::mock(); - $parsedDocument = ['a_field' => 123, '_id' => 'bacon']; - $schemaMapper = m::mock(Schema::class.'[]'); - - $mapper->shouldAllowMockingProtectedMethods(); - - // Expect - $mapper->shouldReceive('getSchemaMapper') - ->once() - ->andReturn($schemaMapper); - - $schemaMapper->shouldReceive('map') - ->once() - ->with($entity) - ->andReturn($parsedDocument); - - // Act - $result = $this->callProtected($mapper, 'parseToDocument', $entity); - - // Assert - $this->assertEquals($parsedDocument, $result); - $this->assertEquals( - 'bacon', // Since this was the parsedDocument _id - $entity->_id - ); - } - - public function testShouldGetSchemaMapper() - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = new DataMapper($connPool); - $mapper->schemaClass = 'MySchema'; - $schema = m::mock(Schema::class); - - Ioc::instance('MySchema', $schema); - - // Act - $result = $this->callProtected($mapper, 'getSchemaMapper'); - - // Assert - $this->assertInstanceOf(SchemaMapper::class, $result); - $this->assertEquals($schema, $result->schema); - } - - public function testShouldGetRawCollection() - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = new DataMapper($connPool); - $connection = m::mock(Connection::class); - $collection = m::mock(Collection::class); - $schema = m::mock(Schema::class); - $schema->collection = 'foobar'; - - $mapper->setSchema($schema); - $connection->defaultDatabase = 'grimory'; - $connection->grimory = (object) ['foobar' => $collection]; - - // Expect - $connPool->shouldReceive('getConnection') - ->once() - ->andReturn($connection); - - $connection->shouldReceive('getRawConnection') - ->andReturn($connection); - - // Act - $result = $this->callProtected($mapper, 'getCollection'); - - // Assert - $this->assertEquals($collection, $result); - } - - /** - * @dataProvider queryValueScenarios - */ - public function testShouldPrepareQueryValue($value, $expectation) - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = new DataMapper($connPool); - - // Act - $result = $this->callProtected($mapper, 'prepareValueQuery', [$value]); - - // Assert - $this->assertMongoQueryEquals($expectation, $result); - } - - /** - * @dataProvider getProjections - */ - public function testPrepareProjectionShouldConvertArray($data, $expectation) - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = new DataMapper($connPool); - - // Act - $result = $this->callProtected($mapper, 'prepareProjection', [$data]); - - // Assert - $this->assertEquals($expectation, $result); - } - - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Invalid projection: 'invalid-key' => 'invalid-value' - */ - public function testPrepareProjectionShouldThrownAnException() - { - // Arrange - $connPool = m::mock(Pool::class); - $mapper = new DataMapper($connPool); - $data = ['valid' => true, 'invalid-key' => 'invalid-value']; - - // Act - $this->callProtected($mapper, 'prepareProjection', [$data]); - } - - protected function getEventService() - { - if (!($this->eventService ?? false)) { - $this->eventService = m::mock(EventTriggerService::class); - Ioc::instance(EventTriggerService::class, $this->eventService); - } - - return $this->eventService; - } - - protected function expectEventToBeFired($event, $entity, bool $halt, $return = true) - { - $event = 'mongolid.'.$event.': '.get_class($entity); - - $this->getEventService()->shouldReceive('fire') - ->with($event, $entity, $halt) - ->atLeast() - ->once() - ->andReturn($return); - } - - protected function expectEventNotToBeFired($event, $entity) - { - $event = 'mongolid.'.$event.': '.get_class($entity); - - $this->getEventService()->shouldReceive('fire') - ->with($event, $entity, m::any()) - ->never(); - } - - public function eventsToBailOperations() - { - return [ - 'Saving event' => [ - 'operation' => 'save', - 'dbOperation' => 'replaceOne', - 'eventName' => 'saving', - ], - // ------------------------ - 'Inserting event' => [ - 'operation' => 'insert', - 'dbOperation' => 'insertOne', - 'eventName' => 'inserting', - ], - // ------------------------ - 'Updating event' => [ - 'operation' => 'update', - 'dbOperation' => 'updateOne', - 'eventName' => 'updating', - ], - // ------------------------ - 'Deleting event' => [ - 'operation' => 'delete', - 'dbOperation' => 'deleteOne', - 'eventName' => 'deleting', - ], - ]; - } - - public function queryValueScenarios() - { - return [ - 'An array' => [ - 'value' => ['age' => ['$gt' => 25]], - 'expectation' => ['age' => ['$gt' => 25]], - ], - // ------------------------ - 'An ObjectId string' => [ - 'value' => '507f1f77bcf86cd799439011', - 'expectation' => ['_id' => new ObjectID('507f1f77bcf86cd799439011')], - ], - // ------------------------ - 'An ObjectId string within a query' => [ - 'value' => ['_id' => '507f1f77bcf86cd799439011'], - 'expectation' => ['_id' => new ObjectID('507f1f77bcf86cd799439011')], - ], - // ------------------------ - 'Other type of _id, sequence for example' => [ - 'value' => 7, - 'expectation' => ['_id' => 7], - ], - // ------------------------ - 'Series of string _ids as the $in parameter' => [ - 'value' => ['_id' => ['$in' => ['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012']]], - 'expectation' => [ - '_id' => [ - '$in' => [ - new ObjectID('507f1f77bcf86cd799439011'), - new ObjectID('507f1f77bcf86cd799439012'), - ], - ], - ], - ], - // ------------------------ - 'Series of string _ids as the $in parameter' => [ - 'value' => ['_id' => ['$nin' => ['507f1f77bcf86cd799439011']]], - 'expectation' => ['_id' => ['$nin' => [new ObjectID('507f1f77bcf86cd799439011')]]], - ], - ]; - } - - public function getWriteConcernVariations() - { - return [ - 'acknowledged write concern with plain object' => [ - 'object' => m::mock(), - 'writeConcern' => 1, - 'shouldFireEventAfter' => true, - 'expected' => true, - ], - 'acknowledged write concern with attributesAccessIntesarface' => [ - 'object' => m::mock(AttributesAccessInterface::class), - 'writeConcern' => 1, - 'shouldFireEventAfter' => true, - 'expected' => true, - ], - 'unacknowledged write concern with plain object' => [ - 'object' => m::mock(), - 'writeConcern' => 0, - 'shouldFireEventAfter' => false, - 'expected' => false, - ], - 'unacknowledged write concern with attributesAccessInterface' => [ - 'object' => m::mock(AttributesAccessInterface::class), - 'writeConcern' => 0, - 'shouldFireEventAfter' => false, - 'expected' => false, - ], - ]; - } - - /** - * Retrieves projections that should be replaced by mapper. - */ - public function getProjections() - { - return [ - 'Should return self array' => [ - 'projection' => ['some' => true, 'fields' => false], - 'expected' => ['some' => true, 'fields' => false], - ], - 'Should convert number' => [ - 'projection' => ['some' => 1, 'fields' => -1], - 'expected' => ['some' => true, 'fields' => false], - ], - 'Should add true in fields' => [ - 'projection' => ['some', 'fields'], - 'expected' => ['some' => true, 'fields' => true], - ], - 'Should add boolean values according to key value' => [ - 'projection' => ['-some', 'fields'], - 'expected' => ['some' => false, 'fields' => true], - ], - ]; - } -} diff --git a/tests/Mongolid/DataMapper/EntityAssemblerTest.php b/tests/Mongolid/DataMapper/EntityAssemblerTest.php deleted file mode 100644 index b41b9f62..00000000 --- a/tests/Mongolid/DataMapper/EntityAssemblerTest.php +++ /dev/null @@ -1,301 +0,0 @@ - $value) { - $schemas[$key] = m::mock(Schema::class.'[]'); - $schemas[$key]->entityClass = $value['entityClass']; - $schemas[$key]->fields = $value['fields']; - } - - // Act - foreach ($schemas as $className => $instance) { - Ioc::instance($className, $instance); - } - - // Assert - $result = $entityAssembler->assemble($inputValue, $schemas[$inputSchema]); - $this->assertEquals($expectedOutput, $result); - } - - public function EntityAssemblerFixture() - { - return [ - //--------------------------- - - 'A simple schema to a entity' => [ - 'inputValue' => [ // Data that will be used to assembly the entity - '_id' => new ObjectID('507f1f77bcf86cd799439011'), - 'name' => 'John Doe', - 'age' => 25, - 'grade' => 7.25, - ], - 'availableSchmas' => [ // Schemas that will exist in the test context - 'studentSchema' => [ - 'entityClass' => _stubStudent::class, - 'fields' => [ - '_id' => 'objectId', - 'name' => 'string', - 'age' => 'integer', - 'grade' => 'float', - 'finalGrade' => 'float', - ], - ], - ], - 'inputSchema' => 'studentSchema', // Schema that will be used to assembly $inputValue - 'expectedOutput' => new _stubStudent([ // Expected output - '_id' => new ObjectID('507f1f77bcf86cd799439011'), - 'name' => 'John Doe', - 'age' => 25, - 'grade' => 7.25, - ]), - ], - - //--------------------------- - - 'A schema containing an embeded schema but with null field' => [ - 'inputValue' => [ // Data that will be used to assembly the entity - '_id' => new ObjectID('507f1f77bcf86cd799439011'), - 'name' => 'John Doe', - 'age' => 25, - 'tests' => null, - 'finalGrade' => 7.25, - ], - 'availableSchmas' => [ // Schemas that will exist in the test context - 'studentSchema' => [ - 'entityClass' => _stubStudent::class, - 'fields' => [ - '_id' => 'objectId', - 'name' => 'string', - 'age' => 'integer', - 'tests' => 'schema.TestSchema', - 'finalGrade' => 'float', - ], - ], - 'TestSchema' => [ - 'entityClass' => _stubTestGrade::class, - 'fields' => [ - '_id' => 'objectId', - 'subject' => 'string', - 'grade' => 'float', - ], - ], - ], - 'inputSchema' => 'studentSchema', // Schema that will be used to assembly $inputValue - 'expectedOutput' => new _stubStudent([ // Expected output - '_id' => new ObjectID('507f1f77bcf86cd799439011'), - 'name' => 'John Doe', - 'age' => 25, - 'tests' => null, - 'finalGrade' => 7.25, - ]), - ], - - //--------------------------- - - 'A stdClass with a schema containing an embeded schema with a document directly into the field' => [ - 'inputValue' => (object) [ // Data that will be used to assembly the entity - '_id' => new ObjectID('507f1f77bcf86cd799439011'), - 'name' => 'John Doe', - 'age' => 25, - 'tests' => [ - '_id' => new ObjectID('507f1f77bcf86cd7994390ea'), - 'subject' => 'math', - 'grade' => 7.25, - ], - 'finalGrade' => 7.25, - ], - 'availableSchmas' => [ // Schemas that will exist in the test context - 'studentSchema' => [ - 'entityClass' => _stubStudent::class, - 'fields' => [ - '_id' => 'objectId', - 'name' => 'string', - 'age' => 'integer', - 'tests' => 'schema.TestSchema', - 'finalGrade' => 'float', - ], - ], - 'TestSchema' => [ - 'entityClass' => _stubTestGrade::class, - 'fields' => [ - '_id' => 'objectId', - 'subject' => 'string', - 'grade' => 'float', - ], - ], - ], - 'inputSchema' => 'studentSchema', // Schema that will be used to assembly $inputValue - 'expectedOutput' => new _stubStudent([ // Expected output - '_id' => new ObjectID('507f1f77bcf86cd799439011'), - 'name' => 'John Doe', - 'age' => 25, - 'tests' => [ - new _stubTestGrade([ - '_id' => new ObjectID('507f1f77bcf86cd7994390ea'), - 'subject' => 'math', - 'grade' => 7.25, - ]), - ], - 'finalGrade' => 7.25, - ]), - ], - - //--------------------------- - - 'A schema containing an embeded schema with multiple documents in the field' => [ - 'inputValue' => [ // Data that will be used to assembly the entity - '_id' => new ObjectID('507f1f77bcf86cd799439011'), - 'name' => 'John Doe', - 'age' => 25, - 'tests' => [ - [ - '_id' => new ObjectID('507f1f77bcf86cd7994390ea'), - 'subject' => 'math', - 'grade' => 7.25, - ], - [ - '_id' => new ObjectID('507f1f77bcf86cd7994390eb'), - 'subject' => 'english', - 'grade' => 9.0, - ], - ], - 'finalGrade' => 7.25, - ], - 'availableSchmas' => [ // Schemas that will exist in the test context - 'studentSchema' => [ - 'entityClass' => _stubStudent::class, - 'fields' => [ - '_id' => 'objectId', - 'name' => 'string', - 'age' => 'integer', - 'tests' => 'schema.TestSchema', - 'finalGrade' => 'float', - ], - ], - 'TestSchema' => [ - 'entityClass' => _stubTestGrade::class, - 'fields' => [ - '_id' => 'objectId', - 'subject' => 'string', - 'grade' => 'float', - ], - ], - ], - 'inputSchema' => 'studentSchema', // Schema that will be used to assembly $inputValue - 'expectedOutput' => new _stubStudent([ // Expected output - '_id' => new ObjectID('507f1f77bcf86cd799439011'), - 'name' => 'John Doe', - 'age' => 25, - 'tests' => [ - new _stubTestGrade([ - '_id' => new ObjectID('507f1f77bcf86cd7994390ea'), - 'subject' => 'math', - 'grade' => 7.25, - ]), - new _stubTestGrade([ - '_id' => new ObjectID('507f1f77bcf86cd7994390eb'), - 'subject' => 'english', - 'grade' => 9.0, - ]), - ], - 'finalGrade' => 7.25, - ]), - ], - - //--------------------------- - - 'A simple schema with a polymorphable interface' => [ - 'inputValue' => [ // Data that will be used to assembly the entity - '_id' => new ObjectID('507f1f77bcf86cd799439011'), - 'name' => 'John Doe', - 'age' => 25, - 'grade' => 7.25, - ], - 'availableSchmas' => [ // Schemas that will exist in the test context - 'studentSchema' => [ - 'entityClass' => _polymorphableStudent::class, - 'fields' => [ - '_id' => 'objectId', - 'name' => 'string', - 'age' => 'integer', - 'grade' => 'float', - 'finalGrade' => 'float', - ], - ], - ], - 'inputSchema' => 'studentSchema', // Schema that will be used to assembly $inputValue - 'expectedOutput' => new _stubStudent([ // Expected output - '_id' => new ObjectID('507f1f77bcf86cd799439011'), - 'name' => 'John Doe', - 'age' => 25, - 'grade' => 7.25, - ]), - ], - ]; - } -} - -class _stubStudent extends \stdClass implements AttributesAccessInterface -{ - use Attributes; - - public function __construct($attr = []) - { - foreach ($attr as $key => $value) { - $this->$key = $value; - } - - $this->original = $this->attributes; - } -} - -class _stubTestGrade extends \stdClass -{ - public function __construct($attr = []) - { - foreach ($attr as $key => $value) { - $this->$key = $value; - } - } -} - -class _polymorphableStudent extends \stdClass implements PolymorphableInterface -{ - public function __construct($attr = []) - { - foreach ($attr as $key => $value) { - $this->$key = $value; - } - } - - public function polymorph() - { - return new _stubStudent((array) $this); - } -} diff --git a/tests/Mongolid/DataMapper/SchemaMapperTest.php b/tests/Mongolid/DataMapper/SchemaMapperTest.php deleted file mode 100644 index 5dd571af..00000000 --- a/tests/Mongolid/DataMapper/SchemaMapperTest.php +++ /dev/null @@ -1,261 +0,0 @@ -fields = [ - 'name' => 'string', - 'age' => 'int', - 'stuff' => 'schema.My\Own\Schema', - ]; - $schemaMapper = m::mock( - SchemaMapper::class.'[clearDynamic,parseField]', - [$schema] - ); - $schemaMapper->shouldAllowMockingProtectedMethods(); - $data = [ - 'name' => 'John', - 'age' => 23, - 'stuff' => 'fooBar', - ]; - - // Act - $schemaMapper->shouldReceive('clearDynamic') - ->once() - ->with($data); - - foreach ($schema->fields as $key => $value) { - $schemaMapper->shouldReceive('parseField') - ->once() - ->with($data[$key], $value) - ->andReturn($data[$key].'.PARSED'); - } - - // Assert - $this->assertEquals( - [ - 'name' => 'John.PARSED', - 'age' => '23.PARSED', - 'stuff' => 'fooBar.PARSED', - ], - $schemaMapper->map($data) - ); - } - - public function testShouldClearDynamicFieldsIfSchemaIsNotDynamic() - { - // Arrange - $schema = m::mock(Schema::class); - $schema->dynamic = false; - $schema->fields = [ - 'name' => 'string', - 'age' => 'int', - ]; - $schemaMapper = new SchemaMapper($schema); - $data = [ - 'name' => 'John', - 'age' => 23, - 'location' => 'Brazil', - ]; - - // Assert - $this->callProtected($schemaMapper, 'clearDynamic', [&$data]); - $this->assertEquals( - [ - 'name' => 'John', - 'age' => 23, - ], - $data - ); - } - - public function testShouldNotClearDynamicFieldsIfSchemaIsDynamic() - { - // Arrange - $schema = m::mock(Schema::class); - $schema->dynamic = true; - $schema->fields = [ - 'name' => 'string', - 'age' => 'int', - ]; - $schemaMapper = new SchemaMapper($schema); - $data = [ - 'name' => 'John', - 'age' => 23, - 'location' => 'Brazil', - ]; - - // Assert - $this->callProtected($schemaMapper, 'clearDynamic', [&$data]); - $this->assertEquals( - [ - 'name' => 'John', - 'age' => 23, - 'location' => 'Brazil', - ], - $data - ); - } - - public function testShouldParseFieldIntoCastableType() - { - // Arrange - $schema = m::mock(Schema::class); - $schemaMapper = new SchemaMapper($schema); - - // Assert - $this->assertSame( - 23, - $schemaMapper->parseField('23', 'int') - ); - - $this->assertSame( - '1234', - $schemaMapper->parseField(1234, 'string') - ); - } - - public function testShouldParseFieldIntoAnotherMappedSchemaIfTypeBeginsWithSchema() - { - // Arrange - $schema = m::mock(Schema::class); - $schemaMapper = m::mock( - SchemaMapper::class.'[mapToSchema]', - [$schema] - ); - $schemaMapper->shouldAllowMockingProtectedMethods(); - - // Act - $schemaMapper->shouldReceive('mapToSchema') - ->once() - ->with(['foo' => 'bar'], 'FooBarSchema') - ->andReturn(['foo' => 123]); - - // Assert - $this->assertSame( - ['foo' => 123], - $schemaMapper->parseField(['foo' => 'bar'], 'schema.FooBarSchema') - ); - } - - public function testShouldParseFieldUsingAMethodInSchemaIfTypeIsAnUnknownString() - { - // Arrange - $schemaClass = new class() extends Schema { - public function pumpkinPoint($value) - { - return $value * 2; - } - }; - - $schema = new $schemaClass(); - $schemaMapper = new SchemaMapper($schema); - - // Assert - $this->assertSame( - 6, - $schemaMapper->parseField(3, 'pumpkinPoint') - ); - } - - public function testShouldMapAnArrayValueToAnotherSchema() - { - // Arrange - $schema = m::mock(Schema::class); - $mySchema = m::mock(Schema::class); - $schemaMapper = new SchemaMapper($schema); - $value = ['foo' => 'bar']; - $test = $this; - - // Act - Ioc::instance('Xd\MySchema', $mySchema); - - // When instantiating the SchemaMapper with the specified $param as dependency - Ioc::bind(SchemaMapper::class, function ($container, $params) use ($value, $mySchema, $test) { - // Check if mySchema has been injected correctly - $test->assertSame($mySchema, $params['schema']); - - // Instantiate a SchemaMapper with mySchema - $anotherSchemaMapper = m::mock(SchemaMapper::class, [$params['schema']]); - - // Set expectation to receive a map call - $anotherSchemaMapper->shouldReceive('map') - ->once() - ->with($value) - ->andReturn(['foo' => 'PARSED']); - - return $anotherSchemaMapper; - }); - - // Assert - $this->assertEquals( - [ - ['foo' => 'PARSED'], - ], - $this->callProtected($schemaMapper, 'mapToSchema', [$value, 'Xd\MySchema']) - ); - } - - public function testShouldParseToArrayGettingObjectAttributes() - { - // Arrange - $schema = m::mock(Schema::class); - $schemaMapper = new SchemaMapper($schema); - $object = (object) ['foo' => 'bar', 'name' => 'wilson']; - - // Assert - $this->assertEquals( - ['foo' => 'bar', 'name' => 'wilson'], - $this->callProtected($schemaMapper, 'parseToArray', [$object]) - ); - } - - public function testShouldParseToArrayIfIsAnArray() - { - // Arrange - $schema = m::mock(Schema::class); - $schemaMapper = new SchemaMapper($schema); - $object = ['age' => 25]; - - // Assert - $this->assertEquals( - $object, - $this->callProtected($schemaMapper, 'parseToArray', [$object]) - ); - } - - public function testShouldGetAttributesWhenGetAttributesMethodIsAvailable() - { - // Arrange - $schema = m::mock(Schema::class); - $schemaMapper = new SchemaMapper($schema); - $object = new class() { - public function getAttributes() - { - return ['foo' => 'bar']; - } - }; - - // Assert - $this->assertEquals( - ['foo' => 'bar'], - $this->callProtected($schemaMapper, 'parseToArray', [$object]) - ); - } -} diff --git a/tests/Mongolid/DynamicSchemaTest.php b/tests/Mongolid/DynamicSchemaTest.php deleted file mode 100644 index bd3cc9ac..00000000 --- a/tests/Mongolid/DynamicSchemaTest.php +++ /dev/null @@ -1,35 +0,0 @@ -assertInstanceOf(Schema::class, $schema); - } - - public function testShouldBeDynamic() - { - // Arrange - $schema = m::mock(DynamicSchema::class.'[]'); - - // Assert - $this->assertAttributeEquals(true, 'dynamic', $schema); - } -} diff --git a/tests/Mongolid/ManagerTest.php b/tests/Mongolid/ManagerTest.php deleted file mode 100644 index 600ceda3..00000000 --- a/tests/Mongolid/ManagerTest.php +++ /dev/null @@ -1,128 +0,0 @@ -setProtected(Manager::class, 'singleton', null); - } - - public function testShouldAddAndGetConnection() - { - // Arrange - $manager = new Manager(); - $connection = m::mock(Connection::class); - $rawConnection = m::mock(Client::class); - - // Act - $connection->shouldReceive('getRawConnection') - ->andReturn($rawConnection); - - // Assert - $manager->addConnection($connection); - $this->assertEquals($rawConnection, $manager->getConnection()); - } - - public function testShouldSetEventTrigger() - { - // Arrange - $test = $this; - $manager = new Manager(); - $container = m::mock(Container::class); - $eventTrigger = m::mock(EventTriggerInterface::class); - - $this->setProtected($manager, 'container', $container); - - // Act - $container->shouldReceive('instance') - ->once() - ->andReturnUsing(function ($class, $eventService) use ($test, $eventTrigger) { - $test->assertEquals(EventTriggerService::class, $class); - $test->assertAttributeEquals($eventTrigger, 'dispatcher', $eventService); - }); - - // Assert - $manager->setEventTrigger($eventTrigger); - } - - public function testShouldRegisterSchema() - { - // Arrange - $manager = new Manager(); - $schema = m::mock(Schema::class); - $schema->entityClass = 'Bacon'; - - // Assert - $manager->registerSchema($schema); - $this->assertAttributeEquals( - ['Bacon' => $schema], - 'schemas', - $manager - ); - } - - public function testShouldGetDataMapperForEntitiesWithRegisteredSchemas() - { - // Arrange - $manager = new Manager(); - $schema = m::mock(Schema::class); - $dataMapper = m::mock(DataMapper::class)->makePartial(); - - $schema->entityClass = 'Bacon'; - - // Act - Ioc::instance(DataMapper::class, $dataMapper); - - // Assert - $manager->registerSchema($schema); - $result = $manager->getMapper('Bacon'); - - $this->assertEquals($dataMapper, $result); - $this->assertAttributeEquals($schema, 'schema', $result); - } - - public function testShouldNotGetDataMapperForUnknownEntities() - { - // Arrange - $manager = new Manager(); - - // Assert - $result = $manager->getMapper('Unknow'); - $this->assertNull($result); - } - - public function testShouldInitializeOnce() - { - // Arrange - $manager = new Manager(); - $this->callProtected($manager, 'init'); - - // Assertion - $this->assertAttributeEquals($manager, 'singleton', Manager::class); - $this->assertAttributeInstanceOf(Container::class, 'container', $manager); - $this->assertAttributeInstanceOf(Pool::class, 'connectionPool', $manager); - $this->assertAttributeInstanceOf(CacheComponentInterface::class, 'cacheComponent', $manager); - - $container = $manager->container; - $this->callProtected($manager, 'init'); - // Initializes again to make sure that it will not instantiate a new container - $this->assertAttributeEquals($container, 'container', $manager); - } -} diff --git a/tests/Mongolid/Model/AttributesTest.php b/tests/Mongolid/Model/AttributesTest.php deleted file mode 100644 index 2acc1998..00000000 --- a/tests/Mongolid/Model/AttributesTest.php +++ /dev/null @@ -1,334 +0,0 @@ -name = 'John'; - $model->age = 25; - $model->child = $childObj; - $this->assertAttributeEquals( - [ - 'name' => 'John', - 'age' => 25, - 'child' => $childObj, - ], - 'attributes', - $model - ); - } - - public function testShouldHaveDynamicGetters() - { - // Arrange - $model = new class() { - use Attributes; - }; - - $childObj = new stdClass(); - $this->setProtected( - $model, - 'attributes', - [ - 'name' => 'John', - 'age' => 25, - 'child' => $childObj, - ] - ); - - // Assert - $this->assertEquals('John', $model->name); - $this->assertEquals(25, $model->age); - $this->assertEquals($childObj, $model->child); - $this->assertEquals(null, $model->nonexistant); - } - - public function testShouldCheckIfAttributeIsSet() - { - // Arrange - $model = new class() { - use Attributes; - }; - - $this->setProtected( - $model, - 'attributes', - ['name' => 'John'] - ); - - // Assert - $this->assertTrue(isset($model->name)); - $this->assertFalse(isset($model->nonexistant)); - } - - public function testShouldCheckIfMutatedAttributeIsSet() - { - // Arrange - $model = new class() { - use Attributes; - - public function getNameAttribute() - { - return 'John'; - } - }; - - /* Enable mutator methods */ - $model->mutable = true; - - // Assert - $this->assertTrue(isset($model->name)); - $this->assertFalse(isset($model->nonexistant)); - } - - public function testShouldUnsetAttributes() - { - // Arrange - $model = new class() { - use Attributes; - }; - - $this->setProtected( - $model, - 'attributes', - [ - 'name' => 'John', - 'age' => 25, - ] - ); - - // Assert - unset($model->age); - $this->assertAttributeEquals( - [ - 'name' => 'John', - ], - 'attributes', - $model - ); - } - - public function testShouldGetAttributeFromMutator() - { - // Arrange - $model = new class() { - use Attributes; - - public function getSomeAttribute() - { - return 'something-else'; - } - }; - - /* Enable mutator methods */ - $model->mutable = true; - $model->some = 'some-value'; - - // Assert - $this->assertEquals('something-else', $model->some); - } - - public function testShouldIgnoreMutators() - { - // Arrange - $model = new class() { - use Attributes; - - public function getSomeAttribute() - { - return 'something-else'; - } - - public function setSomeAttribute($value) - { - return strtoupper($value); - } - }; - - /* Disable mutator methods */ - $model->mutable = false; - $model->some = 'some-value'; - - // Assert - $this->assertEquals('some-value', $model->some); - } - - public function testShouldSetAttributeFromMutator() - { - // Arrange - $model = new class() { - use Attributes; - - public function setSomeAttribute($value) - { - return strtoupper($value); - } - }; - - /* Enable mutator methods */ - $model->mutable = true; - $model->some = 'some-value'; - - // Assert - $this->assertEquals('SOME-VALUE', $model->some); - } - - /** - * @dataProvider getFillableOptions - */ - public function testShouldFillOnlyPermittedAttributes( - $fillable, - $guarded, - $input, - $expected - ) { - // Arrange - $model = new class() { - use Attributes; - }; - - $this->setProtected($model, 'fillable', $fillable); - $this->setProtected($model, 'guarded', $guarded); - - // Assert - $model->fill($input); - $this->assertAttributeEquals($expected, 'attributes', $model); - } - - public function testShouldForceFillAttributes() - { - // Arrange - $model = new class() { - use Attributes; - }; - - $input = [ - 'name' => 'Josh', - 'notAllowedAttribute' => true, - ]; - - // Act - $model->fill($input, true); - - // Assert - $this->assertTrue($model->notAllowedAttribute); - } - - public function testShouldBeCastableToArray() - { - // Arrange - $model = new class() { - use Attributes; - }; - - $model->name = 'John'; - $model->age = 25; - - // Assert - $this->assertEquals( - ['name' => 'John', 'age' => 25], - $model->toArray() - ); - } - - public function testShouldSetOriginalAttributes() - { - // Arrange - $model = new class() implements AttributesAccessInterface { - use Attributes; - }; - - $model->name = 'John'; - $model->age = 25; - - // Act - $model->syncOriginalAttributes(); - - // Assert - $this->assertAttributeEquals($model->attributes, 'original', $model); - } - - public function getFillableOptions() - { - return [ - // ----------------------------- - '$fillable = []; $guarded = []' => [ - 'fillable' => [], - 'guarded' => [], - 'input' => [ - 'name' => 'John', - 'age' => 25, - 'sex' => 'male', - ], - 'expected' => [ - 'name' => 'John', - 'age' => 25, - 'sex' => 'male', - ], - ], - - // ----------------------------- - '$fillable = ["name"]; $guarded = []' => [ - 'fillable' => ['name'], - 'guarded' => [], - 'input' => [ - 'name' => 'John', - 'age' => 25, - 'sex' => 'male', - ], - 'expected' => [ - 'name' => 'John', - ], - ], - - // ----------------------------- - '$fillable = []; $guarded = []' => [ - 'fillable' => [], - 'guarded' => ['sex'], - 'input' => [ - 'name' => 'John', - 'age' => 25, - 'sex' => 'male', - ], - 'expected' => [ - 'name' => 'John', - 'age' => 25, - ], - ], - - // ----------------------------- - '$fillable = ["name", "sex"]; $guarded = ["sex"]' => [ - 'fillable' => ['name', 'sex'], - 'guarded' => ['sex'], - 'input' => [ - 'name' => 'John', - 'age' => 25, - 'sex' => 'male', - ], - 'expected' => [ - 'name' => 'John', - ], - ], - ]; - } -} diff --git a/tests/Mongolid/Model/DocumentEmbedderTest.php b/tests/Mongolid/Model/DocumentEmbedderTest.php deleted file mode 100644 index c7f1825c..00000000 --- a/tests/Mongolid/Model/DocumentEmbedderTest.php +++ /dev/null @@ -1,272 +0,0 @@ -foo = $originalField; - $embeder = new DocumentEmbedder(); - - // Assert - $embeder->$method($parent, 'foo', $entity); - - $result = $parent->foo; - foreach ($expectation as $index => $expectedDoc) { - if ($expectedDoc instanceof ObjectID) { - $this->assertEquals($expectedDoc, $result[$index]); - - continue; - } - - $expectedDocArray = (array) $expectedDoc; - $resultDocArray = (array) $result[$index]; - foreach ($expectedDocArray as $field => $value) { - if ($value instanceof Any) { - $this->assertTrue(isset($resultDocArray[$field])); - } else { - $this->assertEquals($value, $resultDocArray[$field]); - } - } - } - } - - public function getEmbedOptions() - { - return [ - // ------------------------------ - 'embedding array without _id' => [ - 'originalField' => null, - 'entity' => [ - 'name' => 'John Doe', - ], - 'method' => 'embed', - 'expectation' => [ - ['_id' => m::any(), 'name' => 'John Doe'], - ], - ], - - // ------------------------------ - 'embedding array with _id' => [ - 'originalField' => [], - 'entity' => [ - '_id' => (new ObjectID('507f191e810c19729de860ea')), - 'name' => 'John Doe', - ], - 'method' => 'embed', - 'expectation' => [ - ['_id' => (new ObjectID('507f191e810c19729de860ea')), 'name' => 'John Doe'], - ], - ], - - // ------------------------------ - 'embedding object without _id' => [ - 'originalField' => null, - 'entity' => (object) [ - 'name' => 'John Doe', - ], - 'method' => 'embed', - 'expectation' => [ - (object) ['_id' => m::any(), 'name' => 'John Doe'], - ], - ], - - // ------------------------------ - 'embedding object with _id' => [ - 'originalField' => null, - 'entity' => (object) [ - '_id' => (new ObjectID('507f191e810c19729de860ea')), - 'name' => 'John Doe', - ], - 'method' => 'embed', - 'expectation' => [ - (object) ['_id' => (new ObjectID('507f191e810c19729de860ea')), 'name' => 'John Doe'], - ], - ], - - // ------------------------------ - 'updating embedded object with _id' => [ - 'originalField' => [ - [ - '_id' => (new ObjectID('507f191e810c19729de860ea')), - 'name' => 'Bob', - ], - ], - 'entity' => (object) [ - '_id' => (new ObjectID('507f191e810c19729de860ea')), - 'name' => 'John Doe', - ], - 'method' => 'embed', - 'expectation' => [ - (object) ['_id' => (new ObjectID('507f191e810c19729de860ea')), 'name' => 'John Doe'], - ], - ], - - // ------------------------------ - 'updating embedded array with _id' => [ - 'originalField' => [ - [ - '_id' => (new ObjectID()), - 'name' => 'Louis', - ], - [ - '_id' => (new ObjectID('507f191e810c19729de860ea')), - 'name' => 'Bob', - ], - ], - 'entity' => [ - '_id' => (new ObjectID('507f191e810c19729de860ea')), - 'name' => 'John Doe', - ], - 'method' => 'embed', - 'expectation' => [ - [ - '_id' => m::any(), - 'name' => 'Louis', - ], - [ - '_id' => (new ObjectID('507f191e810c19729de860ea')), - 'name' => 'John Doe', - ], - ], - ], - - // ------------------------------ - 'unembeding array with _id' => [ - 'originalField' => [ - [ - '_id' => (new ObjectID('507f191e810c19729de860ea')), - 'name' => 'John Doe', - ], - [ - '_id' => (new ObjectID()), - 'name' => 'Louis', - ], - ], - 'entity' => [ - '_id' => (new ObjectID('507f191e810c19729de860ea')), - 'name' => 'John Doe', - ], - 'method' => 'unembed', - 'expectation' => [ - [ - '_id' => m::any(), - 'name' => 'Louis', - ], - ], - ], - - // ------------------------------ - 'attaching array with _id' => [ - 'originalField' => null, - 'entity' => [ - '_id' => (new ObjectID('507f191e810c19729de860ea')), - 'name' => 'John Doe', - ], - 'method' => 'attach', - 'expectation' => [ - (new ObjectID('507f191e810c19729de860ea')), - ], - ], - - // ------------------------------ - 'attaching object with _id' => [ - 'originalField' => null, - 'entity' => (object) [ - '_id' => (new ObjectID('507f191e810c19729de860ea')), - 'name' => 'John Doe', - ], - 'method' => 'attach', - 'expectation' => [ - (new ObjectID('507f191e810c19729de860ea')), - ], - ], - - // ------------------------------ - 'attaching object with _id that is already attached' => [ - 'originalField' => [ - (new ObjectID('507f191e810c19729de860ea')), - (new ObjectID('507f191e810c19729de86011')), - ], - 'entity' => (object) [ - '_id' => (new ObjectID('507f191e810c19729de860ea')), - 'name' => 'John Doe', - ], - 'method' => 'attach', - 'expectation' => [ - (new ObjectID('507f191e810c19729de860ea')), - (new ObjectID('507f191e810c19729de86011')), - ], - ], - - // ------------------------------ - 'attaching object without _id' => [ - 'originalField' => null, - 'entity' => (object) [ - 'name' => 'John Doe', - ], - 'method' => 'attach', - 'expectation' => [], - ], - - // ------------------------------ - 'detaching an object by its _id' => [ - 'originalField' => [ - (new ObjectID('507f191e810c19729de860ea')), - (new ObjectID('507f191e810c19729de86011')), - ], - 'entity' => (object) [ - '_id' => (new ObjectID('507f191e810c19729de860ea')), - 'name' => 'John Doe', - ], - 'method' => 'detach', - 'expectation' => [ - (new ObjectID('507f191e810c19729de86011')), - ], - ], - - // ------------------------------ - 'attaching an _id' => [ - 'originalField' => null, - 'entity' => new ObjectID('507f191e810c19729de860ea'), - 'method' => 'attach', - 'expectation' => [ - (new ObjectID('507f191e810c19729de860ea')), - ], - ], - - // ------------------------------ - 'detaching an object using only the _id when it is an integer' => [ - 'originalField' => [ - 6, - 7, - ], - 'entity' => 6, - 'method' => 'detach', - 'expectation' => [ - 7, - ], - ], - - // ------------------------------ - ]; - } -} diff --git a/tests/Mongolid/Model/RelationsTest.php b/tests/Mongolid/Model/RelationsTest.php deleted file mode 100644 index 4e81c798..00000000 --- a/tests/Mongolid/Model/RelationsTest.php +++ /dev/null @@ -1,327 +0,0 @@ -makePartial(); - $result = m::mock(); - - $model->$field = $fieldValue; - - // Act - Ioc::instance(DataMapper::class, $dataMapper); - Ioc::instance('EntityClass', $entity); - - $dataMapper->shouldReceive('first') - ->with(m::type('array'), [], $useCache) - ->once() - ->andReturnUsing(function ($query) use ($result, $expectedQuery) { - $this->assertMongoQueryEquals($expectedQuery, $query); - - return $result; - }); - - // Assert - $this->assertSame( - $result, - $this->callProtected($model, 'referencesOne', ['EntityClass', $field, $useCache]) - ); - } - - /** - * @dataProvider referenceScenarios - */ - public function testShouldReferenceMany($entity, $field, $fieldValue, $useCache, $expectedQuery) - { - // Set - $expectedQuery = $expectedQuery['referencesMany']; - $model = m::mock(ActiveRecord::class.'[]'); - $dataMapper = m::mock(DataMapper::class)->makePartial(); - $result = m::mock(Cursor::class); - - $model->$field = $fieldValue; - - // Act - Ioc::instance(DataMapper::class, $dataMapper); - Ioc::instance('EntityClass', $entity); - - $dataMapper->shouldReceive('where') - ->with(m::type('array'), [], $useCache) - ->once() - ->andReturnUsing(function ($query) use ($result, $expectedQuery) { - $this->assertMongoQueryEquals($expectedQuery, $query); - - return $result; - }); - - // Assert - $this->assertSame( - $result, - $this->callProtected($model, 'referencesMany', ['EntityClass', $field, $useCache]) - ); - } - - /** - * @dataProvider embedsScenarios - */ - public function testShouldEmbedsOne($entity, $field, $fieldValue, $expectedItems) - { - // Set - $model = m::mock(ActiveRecord::class.'[]'); - $cursorFactory = m::mock(CursorFactory::class); - $cursor = m::mock(EmbeddedCursor::class); - $document = $fieldValue; - $model->$field = $document; - - $instantiableClass = $entity instanceof Schema ? 'stdClass' : get_class($entity); - - // Act - Ioc::instance(CursorFactory::class, $cursorFactory); - - $cursorFactory->shouldReceive('createEmbeddedCursor') - ->once() - ->with($instantiableClass, $expectedItems) - ->andReturn($cursor); - - $cursor->shouldReceive('first') - ->once() - ->andReturn(new $instantiableClass()); - - // Assert - $result = $this->callProtected($model, 'embedsOne', [get_class($entity), $field]); - $this->assertInstanceOf($instantiableClass, $result); - } - - /** - * @dataProvider embedsScenarios - */ - public function testShouldEmbedsMany($entity, $field, $fieldValue, $expectedItems) - { - // Set - $model = m::mock(ActiveRecord::class.'[]'); - $cursorFactory = m::mock(CursorFactory::class); - $cursor = m::mock(EmbeddedCursor::class); - $document = $fieldValue; - $model->$field = $document; - - $instantiableClass = $entity instanceof Schema ? 'stdClass' : get_class($entity); - - // Act - Ioc::instance(CursorFactory::class, $cursorFactory); - - $cursorFactory->shouldReceive('createEmbeddedCursor') - ->once() - ->with($instantiableClass, $expectedItems) - ->andReturn($cursor); - - // Assert - $result = $this->callProtected($model, 'embedsMany', [get_class($entity), $field]); - $this->assertEquals($cursor, $result); - } - - /** - * @dataProvider manipulativeMethods - */ - public function testShouldEmbeddedUnembedAttachAndDetachDocuments($method) - { - // Set - $model = new class() { - use Relations; - }; - $document = m::mock(); - $documentEmbedder = m::mock(DocumentEmbedder::class); - - // Act - Ioc::instance(DocumentEmbedder::class, $documentEmbedder); - - $documentEmbedder->shouldReceive($method) - ->once() - ->with($model, 'foo', $document); - - // Assert - $model->$method('foo', $document); - } - - public function referenceScenarios() - { - return [ - // ------------------------- - 'Schema referenced by numeric id' => [ - 'entity' => new class() extends Schema { - }, - 'field' => 'foo', - 'fieldValue' => 12345, - 'useCache' => true, - 'expectedQuery' => [ - 'referencesOne' => ['_id' => 12345], - 'referencesMany' => ['_id' => ['$in' => [12345]]], - ], - ], - // ------------------------- - 'ActiveRecord referenced by string id' => [ - 'entity' => new class() extends ActiveRecord { - protected $collection = 'foobar'; - }, - 'field' => 'foo', - 'fieldValue' => 'abc123', - 'useCache' => true, - 'expectedQuery' => [ - 'referencesOne' => ['_id' => 'abc123'], - 'referencesMany' => ['_id' => ['$in' => ['abc123']]], - ], - ], - // ------------------------- - 'Schema referenced by string objectId' => [ - 'entity' => new class() extends Schema { - }, - 'field' => 'foo', - 'fieldValue' => ['553e3c80293fce6572ff2a40', '5571df31cf3fce544481a085'], - 'useCache' => false, - 'expectedQuery' => [ - 'referencesOne' => ['_id' => '553e3c80293fce6572ff2a40'], - 'referencesMany' => ['_id' => ['$in' => [new ObjectID('553e3c80293fce6572ff2a40'), new ObjectID('5571df31cf3fce544481a085')]]], - ], - ], - // ------------------------- - 'ActiveRecord referenced by objectId' => [ - 'entity' => new class() extends ActiveRecord { - protected $collection = 'foobar'; - }, - 'field' => 'foo', - 'fieldValue' => '577afb0b4d3cec136058fa82', - 'useCache' => true, - 'expectedQuery' => [ - 'referencesOne' => ['_id' => '577afb0b4d3cec136058fa82'], - 'referencesMany' => ['_id' => ['$in' => ['577afb0b4d3cec136058fa82']]], - ], - ], - // ------------------------- - 'Schema referenced with series of numeric ids' => [ - 'entity' => new class() extends Schema { - }, - 'field' => 'foo', - 'fieldValue' => [1, 2, 3, 4, 5], - 'useCache' => false, - 'expectedQuery' => [ - 'referencesOne' => ['_id' => 1], - 'referencesMany' => ['_id' => ['$in' => [1, 2, 3, 4, 5]]], - ], - ], - // ------------------------- - 'ActiveRecord referenced with series of string objectIds' => [ - 'entity' => new class() extends ActiveRecord { - protected $collection = 'foobar'; - }, - 'field' => 'foo', - 'fieldValue' => ['577afb0b4d3cec136058fa82', '577afb7e4d3cec136258fa83'], - 'useCache' => false, - 'expectedQuery' => [ - 'referencesOne' => ['_id' => '577afb0b4d3cec136058fa82'], - 'referencesMany' => ['_id' => ['$in' => [new ObjectID('577afb0b4d3cec136058fa82'), new ObjectID('577afb7e4d3cec136258fa83')]]], - ], - ], - // ------------------------- - 'Schema referenced with series of real objectIds' => [ - 'entity' => new class() extends Schema { - }, - 'field' => 'foo', - 'fieldValue' => [new ObjectID('577afb0b4d3cec136058fa82'), new ObjectID('577afb7e4d3cec136258fa83')], - 'useCache' => true, - 'expectedQuery' => [ - 'referencesOne' => ['_id' => new ObjectID('577afb0b4d3cec136058fa82')], - 'referencesMany' => ['_id' => ['$in' => [new ObjectID('577afb0b4d3cec136058fa82'), new ObjectID('577afb7e4d3cec136258fa83')]]], - ], - ], - // ------------------------- - 'ActiveRecord referenced with null' => [ - 'entity' => new class() extends ActiveRecord { - protected $collection = 'foobar'; - }, - 'field' => 'foo', - 'fieldValue' => null, - 'useCache' => false, - 'expectedQuery' => [ - 'referencesOne' => ['_id' => null], - 'referencesMany' => ['_id' => ['$in' => []]], - ], - ], - ]; - } - - public function embedsScenarios() - { - return [ - // ------------------------- - 'Embedded document referent to an Schema' => [ - 'entity' => new class() extends Schema { - }, - 'field' => 'foo', - 'fieldValue' => ['_id' => 12345, 'name' => 'batata'], - 'expectedItems' => [['_id' => 12345, 'name' => 'batata']], - ], - // ------------------------- - 'Embedded documents referent to an Schema' => [ - 'entity' => new class() extends Schema { - }, - 'field' => 'foo', - 'fieldValue' => [['_id' => 12345, 'name' => 'batata'], ['_id' => 67890, 'name' => 'bar']], - 'expectedItems' => [['_id' => 12345, 'name' => 'batata'], ['_id' => 67890, 'name' => 'bar']], - ], - // ------------------------- - 'Embedded document referent to an ActiveRecord entity' => [ - 'entity' => new class() extends ActiveRecord { - protected $collection = 'foobar'; - }, - 'field' => 'foo', - 'fieldValue' => ['_id' => 12345, 'name' => 'batata'], - 'expectedItems' => [['_id' => 12345, 'name' => 'batata']], - ], - // ------------------------- - 'Embedded documents referent to an ActiveRecord entity' => [ - 'entity' => new class() extends ActiveRecord { - protected $collection = 'foobar'; - }, - 'field' => 'foo', - 'fieldValue' => [['_id' => 12345, 'name' => 'batata'], ['_id' => 67890, 'name' => 'bar']], - 'expectedItems' => [['_id' => 12345, 'name' => 'batata'], ['_id' => 67890, 'name' => 'bar']], - ], - // ------------------------- - ]; - } - - public function manipulativeMethods() - { - return [ - ['embed'], - ['unembed'], - ['attach'], - ['detach'], - ]; - } -} diff --git a/tests/Mongolid/SchemaTest.php b/tests/Mongolid/SchemaTest.php deleted file mode 100644 index addbdfb0..00000000 --- a/tests/Mongolid/SchemaTest.php +++ /dev/null @@ -1,187 +0,0 @@ -assertAttributeEquals(false, 'dynamic', $schema); - } - - public function testMustHaveAnEntityClass() - { - // Arrange - $schema = m::mock(Schema::class.'[]'); - - // Assert - $this->assertAttributeEquals('stdClass', 'entityClass', $schema); - } - - public function testShouldCastNullIntoObjectId() - { - // Arrange - $schema = m::mock(Schema::class.'[]'); - $value = null; - - // Assert - $this->assertInstanceOf( - ObjectID::class, - $schema->objectId($value) - ); - } - - public function testShouldNotCastRandomStringIntoObjectId() - { - // Arrange - $schema = m::mock(Schema::class.'[]'); - $value = 'A random string'; - - // Assert - $this->assertEquals( - $value, - $schema->objectId($value) - ); - } - - public function testShouldCastObjectIdStringIntoObjectId() - { - // Arrange - $schema = m::mock(Schema::class.'[]'); - $value = '507f1f77bcf86cd799439011'; - - // Assert - $this->assertInstanceOf( - ObjectID::class, - $schema->objectId($value) - ); - - $this->assertEquals( - $value, - (string) $schema->objectId($value) - ); - } - - public function testShouldCastNullIntoAutoIncrementSequence() - { - // Arrange - $schema = m::mock(Schema::class.'[]'); - $sequenceService = m::mock(SequenceService::class); - $value = null; - - $schema->collection = 'resources'; - - // Act - Ioc::instance(SequenceService::class, $sequenceService); - - $sequenceService->shouldReceive('getNextValue') - ->with('resources') - ->once() - ->andReturn(7); - - // Assertion - $this->assertEquals(7, $schema->sequence($value)); - } - - public function testShouldNotAutoIncrementSequenceIfValueIsNotNull() - { - $schema = m::mock(Schema::class.'[]'); - $sequenceService = m::mock(SequenceService::class); - $value = 3; - - $schema->collection = 'resources'; - - // Act - Ioc::instance(SequenceService::class, $sequenceService); - - $sequenceService->shouldReceive('getNextValue') - ->with('resources') - ->never() - ->andReturn(7); // Should never be returned - - // Assertion - $this->assertEquals(3, $schema->sequence($value)); - } - - public function testShouldCastDocumentTimestamps() - { - // Arrange - $schema = m::mock(Schema::class.'[]'); - $value = null; - - // Assertion - $this->assertInstanceOf( - UTCDateTime::class, - $schema->createdAtTimestamp($value) - ); - } - - public function testShouldRefreshUpdatedAtTimestamps() - { - // Arrange - $schema = m::mock(Schema::class.'[]'); - $value = (new UTCDateTime(25)); - - // Assertion - $result = $schema->updatedAtTimestamp($value); - $this->assertInstanceOf(UTCDateTime::class, $result); - $this->assertNotEquals(25000, (string) $result); - } - - /** - * @dataProvider createdAtTimestampsFixture - */ - public function testShouldNotRefreshCreatedAtTimestamps( - $value, - $expectation, - $compareTimestamp = true - ) { - // Arrange - $schema = m::mock(Schema::class.'[]'); - - // Assertion - $result = $schema->createdAtTimestamp($value); - $this->assertInstanceOf(get_class($expectation), $result); - if ($compareTimestamp) { - $this->assertEquals((string) $expectation, (string) $result); - } - } - - public function createdAtTimestampsFixture() - { - return [ - 'MongoDB driver UTCDateTime' => [ - 'value' => new UTCDateTime(25), - 'expectation' => new UTCDateTime(25), - ], - 'Empty field' => [ - 'value' => null, - 'expectation' => new UTCDateTime(), - 'compareTimestamp' => false, - ], - 'An string' => [ - 'value' => 'foobar', - 'expectation' => new UTCDateTime(), - 'compareTimestamp' => false, - ], - ]; - } -} diff --git a/tests/Mongolid/Util/CacheComponentTest.php b/tests/Mongolid/Util/CacheComponentTest.php deleted file mode 100644 index 1c8ee6eb..00000000 --- a/tests/Mongolid/Util/CacheComponentTest.php +++ /dev/null @@ -1,78 +0,0 @@ -assertInstanceOf(CacheComponentInterface::class, $cacheComponent); - } - - public function testShouldPutAndRetrieveValues() - { - // Arrange - $cacheComponent = $this->getCacheComponent(); - - // Assertion - $cacheComponent->put('saveThe', 'bacon', 1); // 1 minute of ttl - $this->tick(30); // After 30 seconds - $this->assertTrue($cacheComponent->has('saveThe')); - $this->assertEquals('bacon', $cacheComponent->get('saveThe')); - } - - public function testShouldExpireValues() - { - // Arrange - $cacheComponent = $this->getCacheComponent(); - $cacheComponent->put('saveThe', 'bacon', 1); // 1 minute of ttl - - // Act - $this->tick(61); // After 61 seconds - - // Assertion - $this->assertFalse($cacheComponent->has('saveThe')); - $this->assertNull($cacheComponent->get('saveThe')); - } - - protected function getCacheComponent() - { - $test = $this; - $cacheComponent = m::mock(CacheComponent::class.'[time]'); - $cacheComponent->shouldAllowMockingProtectedMethods(); - $cacheComponent->shouldReceive('time') - ->andReturnUsing(function () use ($test) { - return $test->time; - }); - - return $cacheComponent; - } - - /** - * Skips $seconds of time. - */ - protected function tick($seconds) - { - $this->time += $seconds; - } -} diff --git a/tests/Mongolid/Util/LocalDateTimeTest.php b/tests/Mongolid/Util/LocalDateTimeTest.php deleted file mode 100644 index 50e3482b..00000000 --- a/tests/Mongolid/Util/LocalDateTimeTest.php +++ /dev/null @@ -1,90 +0,0 @@ -date = new DateTime('01/05/2017 15:40:00'); - $this->date->setTimezone(new DateTimeZone('UTC')); - - date_default_timezone_set('America/Sao_Paulo'); - } - - /** - * {@inheritdoc} - */ - public function tearDown() - { - parent::tearDown(); - unset($this->date, $this->format); - } - - public function testGetShouldRetrievesDateUsingTimezone() - { - $this->assertEquals( - $this->date, - LocalDateTime::get(new UTCDateTime($this->date)) - ); - } - - public function testFormatShouldRetrievesDateWithDefaultFormat() - { - $this->date->setTimezone( - new DateTimeZone(date_default_timezone_get()) - ); - - $this->assertEquals( - $this->date->format($this->format), - LocalDateTime::format(new UTCDateTime($this->date)) - ); - } - - public function testFormatShouldRetrieesDateUsingGivenFormat() - { - $this->date->setTimezone( - new DateTimeZone(date_default_timezone_get()) - ); - - $format = 'Y-m-d H:i:s'; - - $this->assertEquals( - $this->date->format($format), - LocalDateTime::format(new UTCDateTime($this->date), $format) - ); - } - - public function testTimestampShouldRetrievesTimestampUsingTimezone() - { - $dateTimestamp = $this->date->getTimestamp(); - $mongoDateTimestamp = LocalDateTime::timestamp( - new UTCDateTime($this->date) - ); - - $this->assertEquals( - DateTime::createFromFormat($dateTimestamp, $this->format), - DateTime::createFromFormat($mongoDateTimestamp, $this->format) - ); - } -} diff --git a/tests/Mongolid/Util/ObjectIdUtilsTest.php b/tests/Mongolid/Util/ObjectIdUtilsTest.php deleted file mode 100644 index 19bd18fe..00000000 --- a/tests/Mongolid/Util/ObjectIdUtilsTest.php +++ /dev/null @@ -1,38 +0,0 @@ -assertEquals($expectation, ObjectIdUtils::isObjectId($value)); - } - - public function objectIdStringScenarios() - { - return [ - // [Value, Expectation], - ['577a68c44d3cec1f6c7796a2', true], - ['577a68d24d3cec1f817796a5', true], - ['577a68d14d3cec1f6d7796a3', true], - ['507f1f77bcf86cd799439011', true], - ['507f191e810c19729de860ea', true], - [new ObjectID(), true], - ['1', false], - ['507f191e810c197', false], - ['123456', false], - ['abcdefgh1234567890123456', false], - ['+07f191e810c19729de860ea', false], - [1234567, false], - [0.5, false], - [['key' => 'value'], false], - ]; - } -} diff --git a/tests/Mongolid/Util/SequenceServiceTest.php b/tests/Mongolid/Util/SequenceServiceTest.php deleted file mode 100644 index 6723f694..00000000 --- a/tests/Mongolid/Util/SequenceServiceTest.php +++ /dev/null @@ -1,100 +0,0 @@ -shouldAllowMockingProtectedMethods(); - $rawCollection = m::mock(Collection::class); - - // Act - $sequenceService->shouldReceive('rawCollection') - ->once() - ->andReturn($rawCollection); - - $rawCollection->shouldReceive('findOneAndUpdate') - ->once() - ->with( - ['_id' => $sequenceName], - ['$inc' => ['seq' => 1]], - ['upsert' => true] - )->andReturn( - $currentValue ? (object) ['seq' => $currentValue] : null - ); - - // Assertion - $this->assertEquals( - $expectation, - $sequenceService->getNextValue($sequenceName) - ); - } - - public function testShouldGetRawCollection() - { - // Arrange - $connPool = m::mock(Pool::class); - $sequenceService = new SequenceService($connPool, 'foobar'); - $connection = m::mock(Connection::class); - $collection = m::mock(Collection::class); - - $connection->defaultDatabase = 'grimory'; - $connection->grimory = (object) ['foobar' => $collection]; - - // Act - $connPool->shouldReceive('getConnection') - ->once() - ->andReturn($connection); - - $connection->shouldReceive('getRawConnection') - ->andReturn($connection); - - // Assertion - $this->assertEquals( - $collection, - $this->callProtected($sequenceService, 'rawCollection') - ); - } - - public function sequenceScenarios() - { - return [ - 'New sequence in collection "products"' => [ - 'sequenceName' => 'products', - 'currentValue' => 0, - 'expectation' => 1, - ], - // ----------------------- - 'Existing sequence in collection "unicorns"' => [ - 'sequenceName' => 'unicorns', - 'currentValue' => 7, - 'expectation' => 8, - ], - // ----------------------- - 'Existing sequence in collection "unicorns"' => [ - 'sequenceName' => 'unicorns', - 'currentValue' => 3, - 'expectation' => 4, - ], - ]; - } -} diff --git a/tests/Stubs/EmbeddedUser.php b/tests/Stubs/EmbeddedUser.php new file mode 100644 index 00000000..0d82cef4 --- /dev/null +++ b/tests/Stubs/EmbeddedUser.php @@ -0,0 +1,42 @@ +embedsOne(EmbeddedUser::class); + } + + public function siblings() + { + return $this->embedsMany(EmbeddedUser::class); + } + + public function son() + { + return $this->embedsOne(EmbeddedUser::class, 'arbitrary_field'); + } + + public function grandsons() + { + return $this->embedsMany(EmbeddedUser::class, 'other_arbitrary_field'); + } + + public function sameName() + { + $this->embedsOne(EmbeddedUser::class, 'sameName'); + } +} diff --git a/tests/Stubs/PolymorphedReferencedUser.php b/tests/Stubs/PolymorphedReferencedUser.php new file mode 100644 index 00000000..d6a0c8a0 --- /dev/null +++ b/tests/Stubs/PolymorphedReferencedUser.php @@ -0,0 +1,13 @@ +referencesOne(ReferencedUser::class); + } + + public function siblings() + { + return $this->referencesMany(ReferencedUser::class); + } + + public function son() + { + return $this->referencesOne(ReferencedUser::class, 'arbitrary_field', 'code'); + } + + public function grandsons() + { + return $this->referencesMany(ReferencedUser::class, null, 'code'); + } + + public function invalid() + { + return 'I am not a relation!'; + } + + /** + * {@inheritdoc} + */ + public function polymorph(array $input): string + { + if ('polymorphed' === ($input['type'] ?? '')) { + return PolymorphedReferencedUser::class; + } + + return static::class; + } +} diff --git a/tests/Stubs/ReplaceCollectionModel.php b/tests/Stubs/ReplaceCollectionModel.php new file mode 100644 index 00000000..c40ecf2a --- /dev/null +++ b/tests/Stubs/ReplaceCollectionModel.php @@ -0,0 +1,33 @@ +rawCollection = $collection; + } + + public function getCollection(): Collection + { + return $this->rawCollection; + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php deleted file mode 100644 index 212bdf3c..00000000 --- a/tests/TestCase.php +++ /dev/null @@ -1,110 +0,0 @@ -assertEquals($expectedQuery, $query, 'Queries are not equals'); - - if (!is_array($expectedQuery)) { - return; - } - - foreach ($expectedQuery as $key => $value) { - if (is_object($value)) { - $this->assertInstanceOf(get_class($value), $query[$key], 'Type of an object within the query is not equals'); - - if (method_exists($value, '__toString')) { - $this->assertEquals((string) $expectedQuery[$key], (string) $query[$key], 'Object within the query is not equals'); - } - } - - if (is_array($value)) { - $this->assertMongoQueryEquals($value, $query[$key]); - } - } - } - - /** - * Actually runs a protected method of the given object. - * - * @param $obj - * @param $method - * @param array $args - * - * @return mixed - */ - protected function callProtected($obj, $method, $args = []) - { - $methodObj = new ReflectionMethod(get_class($obj), $method); - $methodObj->setAccessible(true); - - if (is_object($args)) { - $args = [$args]; - } else { - $args = (array) $args; - } - - return $methodObj->invokeArgs($obj, $args); - } - - /** - * Set a protected property of an object. - * - * @param mixed $obj object Instance - * @param string $property property name - * @param mixed $value value to be set - */ - protected function setProtected($obj, $property, $value) - { - $class = new ReflectionClass($obj); - $property = $class->getProperty($property); - $property->setAccessible(true); - - if (is_string($obj)) { // static - $property->setValue($value); - - return; - } - - $property->setValue($obj, $value); - } - - /** - * Get a protected property of an object. - * - * @param mixed $obj object Instance - * @param string $property property name - * - * @return mixed property value - */ - protected function getProtected($obj, $property) - { - $class = new ReflectionClass($obj); - $property = $class->getProperty($property); - $property->setAccessible(true); - - if (is_string($obj)) { // static - return $property->getValue(); - } - - return $property->getValue($obj); - } -} diff --git a/tests/Unit/Connection/ConnectionTest.php b/tests/Unit/Connection/ConnectionTest.php new file mode 100644 index 00000000..b8e82bb2 --- /dev/null +++ b/tests/Unit/Connection/ConnectionTest.php @@ -0,0 +1,58 @@ +assertAttributeInstanceOf(Client::class, 'client', $connection); + $this->assertAttributeSame('my_db', 'defaultDatabase', $connection); + } + + public function testShouldDetermineDatabaseFromACluster() + { + // Set + $server = 'mongodb://my-server,other-server/my_db?replicaSet=someReplica'; + $options = ['some', 'uri', 'options']; + $driverOptions = ['some', 'driver', 'options']; + + // Actions + $connection = new Connection($server, $options, $driverOptions); + + // Assertions + $this->assertAttributeInstanceOf(Client::class, 'client', $connection); + $this->assertAttributeSame('my_db', 'defaultDatabase', $connection); + } + + public function testShouldGetConnection() + { + // Set + $server = 'mongodb://my-server/my_db'; + $options = ['some', 'uri', 'options']; + $driverOptions = ['some', 'driver', 'options']; + $expectedParameters = [ + 'uri' => $server, + 'typeMap' => ['array' => 'array'], + ]; + + // Actions + $connection = new Connection($server, $options, $driverOptions); + $client = $connection->getClient(); + + // Assertions + $this->assertAttributeSame($expectedParameters['uri'], 'uri', $client); + $this->assertAttributeSame($expectedParameters['typeMap'], 'typeMap', $client); + } +} diff --git a/tests/Unit/Connection/ManagerTest.php b/tests/Unit/Connection/ManagerTest.php new file mode 100644 index 00000000..7bcdb9ee --- /dev/null +++ b/tests/Unit/Connection/ManagerTest.php @@ -0,0 +1,80 @@ +setProtected(Manager::class, 'singleton', null); + parent::tearDown(); + } + + public function testShouldAddAndGetConnection() + { + // Set + $manager = new Manager(); + $connection = m::mock(IlluminateContainer::class); + $client = m::mock(Client::class); + + // Expectations + $connection->expects() + ->getClient() + ->andReturn($client); + + // Actions + $manager->setConnection($connection); + + // Assertions + $this->assertSame($client, $manager->getClient()); + } + + public function testShouldSetEventTrigger() + { + // Set + $test = $this; + $manager = new Manager(); + $container = m::mock(IlluminateContainer::class); + $eventTrigger = m::mock(EventTriggerInterface::class); + + $this->setProtected($manager, 'container', $container); + + // Expectations + $container->expects() + ->instance(EventTriggerService::class, m::type(EventTriggerService::class)) + ->andReturnUsing(function ($class, $eventService) use ($test, $eventTrigger) { + $test->assertSame(EventTriggerService::class, $class); + $test->assertAttributeSame($eventTrigger, 'dispatcher', $eventService); + }); + + // Actions + $manager->setEventTrigger($eventTrigger); + } + + public function testShouldInitializeOnce() + { + // Set + $manager = new Manager(); + + // Actions + $this->callProtected($manager, 'init'); + + // Assertions + $this->assertAttributeSame($manager, 'singleton', Manager::class); + $this->assertAttributeInstanceOf(IlluminateContainer::class, 'container', $manager); + + // Actions + $container = $manager->container; + $this->callProtected($manager, 'init'); + + // Assertions + // Initializes again to make sure that it will not instantiate a new container + $this->assertAttributeSame($container, 'container', $manager); + } +} diff --git a/tests/Unit/Container/ContainerTest.php b/tests/Unit/Container/ContainerTest.php new file mode 100644 index 00000000..829796f4 --- /dev/null +++ b/tests/Unit/Container/ContainerTest.php @@ -0,0 +1,47 @@ +flush(); + + parent::tearDown(); + } + + public function testShouldCallMethodsProperlyWithNoArguments() + { + // Set + $illuminateContainer = m::mock(IlluminateContainer::class); + Container::setContainer($illuminateContainer); + + // Expectations + $illuminateContainer->expects() + ->method() + ->andReturn(true); + + // Actions + Container::method(); + } + + public function testShouldCallMethodsProperlyWithArguments() + { + // Set + $illuminateContainer = m::mock(IlluminateContainer::class); + Container::setContainer($illuminateContainer); + + // Expectations + $illuminateContainer->expects() + ->method(1, 2, 3) + ->andReturn(true); + + // Actions + Container::method(1, 2, 3); + } +} diff --git a/tests/Unit/Cursor/CursorTest.php b/tests/Unit/Cursor/CursorTest.php new file mode 100644 index 00000000..1ce36b13 --- /dev/null +++ b/tests/Unit/Cursor/CursorTest.php @@ -0,0 +1,531 @@ +getCursor(); + + // Actions + $cursor->limit(10); + + // Assertions + $this->assertAttributeSame( + [[], ['limit' => 10]], + 'params', + $cursor + ); + } + + public function testShouldSortDocumentsOfCursor() + { + // Set + $cursor = $this->getCursor(); + + // Actions + $cursor->sort(['name' => 1]); + + // Assertions + $this->assertAttributeSame( + [[], ['sort' => ['name' => 1]]], + 'params', + $cursor + ); + } + + public function testShouldSkipDocuments() + { + // Set + $cursor = $this->getCursor(); + + // Actions + $cursor->skip(5); + + // Assertions + $this->assertAttributeSame( + [[], ['skip' => 5]], + 'params', + $cursor + ); + } + + public function testShouldSetNoCursorTimeoutToTrue() + { + // Set + $cursor = $this->getCursor(); + + // Actions + $cursor->disableTimeout(); + + // Assertions + $this->assertAttributeSame( + [[], ['noCursorTimeout' => true]], + 'params', + $cursor + ); + } + + public function testShouldSetReadPreferenceParameterAccordingly() + { + // Set + $cursor = $this->getCursor(); + $mode = ReadPreference::RP_SECONDARY; + + // Actions + $cursor->setReadPreference($mode); + $readPreferenceParameter = $this->getProtected($cursor, 'params')[1]['readPreference']; + $result = $readPreferenceParameter->getMode(); + + // Assertions + $this->assertInstanceOf(ReadPreference::class, $readPreferenceParameter); + $this->assertSame($mode, $result); + } + + public function testShouldBeAbleToSetReadPreferenceAndCursorTimeoutTogether() + { + // Set + $cursor = $this->getCursor(); + $mode = ReadPreference::RP_SECONDARY; + + // Actions + $cursor->setReadPreference($mode); + $cursor->disableTimeout(); + $readPreferenceParameter = $this->getProtected($cursor, 'params')[1]['readPreference']; + $result = $readPreferenceParameter->getMode(); + $timeoutResult = $this->getProtected($cursor, 'params')[1]['noCursorTimeout']; + + // Assertions + $this->assertInstanceOf(ReadPreference::class, $readPreferenceParameter); + $this->assertSame($mode, $result); + $this->assertTrue($timeoutResult); + } + + public function testShouldCountDocuments() + { + // Set + $collection = m::mock(Collection::class); + $cursor = $this->getCursor($collection); + + // Expectations + $collection->expects() + ->count([]) + ->andReturn(5); + + // Actions + $result = $cursor->count(); + + // Assertions + $this->assertSame(5, $result); + } + + public function testShouldCountDocumentsWithCountFunction() + { + // Set + $collection = m::mock(Collection::class); + $cursor = $this->getCursor($collection); + + // Expectations + $collection->expects() + ->count([]) + ->andReturn(5); + + // Actions + $result = count($cursor); + + // Assertions + $this->assertSame(5, $result); + } + + public function testShouldRewind() + { + // Set + $collection = m::mock(Collection::class); + $driverCursor = m::mock(CachingIterator::class); + $cursor = $this->getCursor($collection, 'find', [[]], $driverCursor); + + $this->setProtected($cursor, 'position', 10); + + // Expectations + $driverCursor->expects() + ->rewind(); + + // Actions + $cursor->rewind(); + + // Assertions + $this->assertAttributeSame(0, 'position', $cursor); + } + + public function testShouldRewindACursorThatHasAlreadyBeenInitialized() + { + // Set + $collection = m::mock(Collection::class); + $driverCursor = m::mock(CachingIterator::class); + $cursor = $this->getCursor($collection, 'find', [[]], $driverCursor); + + $this->setProtected($cursor, 'position', 10); + + // Expectations + $driverCursor->expects() + ->rewind() + ->andReturnUsing( + function () use ($cursor) { + if ($this->getProtected($cursor, 'cursor')) { + throw new LogicException('Cursor already initialized', 1); + } + } + ); + + // Actions + $cursor->rewind(); + + // Assertions + $this->assertAttributeSame(0, 'position', $cursor); + } + + public function testShouldGetCurrent() + { + // Set + $collection = m::mock(Collection::class); + $object = new class extends AbstractModel + { + }; + $object->name = 'John Doe'; + $driverCursor = new ArrayIterator([$object]); + $cursor = $this->getCursor($collection, 'find', [[]], $driverCursor); + + // Actions + $model = $cursor->current(); + + // Assertions + $this->assertInstanceOf(AbstractModel::class, $model); + $this->assertSame('John Doe', $model->name); + } + + public function testShouldGetFirst() + { + // Set + $collection = m::mock(Collection::class); + $object = new class extends AbstractModel + { + }; + $object->name = 'John Doe'; + $driverCursor = new CachingIterator(new ArrayObject([$object])); + $cursor = $this->getCursor($collection, 'find', [[]], $driverCursor); + + // Actions + $model = $cursor->first(); + + // Assertions + $this->assertInstanceOf(get_class($object), $model); + $this->assertSame('John Doe', $model->name); + } + + public function testShouldGetFirstWhenEmpty() + { + // Set + $collection = m::mock(Collection::class); + $driverCursor = new CachingIterator(new ArrayObject()); + + $cursor = $this->getCursor($collection, 'find', [[]], $driverCursor); + + // Actions + $result = $cursor->first(); + + // Assertions + $this->assertNull($result); + } + + public function testShouldRefreshTheCursor() + { + // Set + $driverCursor = new CachingIterator(new ArrayObject()); + $cursor = $this->getCursor(); + $this->setProtected($cursor, 'cursor', $driverCursor); + + // Actions + $cursor->fresh(); + + // Assertions + $this->assertAttributeSame(null, 'cursor', $cursor); + } + + public function testShouldImplementKeyMethodFromIterator() + { + // Set + $cursor = $this->getCursor(); + $this->setProtected($cursor, 'position', 7); + + // Actions + $result = $cursor->key(); + + // Assertions + $this->assertSame(7, $result); + } + + public function testShouldImplementNextMethodFromIterator() + { + // Set + $collection = m::mock(Collection::class); + $driverCursor = m::mock(CachingIterator::class); + $cursor = $this->getCursor($collection, 'find', [[]], $driverCursor); + + $this->setProtected($cursor, 'position', 7); + + // Expectations + $driverCursor->expects() + ->next(); + + // Actions + $cursor->next(); + + // Assertions + $this->assertSame(8, $cursor->key()); + } + + public function testShouldImplementValidMethodFromIterator() + { + // Set + $collection = m::mock(Collection::class); + $driverCursor = m::mock(CachingIterator::class); + $cursor = $this->getCursor($collection, 'find', [[]], $driverCursor); + + // Expectations + $driverCursor->expects() + ->valid() + ->andReturn(true); + + // Actions + $result = $cursor->valid(); + + // Assertions + $this->assertTrue($result); + } + + public function testShouldWrapMongoDriverCursorWithIterator() + { + // Set + $collection = m::mock(Collection::class); + $cursor = $this->getCursor($collection, 'find', [['bacon' => true]]); + $driverCursor = m::mock(Traversable::class); + $driverIterator = m::mock(Iterator::class); + + // Expectations + $collection->expects() + ->find(['bacon' => true]) + ->andReturn($driverCursor); + + $driverCursor->expects() + ->getIterator() + ->andReturn($driverIterator); + + // Because when creating an IteratorIterator with the driverCursor + // this methods will be called once to initialize the iterable object. + $driverIterator->expects() + ->rewind() + ->andReturn(true); + + $driverIterator->expects() + ->valid() + ->andReturn(true); + + $driverIterator->expects() + ->current() + ->andReturn(true); + + $driverIterator->expects() + ->key() + ->andReturn(true); + + // Actions + $result = $this->callProtected($cursor, 'getCursor'); + + // Assertions + $this->assertInstanceOf(CachingIterator::class, $result); + } + + public function testShouldReturnAllResults() + { + // Set + $collection = m::mock(Collection::class); + $object = new class extends AbstractModel + { + }; + $class = get_class($object); + $bob = new $class(); + $bob->name = 'bob'; + $bob->occupation = 'coder'; + + $jef = new $class(); + $jef->name = 'jef'; + $jef->occupation = 'tester'; + + $driverCursor = new CachingIterator(new ArrayObject([$bob, $jef])); + $cursor = $this->getCursor($collection, 'find', [[]], $driverCursor); + + // Actions + $result = $cursor->all(); + + // Assertions + $this->assertCount(2, $result); + $this->assertContainsOnlyInstancesOf($class, $result); + + $firstModel = $result[0]; + $this->assertSame('bob', $firstModel->name); + $this->assertSame('coder', $firstModel->occupation); + + $nextModel = $result[1]; + $this->assertSame('jef', $nextModel->name); + $this->assertSame('tester', $nextModel->occupation); + } + + public function testShouldReturnResultsToArray() + { + // Set + $collection = m::mock(Collection::class); + $driverCursor = m::mock(CachingIterator::class); + $cursor = $this->getCursor($collection, 'find', [[]], $driverCursor); + + // Expectations + $driverCursor->expects() + ->rewind(); + + $driverCursor->expects() + ->valid() + ->times(3) + ->andReturn(true, true, false); + + $driverCursor->expects() + ->next() + ->twice() + ->andReturn(true, false); + + $driverCursor->expects() + ->current() + ->twice() + ->andReturn( + ['name' => 'bob', 'occupation' => 'coder'], + ['name' => 'jef', 'occupation' => 'tester'] + ); + + // Actions + $result = $cursor->toArray(); + + // Assertions + $this->assertSame( + [ + ['name' => 'bob', 'occupation' => 'coder'], + ['name' => 'jef', 'occupation' => 'tester'], + ], + $result + ); + } + + public function testShouldSerializeAnActiveCursor() + { + // Set + $connection = $this->instance(Connection::class, m::mock(Connection::class)); + $cursor = $this->getCursor(null, 'find', [[]]); + $driverCollection = $this->getDriverCollection(); + + $this->setProtected($cursor, 'collection', $driverCollection); + + $client = m::mock(Client::class); + $database = m::mock(Database::class); + $connection->defaultDatabase = 'db'; + + // Expectations + $connection->expects() + ->getClient() + ->andReturn($client); + + $client->expects() + ->selectDatabase('db') + ->andReturn($database); + + $database->expects() + ->selectCollection('my_collection') + ->andReturn($driverCollection); + + // Actions + $result = unserialize(serialize($cursor)); + + // Assertions + $this->assertEquals($cursor, $result); + } + + protected function getCursor( + $collection = null, + $command = 'find', + $params = [[]], + $driverCursor = null + ): Cursor { + if (!$collection) { + $collection = m::mock(Collection::class); + } + + if (!$driverCursor) { + return new Cursor($collection, $command, $params); + } + + $mock = m::mock( + Cursor::class.'[getCursor]', + [$collection, $command, $params] + ); + + $mock->shouldAllowMockingProtectedMethods(); + $mock->allows() + ->getCursor() + ->andReturn($driverCursor); + + $this->setProtected($mock, 'cursor', $driverCursor); + + return $mock; + } + + /** + * Since the MongoDB\Collection is not serializable. This method will + * emulate an unserializable collection from mongoDb driver. + */ + protected function getDriverCollection(): Serializable + { + /* + * Emulates a MongoDB\Collection non serializable behavior. + */ + return new class() implements Serializable + { + public function serialize() + { + throw new Exception('Unable to serialize', 1); + } + + public function unserialize($serialized) + { + } + + public function getCollectionName(): string + { + return 'my_collection'; + } + }; + } +} diff --git a/tests/Mongolid/Cursor/EmbeddedCursorTest.php b/tests/Unit/Cursor/EmbeddedCursorTest.php similarity index 50% rename from tests/Mongolid/Cursor/EmbeddedCursorTest.php rename to tests/Unit/Cursor/EmbeddedCursorTest.php index 1488cb1f..8e5f22a4 100644 --- a/tests/Mongolid/Cursor/EmbeddedCursorTest.php +++ b/tests/Unit/Cursor/EmbeddedCursorTest.php @@ -1,34 +1,27 @@ 'A'], ['name' => 'B'], ['name' => 'C'], ]; - $cursor = $this->getCursor(stdClass::class, $items); + $cursor = new EmbeddedCursor($items); - // Assert + // Actions $cursor->limit(2); - $this->assertAttributeEquals( + + // Assertions + $this->assertAttributeSame( [ ['name' => 'A'], ['name' => 'B'], @@ -41,33 +34,33 @@ public function testShouldLimitDocumentQuantity() /** * @dataProvider getDocumentsToSort */ - public function testShouldSortDocuments($items, $parameters, $expected) + public function testShouldSortDocuments(array $items, array $parameters, array $expected) { - // Arrange - $cursor = $this->getCursor(stdClass::class, $items); + // Set + $cursor = new EmbeddedCursor($items); - // Assert + // Actions $cursor->sort($parameters); - $this->assertAttributeSame( - $expected, - 'items', - $cursor - ); + + // Assertions + $this->assertAttributeSame($expected, 'items', $cursor); } public function testShouldSkipDocuments() { - // Arrange + // Set $items = [ ['name' => 'A'], ['name' => 'B'], ['name' => 'C'], ]; - $cursor = $this->getCursor(stdClass::class, $items); + $cursor = new EmbeddedCursor($items); - // Assert + // Actions $cursor->skip(2); - $this->assertAttributeEquals( + + // Assertions + $this->assertAttributeSame( [ ['name' => 'C'], ], @@ -78,220 +71,266 @@ public function testShouldSkipDocuments() public function testShouldCountDocuments() { - // Arrange + // Set $items = [ ['name' => 'A'], ['name' => 'B'], ['name' => 'C'], ]; - $cursor = $this->getCursor(stdClass::class, $items); + $cursor = new EmbeddedCursor($items); - // Assert - $this->assertEquals(3, $cursor->count()); + // Actions + $result = $cursor->count(); + + // Assertions + $this->assertSame(3, $result); } public function testShouldCountDocumentsWithCountFunction() { - // Arrange + // Set $items = [ ['name' => 'A'], ['name' => 'B'], ['name' => 'C'], ]; - $cursor = $this->getCursor(stdClass::class, $items); + $cursor = new EmbeddedCursor($items); + + // Actions + $result = count($cursor); - // Assert - $this->assertEquals(3, count($cursor)); + // Assertions + $this->assertSame(3, $result); } public function testShouldRewind() { - // Arrange - $cursor = $this->getCursor(); + // Set + $cursor = new EmbeddedCursor([]); - // Assert + // Actions $cursor->rewind(); - $this->assertAttributeEquals(0, 'position', $cursor); + + // Assertions + $this->assertAttributeSame(0, 'position', $cursor); } public function testShouldGetCurrent() { - // Arrange + // Set + $object = new class extends AbstractModel + { + }; + $class = get_class($object); + $itemA = new $class(); + $itemA->name = 'A'; + + $itemB = new $class(); + $itemB->name = 'B'; + + $itemC = new $class(); + $itemC->name = 'C'; + $items = [ - ['name' => 'A'], - ['name' => 'B'], - ['name' => 'C'], + $itemA, + $itemB, + $itemC, ]; - $cursor = $this->getCursor(stdClass::class, $items); + $cursor = new EmbeddedCursor($items); $this->setProtected($cursor, 'position', 1); - // Assert - $entity = $cursor->current(); - $this->assertInstanceOf(stdClass::class, $entity); - $this->assertAttributeEquals('B', 'name', $entity); + // Actions + $model = $cursor->current(); + + // Assertions + $this->assertInstanceOf($class, $model); + $this->assertSame('B', $model->name); } public function testShouldNotGetCurrentWhenCursorIsInvalid() { - // Arrange + // Set $items = []; - $cursor = $this->getCursor(stdClass::class, $items); + $cursor = new EmbeddedCursor($items); $this->setProtected($cursor, 'position', 1); - // Assert - $entity = $cursor->current(); - $this->assertNull($entity); + // Actions + $model = $cursor->current(); + + // Assertions + $this->assertNull($model); } - public function testShouldGetCurrentUsingEntityClass() + public function testShouldGetCurrentUsingModelClass() { - // Arrange + // Set $object = new stdClass(); $object->name = 'A'; $items = [$object]; - $cursor = $this->getCursor(stdClass::class, $items); + $cursor = new EmbeddedCursor($items); $this->setProtected($cursor, 'position', 0); - // Assert - $entity = $cursor->current(); - $this->assertInstanceOf(stdClass::class, $entity); - $this->assertAttributeEquals('A', 'name', $entity); + // Actions + $model = $cursor->current(); + + // Assertions + $this->assertInstanceOf(stdClass::class, $model); + $this->assertAttributeSame('A', 'name', $model); } - public function testShouldGetCurrentUsingEntityClassAndMorphinIt() + public function testShouldGetCurrentUsingModelClassMorphingIt() { - // Arrange - $object = new class() extends ActiveRecord implements PolymorphableInterface { - public function polymorph() - { - return 'Bacon'; - } + // Set + $model = new class() extends AbstractModel + { }; + $model->name = 'John'; + $model->syncOriginalDocumentAttributes(); - $class = get_class($object); - $items = [$object->attributes]; - $cursor = $this->getCursor($class, $items); + $items = [$model]; + $cursor = new EmbeddedCursor($items); - $this->setProtected($cursor, 'position', 0); + // Actions + $result = $cursor->current(); - // Assert - $entity = $cursor->current(); - $this->assertEquals('Bacon', $entity); + // Assertions + $this->assertSame($model, $result); + $this->assertSame('John', $result->name); } public function testShouldGetFirst() { - // Arrange + // Set + $object = new class extends AbstractModel + { + }; + $class = get_class($object); + $modelA = new $class(); + $modelA->name = 'A'; + $modelA->syncOriginalDocumentAttributes(); + $modelB = clone $modelA; + $modelB->name = 'B'; + $modelB->syncOriginalDocumentAttributes(); + $items = [ - ['name' => 'A'], - ['name' => 'B'], - ['name' => 'C'], + $modelA, + $modelB, ]; - $cursor = $this->getCursor(stdClass::class, $items); + $cursor = new EmbeddedCursor($items); $this->setProtected($cursor, 'position', 1); - // Assert - $entity = $cursor->first(); - $this->assertInstanceOf(stdClass::class, $entity); - $this->assertAttributeEquals('A', 'name', $entity); + // Actions + $model = $cursor->first(); + + // Assertions + $this->assertInstanceOf($class, $model); + $this->assertSame('A', $model->name); } public function testShouldGetAllItems() { - // Arrange + // Set + $modelA = new class extends AbstractModel + { + }; + $modelA->name = 'A'; + $modelA->syncOriginalDocumentAttributes(); + $modelB = clone $modelA; + $modelB->name = 'B'; + $modelB->syncOriginalDocumentAttributes(); + $items = [ - ['name' => 'A'], - ['name' => 'B'], + $modelA, + $modelB, ]; - $cursor = $this->getCursor(stdClass::class, $items); - + $cursor = new EmbeddedCursor($items); $this->setProtected($cursor, 'position', 1); - $entityA = new stdClass(); - $entityA->name = 'A'; - - $entityB = new stdClass(); - $entityB->name = 'B'; - $expected = [ - $entityA, - $entityB, + $modelA, + $modelB, ]; - // Assert + // Actions $result = $cursor->all(); - $this->assertEquals($expected, $result); + // Assertions + $this->assertSame($expected, $result); } public function testShouldGetAllInArrayFormat() { - // Arrange + // Set $items = [ ['name' => 'A'], ['name' => 'B'], ['name' => 'C'], ]; - $cursor = $this->getCursor(stdClass::class, $items); - + $cursor = new EmbeddedCursor($items); $this->setProtected($cursor, 'position', 1); - // Assert + // Actions $result = $cursor->toArray(); - $this->assertEquals($items, $result); + + // Assertions + $this->assertSame($items, $result); } public function testShouldImplementKeyMethodFromIterator() { - // Arrange - $cursor = $this->getCursor(); - + // Set + $cursor = new EmbeddedCursor([]); $this->setProtected($cursor, 'position', 7); - // Assertion - $this->assertEquals(7, $cursor->key()); + // Actions + $result = $cursor->key(); + + // Assertions + $this->assertSame(7, $result); } public function testShouldImplementNextMethodFromIterator() { - // Arrange - $cursor = $this->getCursor(); - + // Set + $cursor = new EmbeddedCursor([]); $this->setProtected($cursor, 'position', 7); - // Assertion + // Actions $cursor->next(); - $this->assertAttributeEquals(8, 'position', $cursor); + + // Assertions + $this->assertAttributeSame(8, 'position', $cursor); } public function testShouldImplementValidMethodFromIterator() { - // Arrange + // Set $items = [ ['name' => 'A'], ['name' => 'B'], ['name' => 'C'], ]; - $cursor = $this->getCursor(stdClass::class, $items); + $cursor = new EmbeddedCursor($items); - // Assert - $this->assertTrue($cursor->valid()); + // Actions + $result = $cursor->valid(); + + // Assertions + $this->assertTrue($result); + + // Actions $this->setProtected($cursor, 'position', 8); - $this->assertFalse($cursor->valid()); - } + $result = $cursor->valid(); - protected function getCursor( - $entityClass = stdClass::class, - $items = [] - ) { - return new EmbeddedCursor($entityClass, $items); + // Assertions + $this->assertFalse($result); } - public function getDocumentsToSort() + public function getDocumentsToSort(): array { $age24 = (object) ['age' => 24]; @@ -301,15 +340,17 @@ public function getDocumentsToSort() ['age' => 26, 'name' => 'Abe'], ['age' => 25], $age24, - ['age' => 26, 'name' => 'Zizaco'], + ['age' => 26, 'name' => 'Wilson'], ['age' => 26, 'name' => 'John'], + [], ], 'parameters' => ['age' => 1], 'expected' => [ + [], $age24, ['age' => 25], ['age' => 26, 'name' => 'Abe'], - ['age' => 26, 'name' => 'Zizaco'], + ['age' => 26, 'name' => 'Wilson'], ['age' => 26, 'name' => 'John'], ], ], @@ -318,16 +359,18 @@ public function getDocumentsToSort() ['age' => 26, 'name' => 'Abe'], ['age' => 25], $age24, - ['age' => 26, 'name' => 'Zizaco'], + ['age' => 26, 'name' => 'Wilson'], ['age' => 26, 'name' => 'John'], + [], ], 'parameters' => ['age' => -1], 'expected' => [ ['age' => 26, 'name' => 'Abe'], - ['age' => 26, 'name' => 'Zizaco'], + ['age' => 26, 'name' => 'Wilson'], ['age' => 26, 'name' => 'John'], ['age' => 25], $age24, + [], ], ], 'two sorting parameters' => [ @@ -335,14 +378,16 @@ public function getDocumentsToSort() ['age' => 26, 'name' => 'Abe'], ['age' => 25], $age24, - ['age' => 26, 'name' => 'Zizaco'], + ['age' => 26, 'name' => 'Wilson'], ['age' => 26, 'name' => 'John'], + [], ], 'parameters' => ['age' => 1, 'name' => -1], 'expected' => [ + [], $age24, ['age' => 25], - ['age' => 26, 'name' => 'Zizaco'], + ['age' => 26, 'name' => 'Wilson'], ['age' => 26, 'name' => 'John'], ['age' => 26, 'name' => 'Abe'], ], @@ -352,16 +397,16 @@ public function getDocumentsToSort() ['age' => 26, 'name' => 'Abe', 'color' => 'red'], ['age' => 25], $age24, - ['age' => 26, 'name' => 'Zizaco', 'color' => 'red'], - ['age' => 26, 'name' => 'Zizaco', 'color' => 'blue'], + ['age' => 26, 'name' => 'Wilson', 'color' => 'red'], + ['age' => 26, 'name' => 'Wilson', 'color' => 'blue'], ['age' => 26, 'name' => 'John'], ], 'parameters' => ['age' => 1, 'name' => -1, 'color' => 1], 'expected' => [ $age24, ['age' => 25], - ['age' => 26, 'name' => 'Zizaco', 'color' => 'blue'], - ['age' => 26, 'name' => 'Zizaco', 'color' => 'red'], + ['age' => 26, 'name' => 'Wilson', 'color' => 'blue'], + ['age' => 26, 'name' => 'Wilson', 'color' => 'red'], ['age' => 26, 'name' => 'John'], ['age' => 26, 'name' => 'Abe', 'color' => 'red'], ], diff --git a/tests/Mongolid/Event/EventTriggerServiceTest.php b/tests/Unit/Event/EventTriggerServiceTest.php similarity index 53% rename from tests/Mongolid/Event/EventTriggerServiceTest.php rename to tests/Unit/Event/EventTriggerServiceTest.php index 81ee37b9..5c6c11d0 100644 --- a/tests/Mongolid/Event/EventTriggerServiceTest.php +++ b/tests/Unit/Event/EventTriggerServiceTest.php @@ -1,51 +1,46 @@ registerEventDispatcher($dispatcher); - // Act - $dispatcher->shouldReceive('fire') - ->once() - ->with('foobar', ['answer' => 23], true) + // Expectations + $dispatcher->expects() + ->fire('foobar', ['answer' => 23], true) ->andReturn(true); - // Assertion - $service->registerEventDispatcher($dispatcher); - $this->assertTrue( - $service->fire('foobar', ['answer' => 23], true) - ); + // Actions + $result = $service->fire('foobar', ['answer' => 23], true); + + // Assertions + $this->assertTrue($result); } public function testShouldReturnTrueIfThereIsNoExternalDispatcher() { - // Arrange + // Set $dispatcher = m::mock(EventTriggerInterface::class); $service = new EventTriggerService(); - // Act - $dispatcher->shouldReceive('fire') + // Expectations + $dispatcher->expects() + ->fire() ->never(); - // Assertion + // Actions + $result = $service->fire('foobar', ['answer' => 23], true); + + // Assertions /* without calling registerEventDispatcher */ - $this->assertTrue( - $service->fire('foobar', ['answer' => 23], true) - ); + $this->assertTrue($result); } } diff --git a/tests/Unit/Model/AbstractModelTest.php b/tests/Unit/Model/AbstractModelTest.php new file mode 100644 index 00000000..3f68da88 --- /dev/null +++ b/tests/Unit/Model/AbstractModelTest.php @@ -0,0 +1,581 @@ +model = new class() extends AbstractModel + { + /** + * {@inheritdoc} + */ + protected $collection = 'mongolid'; + + public function unsetCollection() + { + unset($this->collection); + } + }; + } + + /** + * {@inheritdoc} + */ + protected function tearDown() + { + unset($this->model); + parent::tearDown(); + } + + public function testShouldImplementModelTraits() + { + // Actions + $result = array_keys(class_uses(AbstractModel::class)); + + // Assertions + $this->assertSame( + [HasAttributesTrait::class, HasRelationsTrait::class], + $result + ); + } + + public function testShouldImplementModelInterface() + { + // Actions + $result = array_keys(class_implements(AbstractModel::class)); + + // Assertions + $this->assertSame( + [ + ModelInterface::class, + Unserializable::class, + Serializable::class, + Type::class, + Persistable::class, + HasAttributesInterface::class, + ], + $result + ); + } + + public function testShouldSave() + { + // Set + $builder = $this->instance(Builder::class, m::mock(Builder::class)); + + // Expectations + $builder->expects() + ->save($this->model, ['writeConcern' => new WriteConcern(1)]) + ->andReturn(true); + + // Actions + $result = $this->model->save(); + + // Assertions + $this->assertTrue($result); + } + + public function testShouldInsert() + { + // Set + $builder = $this->instance(Builder::class, m::mock(Builder::class)); + + // Expectations + $builder->expects() + ->insert($this->model, ['writeConcern' => new WriteConcern(1)]) + ->andReturn(true); + + // Actions + $result = $this->model->insert(); + + // Assertions + $this->assertTrue($result); + } + + public function testShouldUpdate() + { + // Set + $builder = $this->instance(Builder::class, m::mock(Builder::class)); + + // Expectations + $builder->expects() + ->update($this->model, ['writeConcern' => new WriteConcern(1)]) + ->andReturn(true); + + // Actions + $result = $this->model->update(); + + // Assertions + $this->assertTrue($result); + } + + public function testShouldDelete() + { + // Set + $builder = $this->instance(Builder::class, m::mock(Builder::class)); + + // Expectations + $builder->expects() + ->delete($this->model, ['writeConcern' => new WriteConcern(1)]) + ->andReturn(true); + + // Actions + $result = $this->model->delete(); + + // Assertions + $this->assertTrue($result); + } + + public function testSaveShouldThrowExceptionIfCollectionIsNull() + { + // Set + $this->model->unsetCollection(); + + // Expectations + $this->expectException(NoCollectionNameException::class); + $this->expectExceptionMessage('Collection name not specified into Model instance'); + + // Actions + $this->model->save(); + } + + public function testUpdateShouldThrowExceptionIfCollectionIsNull() + { + // Set + $this->model->unsetCollection(); + + // Expectations + $this->expectException(NoCollectionNameException::class); + $this->expectExceptionMessage('Collection name not specified into Model instance'); + + // Actions + $this->model->update(); + } + + public function testInsertShouldThrowExceptionIfCollectionIsNull() + { + // Set + $this->model->unsetCollection(); + + // Expectations + $this->expectException(NoCollectionNameException::class); + $this->expectExceptionMessage('Collection name not specified into Model instance'); + + // Actions + $this->model->insert(); + } + + public function testDeleteShouldThrowExceptionIfCollectionIsNull() + { + // Set + $this->model->unsetCollection(); + + // Expectations + $this->expectException(NoCollectionNameException::class); + $this->expectExceptionMessage('Collection name not specified into Model instance'); + + // Actions + $this->model->delete(); + } + + public function testShouldGetWithWhereQuery() + { + // Set + $query = ['foo' => 'bar']; + $projection = ['some', 'fields']; + $builder = $this->instance(Builder::class, m::mock(Builder::class)); + + $cursor = m::mock(CursorInterface::class); + + // Expectations + $builder->expects() + ->where(m::type(get_class($this->model)), $query, $projection) + ->andReturn($cursor); + + // Actions + $result = $this->model->where($query, $projection); + + // Assertions + $this->assertSame($cursor, $result); + } + + public function testShouldGetAll() + { + // Set + $builder = $this->instance(Builder::class, m::mock(Builder::class)); + $cursor = m::mock(CursorInterface::class); + + // Expectations + $builder->expects() + ->all(m::type(get_class($this->model))) + ->andReturn($cursor); + + // Actions + $result = $this->model->all(); + + // Assertions + $this->assertSame($cursor, $result); + } + + public function testShouldGetFirstWithQuery() + { + // Set + $query = ['foo' => 'bar']; + $projection = ['some', 'fields']; + $builder = $this->instance(Builder::class, m::mock(Builder::class)); + + // Expectations + $builder->expects() + ->first(m::type(get_class($this->model)), $query, $projection) + ->andReturn($this->model); + + // Actions + $result = $this->model->first($query, $projection); + + // Assertions + $this->assertSame($this->model, $result); + } + + public function testShouldGetFirstOrFail() + { + // Set + $builder = $this->instance(Builder::class, m::mock(Builder::class)); + $query = ['foo' => 'bar']; + $projection = ['some', 'fields']; + + // Expectations + $builder->expects() + ->firstOrFail(m::type(get_class($this->model)), $query, $projection) + ->andReturn($this->model); + + // Actions + $result = $this->model->firstOrFail($query, $projection); + + // Assertions + $this->assertSame($this->model, $result); + } + + public function testShouldGetFirstOrNewAndReturnExistingModel() + { + // Set + $builder = $this->instance(Builder::class, m::mock(Builder::class)); + $id = 123; + + // Expectations + $builder->expects() + ->first(m::type(get_class($this->model)), $id, []) + ->andReturn($this->model); + + // Actions + $result = $this->model->firstOrNew($id); + + // Assertions + $this->assertSame($this->model, $result); + } + + public function testShouldGetFirstOrNewAndReturnNewModel() + { + // Set + $builder = $this->instance(Builder::class, m::mock(Builder::class)); + $id = 123; + + // Expectations + $builder->expects() + ->first(m::type(get_class($this->model)), $id, []) + ->andReturn(null); + + // Actions + $result = $this->model->firstOrNew($id); + + // Assertions + $this->assertNotEquals($this->model, $result); + } + + public function testShouldGetBuilder() + { + // Set + $model = new class extends AbstractModel + { + }; + + // Actions + $result = $this->callProtected($model, 'getBuilder'); + + // Assertions + $this->assertInstanceOf(Builder::class, $result); + } + + public function testShouldRaiseExceptionWhenHasNoCollectionAndTryToCallAllFunction() + { + // Set + $model = new class() extends AbstractModel + { + }; + + // Expectations + $this->expectException(NoCollectionNameException::class); + + // Actions + $model->all(); + } + + public function testShouldRaiseExceptionWhenHasNoCollectionAndTryToCallFirstFunction() + { + // Set + $model = new class() extends AbstractModel + { + }; + + // Expectations + $this->expectException(NoCollectionNameException::class); + + // Actions + $model->first(); + } + + public function testShouldRaiseExceptionWhenHasNoCollectionAndTryToCallWhereFunction() + { + // Set + $model = new class() extends AbstractModel + { + }; + + // Expectations + $this->expectException(NoCollectionNameException::class); + + // Actions + $model->where(); + } + + public function testShouldGetCollectionName() + { + // Actions + $result = $this->model->getCollectionName(); + + // Assertions + $this->assertSame('mongolid', $result); + } + + public function testShouldHaveDefaultWriteConcern() + { + // Actions + $result = $this->model->getWriteConcern(); + + // Assertions + $this->assertSame(1, $result); + } + + public function testShouldSetWriteConcern() + { + // Actions + $this->model->setWriteConcern(0); + $result = $this->model->getWriteConcern(); + + // Assertions + $this->assertSame(0, $result); + } + + public function testShouldHaveDynamicSetters() + { + // Set + $model = new class() extends AbstractModel + { + }; + + $childObj = new stdClass(); + $model->name = 'John'; + $model->age = 25; + $model->child = $childObj; + + // Actions + $result = $model->getDocumentAttributes(); + + // Assertions + $this->assertSame( + [ + 'name' => 'John', + 'age' => 25, + 'child' => $childObj, + ], + $result + ); + } + + public function testShouldHaveDynamicGetters() + { + // Set + $child = new class() extends AbstractModel + { + }; + $model = new class() extends AbstractModel + { + }; + + // Actions + $model = $model::fill( + [ + 'name' => 'John', + 'age' => 25, + 'child' => $child, + ], + $model + ); + + // Assertions + $this->assertSame('John', $model->name); + $this->assertSame(25, $model->age); + $this->assertSame($child, $model->child); + $this->assertSame(null, $model->nonexistant); + } + + public function testShouldCheckIfAttributeIsSet() + { + // Set + $model = new class() extends AbstractModel + { + }; + + // Actions + $model = $model::fill(['name' => 'John', 'ignored' => null]); + + // Assertions + $this->assertTrue(isset($model->name)); + $this->assertFalse(isset($model->nonexistant)); + $this->assertFalse(isset($model->ignored)); + } + + public function testShouldCheckIfMutatedAttributeIsSet() + { + // Set + $model = new class() extends AbstractModel + { + /** + * {@inheritdoc} + */ + public $mutable = true; + + public function getNameDocumentAttribute() + { + return 'John'; + } + }; + + // Assertions + $this->assertTrue(isset($model->name)); + $this->assertFalse(isset($model->nonexistant)); + } + + public function testShouldUnsetAttributes() + { + // Set + $model = new class() extends AbstractModel + { + }; + $model = $model::fill( + [ + 'name' => 'John', + 'age' => 25, + ] + ); + + // Actions + unset($model->age); + $result = $model->getDocumentAttributes(); + + // Assertions + $this->assertSame(['name' => 'John'], $result); + } + + public function testShouldGetAttributeFromMutator() + { + // Set + $model = new class() extends AbstractModel + { + /** + * {@inheritdoc} + */ + public $mutable = true; + + public function getShortNameDocumentAttribute() + { + return 'Other name'; + } + }; + + // Actions + $model->short_name = 'My awesome name'; + $result = $model->short_name; + + // Assertions + $this->assertSame('Other name', $result); + } + + public function testShouldIgnoreMutators() + { + // Set + $model = new class() extends AbstractModel + { + public function getShortNameDocumentAttribute() + { + return 'Other name'; + } + + public function setShortNameDocumentAttribute($value) + { + return strtoupper($value); + } + }; + + // Actions + $model->short_name = 'My awesome name'; + + // Assertions + $this->assertSame('My awesome name', $model->short_name); + } + + public function testShouldSetAttributeFromMutator() + { + // Set + $model = new class() extends AbstractModel + { + /** + * {@inheritdoc} + */ + protected $mutable = true; + + public function setShortNameDocumentAttribute($value) + { + return strtoupper($value); + } + }; + + // Actions + $model->short_name = 'My awesome name'; + $result = $model->short_name; + + // Assertions + $this->assertSame('MY AWESOME NAME', $result); + } +} diff --git a/tests/Unit/Model/Exception/ModelNotFoundExceptionTest.php b/tests/Unit/Model/Exception/ModelNotFoundExceptionTest.php new file mode 100644 index 00000000..e4b0963c --- /dev/null +++ b/tests/Unit/Model/Exception/ModelNotFoundExceptionTest.php @@ -0,0 +1,22 @@ +setModel('User'); + + // Actions + $modelResult = $object->getModel(); + $messageResult = $object->getMessage(); + + // Assertions + $this->assertSame('User', $modelResult); + $this->assertSame('No query results for model [User].', $messageResult); + } +} diff --git a/tests/Unit/Model/HasAttributesTraitTest.php b/tests/Unit/Model/HasAttributesTraitTest.php new file mode 100644 index 00000000..f409e02f --- /dev/null +++ b/tests/Unit/Model/HasAttributesTraitTest.php @@ -0,0 +1,457 @@ +mutable = true; + } + + public function getShortNameDocumentAttribute() + { + return 'Other name'; + } + }; + + // Actions + $model->setDocumentAttribute('short_name', 'My awesome name'); + $result = $model->getDocumentAttribute('short_name'); + + // Assertions + $this->assertSame('Other name', $result); + } + + public function testShouldIgnoreMutators() + { + // Set + $model = new class() + { + use HasAttributesTrait; + use HasRelationsTrait; + + public function getShortNameDocumentAttribute() + { + return 'Other name'; + } + + public function setShortNameDocumentAttribute($value) + { + return strtoupper($value); + } + }; + + // Actions + $model->setDocumentAttribute('short_name', 'My awesome name'); + $result = $model->getDocumentAttribute('short_name'); + + // Assertions + $this->assertSame('My awesome name', $result); + } + + public function testShouldSetAttributeFromMutator() + { + // Set + $model = new class() + { + use HasAttributesTrait; + use HasRelationsTrait; + + public function __construct() + { + $this->mutable = true; + } + + public function setShortNameDocumentAttribute($value) + { + return strtoupper($value); + } + }; + + // Actions + $model->setDocumentAttribute('short_name', 'My awesome name'); + $result = $model->getDocumentAttribute('short_name'); + + // Assertions + $this->assertSame('MY AWESOME NAME', $result); + } + + /** + * @dataProvider getFillableOptions + */ + public function testShouldFillOnlyPermittedAttributes( + array $fillable, + array $guarded, + array $input, + array $expected + ) { + // Set + $model = new class($fillable, $guarded) implements HasAttributesInterface + { + use HasAttributesTrait; + use HasRelationsTrait; + + public function __construct(array $fillable, array $guarded) + { + $this->fillable = $fillable; + $this->guarded = $guarded; + } + }; + + // Actions + $model = $model::fill($input, $model); + + // Assertions + $this->assertSame($expected, $model->getDocumentAttributes()); + } + + public function testFillShouldRetrievePolymorphedModel() + { + // Set + $input = [ + 'type' => 'polymorphed', + 'new_field' => 'hello', + ]; + // Actions + $result = ReferencedUser::fill($input); + + // Assertions + $this->assertInstanceOf(PolymorphedReferencedUser::class, $result); + $this->assertSame('polymorphed', $result->type); + $this->assertSame('hello', $result->new_field); + } + + public function testFillShouldRetrievePolymorphedModelConsideringModelAttributes() + { + // Set + $input = [ + 'new_field' => 'hello', + ]; + $model = new ReferencedUser(); + $model->type = 'polymorphed'; + + // Actions + $result = ReferencedUser::fill($input, $model); + + // Assertions + $this->assertInstanceOf(PolymorphedReferencedUser::class, $result); + $this->assertSame('polymorphed', $result->type); + $this->assertSame('hello', $result->new_field); + } + + public function testFillShouldRetrievePolymorphedModelConsideringModelAttributesButPrioritizingInput() + { + // Set + $input = [ + 'type' => 'default', + 'new_field' => 'hello', + ]; + $model = new PolymorphedReferencedUser(); + $model->type = 'polymorphed'; + + // Actions + $result = ReferencedUser::fill($input, $model); + + // Assertions + $this->assertInstanceOf(ReferencedUser::class, $result); + $this->assertSame('default', $result->type); + $this->assertSame('hello', $result->new_field); + } + + public function testFillShouldRetrievePolymorphedModelEvenWithExistingModel() + { + // Set + $input = [ + 'type' => 'polymorphed', + 'new_field' => 'hello', + 'exclusive' => 'value', // should not be set + 'other_exclusive' => 'value from fill', // should not be set + ]; + $model = new ReferencedUser(); + $id = new ObjectId(); + $model->_id = $id; + $model->name = 'Albert'; + $model->other_exclusive = 'other value'; // should be inherited + // Actions + $result = ReferencedUser::fill($input, $model); + + // Assertions + $this->assertInstanceOf(PolymorphedReferencedUser::class, $result); + $this->assertSame('polymorphed', $result->type); + $this->assertSame('hello', $result->new_field); + $this->assertSame($id, $result->_id); + $this->assertSame('Albert', $result->name); + $this->assertNull($result->exclusive); + $this->assertSame('other value', $result->other_exclusive); + } + + public function testFillShouldHoldValuesOnModel() + { + // Set + $input = [ + 'type' => 'regular', + 'new_field' => 'hello', // should not be set + ]; + $model = new ReferencedUser(); + $id = new ObjectId(); + $model->_id = $id; + $model->name = 'Albert'; + // Actions + $result = ReferencedUser::fill($input, $model); + + // Assertions + $this->assertSame($model, $result); + $this->assertSame( + [ + '_id' => $id, + 'name' => 'Albert', + 'type' => 'regular', + ], + $model->getDocumentAttributes() + ); + } + + public function testFillShouldNotHoldValuesOnModelIfPolymorphed() + { + // Set + $input = [ + 'type' => 'polymorphed', + 'new_field' => 'hello', + ]; + $model = new ReferencedUser(); + $id = new ObjectId(); + $model->_id = $id; + $model->name = 'Albert'; + // Actions + $result = ReferencedUser::fill($input, $model); + + // Assertions + $this->assertNotSame($model, $result); + $this->assertSame( + [ + '_id' => $id, + 'name' => 'Albert', + 'type' => 'polymorphed', + 'new_field' => 'hello', + ], + $result->getDocumentAttributes() + ); + $this->assertSame( + [ + '_id' => $id, + 'name' => 'Albert', + ], + $model->getDocumentAttributes() + ); + } + + public function testShouldForceFillAttributes() + { + // Set + $model = new class() implements HasAttributesInterface + { + use HasAttributesTrait; + use HasRelationsTrait; + }; + + $input = [ + 'name' => 'Josh', + 'not_allowed_attribute' => true, + ]; + + // Actions + $model = $model::fill($input, $model, true); + $result = $model->getDocumentAttribute('not_allowed_attribute'); + + // Assertions + $this->assertTrue($result); + } + + public function testShouldBeCastableToArray() + { + // Set + $model = new class() + { + use HasAttributesTrait; + use HasRelationsTrait; + }; + + $model->setDocumentAttribute('name', 'John'); + $model->setDocumentAttribute('age', 25); + + // Actions + $result = $model->toArray(); + + // Assertions + $this->assertSame(['name' => 'John', 'age' => 25], $result); + } + + public function testShouldSetOriginalAttributes() + { + // Set + $model = new class() implements HasAttributesInterface + { + use HasAttributesTrait; + use HasRelationsTrait; + }; + + $model->name = 'John'; + $model->age = 25; + + // Actions + $model->syncOriginalDocumentAttributes(); + $result = $model->getOriginalDocumentAttributes(); + + // Assertions + $this->assertSame($model->getDocumentAttributes(), $result); + } + + public function testShouldFallbackOriginalAttributesIfUnserializationFails() + { + // Set + $model = new class() implements HasAttributesInterface + { + use HasAttributesTrait; + use HasRelationsTrait; + + public function __construct() + { + $this->attributes = [ + function () { + }, + ]; + } + }; + + // Actions + $model->syncOriginalDocumentAttributes(); + $result = $model->getOriginalDocumentAttributes(); + + // Assertions + $this->assertSame($model->getDocumentAttributes(), $result); + } + + public function testShouldCheckIfAttributeIsSet() + { + // Set + $model = new class() extends AbstractModel + { + }; + + // Actions + $model = $model::fill(['name' => 'John', 'ignored' => null]); + + // Assertions + $this->assertTrue(isset($model->name)); + $this->assertFalse(isset($model->nonexistant)); + $this->assertFalse(isset($model->ignored)); + } + + public function testShouldCheckIfMutatedAttributeIsSet() + { + // Set + $model = new class() extends AbstractModel + { + /** + * {@inheritdoc} + */ + public $mutable = true; + + public function getNameDocumentAttribute() + { + return 'John'; + } + }; + + // Assertions + $this->assertTrue(isset($model->name)); + $this->assertFalse(isset($model->nonexistant)); + } + + public function getFillableOptions(): array + { + return [ + '$fillable = []; $guarded = []' => [ + 'fillable' => [], + 'guarded' => [], + 'input' => [ + 'name' => 'John', + 'age' => 25, + 'sex' => 'male', + ], + 'expected' => [ + 'name' => 'John', + 'age' => 25, + 'sex' => 'male', + ], + ], + '$fillable = ["name"]; $guarded = []' => [ + 'fillable' => ['name'], + 'guarded' => [], + 'input' => [ + 'name' => 'John', + 'age' => 25, + 'sex' => 'male', + ], + 'expected' => [ + 'name' => 'John', + ], + ], + '$fillable = []; $guarded = [sex]' => [ + 'fillable' => [], + 'guarded' => ['sex'], + 'input' => [ + 'name' => 'John', + 'age' => 25, + 'sex' => 'male', + ], + 'expected' => [ + 'name' => 'John', + 'age' => 25, + ], + ], + '$fillable = ["name", "sex"]; $guarded = ["sex"]' => [ + 'fillable' => ['name', 'sex'], + 'guarded' => ['sex'], + 'input' => [ + 'name' => 'John', + 'age' => 25, + 'sex' => 'male', + ], + 'expected' => [ + 'name' => 'John', + ], + ], + 'ignore nulls but not falsy ones' => [ + 'fillable' => ['name', 'surname', 'sex', 'age', 'has_sex'], + 'guarded' => [], + 'input' => [ + 'name' => 'John', + 'surname' => '', + 'sex' => null, + 'age' => 0, + 'has_sex' => false, + ], + 'expected' => [ + 'name' => 'John', + 'surname' => '', + 'age' => 0, + 'has_sex' => false, + ], + ], + ]; + } +} diff --git a/tests/Unit/Model/HasRelationsTraitTest.php b/tests/Unit/Model/HasRelationsTraitTest.php new file mode 100644 index 00000000..80485790 --- /dev/null +++ b/tests/Unit/Model/HasRelationsTraitTest.php @@ -0,0 +1,231 @@ +parent_id = $fieldValue; + + $builder = $this->instance(Builder::class, m::mock(Builder::class)->makePartial()); + $expected = new ReferencedUser(); + + // Expectations + $builder->expects() + ->first(m::type(ReferencedUser::class), $expectedQuery, []) + ->andReturn($expected); + + // Actions + $result = $model->parent; + + // Assertions + $this->assertSame($expected, $result); + } + + public function testShouldNotPerformQueryForNullReference() + { + // Set + $model = new ReferencedUser(); + + $builder = $this->instance(Builder::class, m::mock(Builder::class)->makePartial()); + + // Expectations + $builder->expects() + ->first() + ->withAnyArgs() + ->never(); + + // Actions + $result = $model->parent; + + // Assertions + $this->assertNull($result); + } + + /** + * @dataProvider referencesManyScenarios + */ + public function testShouldReferenceMany($fieldValue, array $expectedQuery) + { + // Set + $model = new ReferencedUser(); + $model->siblings_ids = $fieldValue; + + $builder = $this->instance(Builder::class, m::mock(Builder::class)->makePartial()); + $expected = new EmbeddedCursor([]); + + // Expectations + $builder->expects() + ->where(m::type(ReferencedUser::class), $expectedQuery, []) + ->andReturn($expected); + + // Actions + $result = $model->siblings; + + // Assertions + $this->assertSame($expected, $result); + } + + public function testShouldEmbedOne() + { + // Set + $model = new EmbeddedUser(); + + $embeddedModel = new EmbeddedUser(); + $embeddedModel->_id = 12345; + $embeddedModel->name = 'John'; + $embeddedModel->syncOriginalDocumentAttributes(); + + $model->embedded_parent = $embeddedModel; + + // Actions + $result = $model->parent; + + // Assertions + $this->assertInstanceOf(EmbeddedUser::class, $result); + $this->assertSame($embeddedModel, $result); + } + + public function testEmbedOneShouldAllowOnlyOneEmbeddedModel() + { + // Set + $model = new EmbeddedUser(); + + $oldEmbeddedModel = new EmbeddedUser(); + $oldEmbeddedModel->_id = 12345; + $oldEmbeddedModel->name = 'John'; + $oldEmbeddedModel->syncOriginalDocumentAttributes(); + + $newEmbeddedModel = new EmbeddedUser(); + $newEmbeddedModel->_id = 54321; + $newEmbeddedModel->name = 'Bob'; + $newEmbeddedModel->syncOriginalDocumentAttributes(); + + $model->embedded_parent = $oldEmbeddedModel; + + // Actions + $model->parent()->add($newEmbeddedModel); + $result = $model->parent; + + // Assertions + $this->assertInstanceOf(EmbeddedUser::class, $result); + $this->assertSame($newEmbeddedModel, $result); + } + + /** + * @dataProvider embedsManyScenarios + */ + public function testShouldEmbedMany($fieldValue, array $expectedItems) + { + // Set + $model = new EmbeddedUser(); + $model->embedded_siblings = $fieldValue; + + // Actions + $result = $model->siblings; + + // Assertions + $this->assertInstanceOf(EmbeddedCursor::class, $result); + $this->assertContainsOnlyInstancesOf(EmbeddedUser::class, $result->all()); + $this->assertSame($expectedItems, $result->all()); + } + + public function referencesOneScenarios(): array + { + return [ + 'referenced by string id' => [ + 'fieldValue' => 'abc123', + 'expectedQuery' => ['_id' => 'abc123'], + ], + 'referenced by objectId represented as string' => [ + 'fieldValue' => '577afb0b4d3cec136058fa82', + 'expectedQuery' => ['_id' => new ObjectId('577afb0b4d3cec136058fa82')], + ], + 'referenced by an objectId itself' => [ + 'fieldValue' => new ObjectId('577afb0b4d3cec136058fa82'), + 'expectedQuery' => ['_id' => new ObjectId('577afb0b4d3cec136058fa82')], + ], + ]; + } + + public function referencesManyScenarios(): array + { + return [ + 'referenced by string id' => [ + 'fieldValue' => 'abc123', + 'expectedQuery' => ['_id' => ['$in' => ['abc123']]], + ], + 'referenced by objectId represented as string' => [ + 'fieldValue' => '577afb0b4d3cec136058fa82', + 'expectedQuery' => ['_id' => ['$in' => [new ObjectId('577afb0b4d3cec136058fa82')]]], + ], + 'referenced by an objectId itself' => [ + 'fieldValue' => new ObjectId('577afb0b4d3cec136058fa82'), + 'expectedQuery' => ['_id' => ['$in' => [new ObjectId('577afb0b4d3cec136058fa82')]]], + ], + 'series of objectIds' => [ + 'fieldValue' => [new ObjectId('577afb0b4d3cec136058fa82'), new ObjectId('577afb7e4d3cec136258fa83')], + 'expectedQuery' => [ + '_id' => [ + '$in' => [ + new ObjectId('577afb0b4d3cec136058fa82'), + new ObjectId('577afb7e4d3cec136258fa83'), + ], + ], + ], + ], + 'series of objectIds as strings' => [ + 'fieldValue' => ['577afb0b4d3cec136058fa82', '577afb7e4d3cec136258fa83'], + 'expectedQuery' => [ + '_id' => [ + '$in' => [ + new ObjectId('577afb0b4d3cec136058fa82'), + new ObjectId('577afb7e4d3cec136258fa83'), + ], + ], + ], + ], + 'Model referenced with null' => [ + 'fieldValue' => null, + 'expectedQuery' => ['_id' => ['$in' => []]], + ], + ]; + } + + public function embedsManyScenarios(): array + { + $model1 = new EmbeddedUser(); + $model1->_id = 12345; + $model1->name = 'John'; + $model1->syncOriginalDocumentAttributes(); + + $model2 = new EmbeddedUser(); + $model2->_id = 67890; + $model2->name = 'Bob'; + $model2->syncOriginalDocumentAttributes(); + + return [ + 'A single embedded document' => [ + 'fieldValue' => $model1, + 'expectedItems' => [$model1], + ], + 'Many embedded documents' => [ + 'fieldValue' => [$model1, $model2], + 'expectedItems' => [$model1, $model2], + ], + ]; + } +} diff --git a/tests/Unit/Query/BuilderTest.php b/tests/Unit/Query/BuilderTest.php new file mode 100644 index 00000000..c7d88109 --- /dev/null +++ b/tests/Unit/Query/BuilderTest.php @@ -0,0 +1,852 @@ +assertAttributeSame($connection, 'connection', $builder); + } + + /** + * @dataProvider getWriteConcernVariations + */ + public function testShouldSave(ReplaceCollectionModel $model, int $writeConcern, bool $shouldFireEventAfter, bool $expected) + { + // Set + $connection = m::mock(Connection::class); + $builder = new Builder($connection); + $options = ['writeConcern' => new WriteConcern($writeConcern)]; + + $collection = m::mock(Collection::class); + $operationResult = m::mock(); + + $model->setCollection($collection); + + // Expectations + $collection->expects() + ->replaceOne( + ['_id' => 123], + $model, + ['upsert' => true, 'writeConcern' => new WriteConcern($writeConcern)] + )->andReturn($operationResult); + + $operationResult->expects() + ->isAcknowledged() + ->andReturn((bool) $writeConcern); + + $operationResult->allows() + ->getModifiedCount() + ->andReturn(1); + + $operationResult->allows() + ->getUpsertedCount() + ->andReturn(1); + + $this->expectEventToBeFired('saving', $model, true); + + if ($shouldFireEventAfter) { + $this->expectEventToBeFired('saved', $model, false); + } else { + $this->expectEventNotToBeFired('saved', $model); + } + + // Actions + $result = $builder->save($model, $options); + + // Assertions + $this->assertSame($expected, $result); + } + + /** + * @dataProvider getWriteConcernVariations + */ + public function testShouldInsert(ReplaceCollectionModel $model, int $writeConcern, bool $shouldFireEventAfter, bool $expected) + { + // Set + $connection = m::mock(Connection::class); + $builder = new Builder($connection); + $options = ['writeConcern' => new WriteConcern($writeConcern)]; + + $collection = m::mock(Collection::class); + $operationResult = m::mock(); + + $model->setCollection($collection); + $model->_id = null; + + // Expectations + $collection->expects() + ->insertOne($model, ['writeConcern' => new WriteConcern($writeConcern)]) + ->andReturn($operationResult); + + $operationResult->expects() + ->isAcknowledged() + ->andReturn((bool) $writeConcern); + + $operationResult->allows() + ->getInsertedCount() + ->andReturn(1); + + $this->expectEventToBeFired('inserting', $model, true); + + if ($shouldFireEventAfter) { + $this->expectEventToBeFired('inserted', $model, false); + } else { + $this->expectEventNotToBeFired('inserted', $model); + } + + // Actions + $result = $builder->insert($model, $options); + + // Assertions + $this->assertSame($expected, $result); + } + + /** + * @dataProvider getWriteConcernVariations + */ + public function testShouldInsertWithoutFiringEvents( + ReplaceCollectionModel $model, + int $writeConcern, + bool $shouldFireEventAfter, + bool $expected + ) { + // Set + $connection = m::mock(Connection::class); + $builder = new Builder($connection); + $options = ['writeConcern' => new WriteConcern($writeConcern)]; + + $collection = m::mock(Collection::class); + $operationResult = m::mock(); + + $model->setCollection($collection); + $model->_id = null; + + // Expectations + $collection->expects() + ->insertOne($model, ['writeConcern' => new WriteConcern($writeConcern)]) + ->andReturn($operationResult); + + $operationResult->expects() + ->isAcknowledged() + ->andReturn((bool) $writeConcern); + + $operationResult->allows() + ->getInsertedCount() + ->andReturn(1); + + $this->expectEventNotToBeFired('inserting', $model); + $this->expectEventNotToBeFired('inserted', $model); + + // Actions + $result = $builder->insert($model, $options, false); + + // Assertions + $this->assertSame($expected, $result); + } + + /** + * @dataProvider getWriteConcernVariations + */ + public function testShouldUpdate(ReplaceCollectionModel $model, int $writeConcern, bool $shouldFireEventAfter, bool $expected) + { + // Set + $connection = m::mock(Connection::class); + $builder = new Builder($connection); + + $collection = m::mock(Collection::class); + $parsedObject = ['_id' => 123]; + $operationResult = m::mock(); + $options = ['writeConcern' => new WriteConcern($writeConcern)]; + + $model->setCollection($collection); + + // Expectations + $collection->expects() + ->updateOne( + ['_id' => 123], + ['$set' => $parsedObject], + $options + )->andReturn($operationResult); + + $operationResult->expects() + ->isAcknowledged() + ->andReturn((bool) $writeConcern); + + $operationResult->allows() + ->getModifiedCount() + ->andReturn(1); + + $this->expectEventToBeFired('updating', $model, true); + + if ($shouldFireEventAfter) { + $this->expectEventToBeFired('updated', $model, false); + } else { + $this->expectEventNotToBeFired('updated', $model); + } + + // Actions + $result = $builder->update($model, $options); + + // Assertions + $this->assertSame($expected, $result); + } + + public function testShouldUpdateUnsettingFields() + { + // Set + $connection = m::mock(Connection::class); + $builder = new Builder($connection); + + $model = new class() extends ReplaceCollectionModel + { + /** + * {@inheritdoc} + */ + public $fillable = [ + 'name', + 'unchanged', + ]; + + /** + * {@inheritdoc} + */ + protected $dynamic = false; + }; + $collection = m::mock(Collection::class); + $operationResult = m::mock(); + $options = ['writeConcern' => new WriteConcern(1)]; + $model->setCollection($collection); + + $model->unchanged = 'unchanged'; + $model->notOnFillable = 'to be deleted'; + $model->name = 'John'; + $model->syncOriginalDocumentAttributes(); + $model->_id = 123; + unset($model->name); + + // Expectations + $collection->expects() + ->updateOne( + ['_id' => 123], + ['$set' => ['_id' => 123], '$unset' => ['name' => '', 'notOnFillable' => '']], + $options + )->andReturn($operationResult); + + $operationResult->expects() + ->isAcknowledged() + ->andReturn(true); + + $operationResult->allows() + ->getModifiedCount() + ->andReturn(1); + + $this->expectEventToBeFired('updating', $model, true); + + $this->expectEventToBeFired('updated', $model, false); + + // Actions + $result = $builder->update($model, $options); + + // Assertions + $this->assertTrue($result); + } + + public function testUpdateShouldCalculateChangesAccordingly() + { + // Set + $connection = m::mock(Connection::class); + $builder = new Builder($connection); + + $model = new class() extends ReplaceCollectionModel + { + }; + $collection = m::mock(Collection::class); + $operationResult = m::mock(); + $options = ['writeConcern' => new WriteConcern(1)]; + $model->setCollection($collection); + + $model->unchanged = 'unchanged'; + $model->name = 'John'; + $model->surname = 'Doe'; + $model->addresses = ['1 Blue Street']; + $model->syncOriginalDocumentAttributes(); + $model->_id = 123; + unset($model->name); + $model->surname = ['Doe', 'Jr']; + $model->addresses = ['1 Blue Street', '2 Green Street']; + + // Expectations + $collection->expects() + ->updateOne( + ['_id' => 123], + [ + '$set' => ['_id' => 123, 'surname' => ['Doe', 'Jr'], 'addresses.1' => '2 Green Street'], + '$unset' => ['name' => ''], + ], + $options + )->andReturn($operationResult); + + $operationResult->expects() + ->isAcknowledged() + ->andReturn(true); + + $operationResult->allows() + ->getModifiedCount() + ->andReturn(1); + + $this->expectEventToBeFired('updating', $model, true); + + $this->expectEventToBeFired('updated', $model, false); + + // Actions + $result = $builder->update($model, $options); + + // Assertions + $this->assertTrue($result); + } + + /** + * @dataProvider getWriteConcernVariations + */ + public function testUpdateShouldCallInsertWhenObjectHasNoId( + ReplaceCollectionModel $model, + int $writeConcern, + bool $shouldFireEventAfter, + bool $expected + ) { + // Set + $connection = m::mock(Connection::class); + $builder = new Builder($connection); + + $collection = m::mock(Collection::class); + $operationResult = m::mock(); + $options = ['writeConcern' => new WriteConcern($writeConcern)]; + + $model->setCollection($collection); + $model->_id = null; + + // Actions + $collection->expects() + ->insertOne( + $model, + ['writeConcern' => new WriteConcern($writeConcern)] + )->andReturn($operationResult); + + $operationResult->expects() + ->isAcknowledged() + ->andReturn((bool) $writeConcern); + + $operationResult->allows() + ->getInsertedCount() + ->andReturn(1); + + $this->expectEventToBeFired('updating', $model, true); + + if ($shouldFireEventAfter) { + $this->expectEventToBeFired('updated', $model, false); + } else { + $this->expectEventNotToBeFired('updated', $model); + } + + $this->expectEventNotToBeFired('inserting', $model); + $this->expectEventNotToBeFired('inserted', $model); + + // Actions + $result = $builder->update($model, $options); + + // Assertions + $this->assertSame($expected, $result); + } + + /** + * @dataProvider getWriteConcernVariations + */ + public function testShouldDelete(ReplaceCollectionModel $model, int $writeConcern, bool $shouldFireEventAfter, bool $expected) + { + // Set + $connection = m::mock(Connection::class); + $builder = new Builder($connection); + + $collection = m::mock(Collection::class); + $operationResult = m::mock(); + $options = ['writeConcern' => new WriteConcern($writeConcern)]; + + $model->setCollection($collection); + + // Expectations + $collection->expects() + ->deleteOne(['_id' => 123], ['writeConcern' => new WriteConcern($writeConcern)]) + ->andReturn($operationResult); + + $operationResult->expects() + ->isAcknowledged() + ->andReturn((bool) $writeConcern); + + $operationResult->allows() + ->getDeletedCount() + ->andReturn(1); + + $this->expectEventToBeFired('deleting', $model, true); + + if ($shouldFireEventAfter) { + $this->expectEventToBeFired('deleted', $model, false); + } else { + $this->expectEventNotToBeFired('deleted', $model); + } + + // Actions + $result = $builder->delete($model, $options); + + // Assertions + $this->assertSame($expected, $result); + } + + /** + * @dataProvider eventsToBailOperations + */ + public function testDatabaseOperationsShouldBailOutIfTheEventHandlerReturnsFalse( + string $operation, + string $dbOperation, + string $eventName + ) { + // Set + $connection = m::mock(Connection::class); + $builder = m::mock(Builder::class.'[getCollection]', [$connection]); + $collection = m::mock(Collection::class); + $model = m::mock(ModelInterface::class); + + $builder->shouldAllowMockingProtectedMethods(); + + // Expectations + $builder->allows() + ->getCollection($model) + ->andReturn($collection); + + $collection->expects($dbOperation) + ->never(); + + /* "Mocks" the fireEvent to return false and bail the operation */ + $this->expectEventToBeFired($eventName, $model, true, false); + + // Actions + $result = $builder->$operation($model); + + // Assertions + $this->assertFalse($result); + } + + public function testShouldGetWithWhereQuery() + { + // Set + $connection = m::mock(Connection::class); + $builder = m::mock(Builder::class.'[prepareValueQuery]', [$connection]); + $builder->shouldAllowMockingProtectedMethods(); + + $collection = m::mock(Collection::class); + $model = new ReplaceCollectionModel(); + $model->setCollection($collection); + $query = 123; + $preparedQuery = ['_id' => 123]; + $projection = ['project' => true, '_id' => false, '__pclass' => true]; + + // Expectations + $builder->expects() + ->prepareValueQuery($query) + ->andReturn($preparedQuery); + + // Actions + $result = $builder->where($model, $query, $projection); + + // Assertions + $this->assertInstanceOf(Cursor::class, $result); + $this->assertAttributeSame($collection, 'collection', $result); + $this->assertAttributeSame('find', 'command', $result); + $this->assertAttributeSame( + [$preparedQuery, ['projection' => $projection]], + 'params', + $result + ); + } + + public function testShouldGetAll() + { + // Set + $connection = m::mock(Connection::class); + $builder = m::mock(Builder::class.'[where]', [$connection]); + $mongolidCursor = m::mock(Cursor::class); + $model = m::mock(ModelInterface::class); + + // Expectations + $builder->expects() + ->where($model, []) + ->andReturn($mongolidCursor); + + // Actions + $result = $builder->all($model); + + // Assertions + $this->assertSame($mongolidCursor, $result); + } + + public function testShouldGetFirstWithQuery() + { + // Set + $connection = m::mock(Connection::class); + $builder = m::mock(Builder::class.'[prepareValueQuery]', [$connection]); + $builder->shouldAllowMockingProtectedMethods(); + $collection = m::mock(Collection::class); + $query = 123; + $preparedQuery = ['_id' => 123]; + $model = new ReplaceCollectionModel(); + $model->setCollection($collection); + + // Expectations + $builder->expects() + ->prepareValueQuery($query) + ->andReturn($preparedQuery); + + $collection->expects() + ->findOne($preparedQuery, ['projection' => []]) + ->andReturn($model); + + // Actions + $result = $builder->first($model, $query); + + // Assertions + $this->assertSame($model, $result); + } + + public function testFirstWithNullShouldNotHitTheDatabase() + { + // Set + $connection = m::mock(Connection::class); + $builder = new Builder($connection); + + // Actions + $result = $builder->first(m::mock(ModelInterface::class), null); + + // Assertions + $this->assertNull($result); + } + + public function testFirstOrFailShouldGetFirst() + { + // Set + $connection = m::mock(Connection::class); + $builder = m::mock(Builder::class.'[prepareValueQuery]', [$connection]); + $builder->shouldAllowMockingProtectedMethods(); + $collection = m::mock(Collection::class); + $query = 123; + $preparedQuery = ['_id' => 123]; + $model = new ReplaceCollectionModel(); + $model->setCollection($collection); + + // Expectations + $builder->expects() + ->prepareValueQuery($query) + ->andReturn($preparedQuery); + + $collection->expects() + ->findOne($preparedQuery, ['projection' => []]) + ->andReturn($model); + + // Actions + $result = $builder->firstOrFail($model, $query); + + // Assertions + $this->assertSame($model, $result); + } + + public function testFirstOrFailWithNullShouldFail() + { + // Set + $connection = m::mock(Connection::class); + $builder = new Builder($connection); + $model = new class extends AbstractModel + { + }; + + // Expectations + $this->expectException(ModelNotFoundException::class); + $this->expectExceptionMessage('No query results for model ['.get_class($model).'].'); + + // Actions + $builder->firstOrFail($model, null); + } + + public function testShouldGetNullIfFirstCantFindAnything() + { + // Set + $connection = m::mock(Connection::class); + $builder = m::mock(Builder::class.'[prepareValueQuery]', [$connection]); + $builder->shouldAllowMockingProtectedMethods(); + $collection = m::mock(Collection::class); + $query = 123; + $preparedQuery = ['_id' => 123]; + $model = new ReplaceCollectionModel(); + $model->setCollection($collection); + + // Expectations + $builder->expects() + ->prepareValueQuery($query) + ->andReturn($preparedQuery); + + $collection->expects() + ->findOne($preparedQuery, ['projection' => []]) + ->andReturn(null); + + // Actions + $result = $builder->first($model, $query); + + // Assertions + $this->assertNull($result); + } + + public function testShouldGetFirstProjectingFields() + { + // Set + $connection = m::mock(Connection::class); + $builder = m::mock(Builder::class.'[prepareValueQuery]', [$connection]); + $builder->shouldAllowMockingProtectedMethods(); + + $collection = m::mock(Collection::class); + $query = 123; + $preparedQuery = ['_id' => 123]; + $projection = ['project' => true, 'fields' => false, '__pclass' => true]; + $model = new ReplaceCollectionModel(); + $model->setCollection($collection); + + // Expectations + $builder->expects() + ->prepareValueQuery($query) + ->andReturn($preparedQuery); + + $collection->expects() + ->findOne($preparedQuery, ['projection' => $projection]) + ->andReturn(null); + + // Actions + $result = $builder->first($model, $query, $projection); + + // Assertions + $this->assertNull($result); + } + + /** + * @dataProvider queryValueScenarios + */ + public function testShouldPrepareQueryValue($value, $expectation) + { + // Set + $connection = m::mock(Connection::class); + $builder = new Builder($connection); + + // Actions + $result = $this->callProtected($builder, 'prepareValueQuery', [$value]); + + // Assertions + $this->assertEquals($expectation, $result, 'Queries are not equals'); + } + + /** + * @dataProvider getProjections + */ + public function testPrepareProjectionShouldConvertArray($data, $expectation) + { + // Set + $connection = m::mock(Connection::class); + $builder = new Builder($connection); + + // Actions + $result = $this->callProtected($builder, 'prepareProjection', [$data]); + + // Assertions + $this->assertSame($expectation, $result); + } + + public function testPrepareProjectionShouldThrownAnException() + { + // Set + $connection = m::mock(Connection::class); + $builder = new Builder($connection); + $data = ['valid' => true, 'invalid-key' => 'invalid-value']; + + // Expectations + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Invalid projection: 'invalid-key' => 'invalid-value'"); + + // Actions + $this->callProtected($builder, 'prepareProjection', [$data]); + } + + public function eventsToBailOperations(): array + { + return [ + 'Saving event' => [ + 'operation' => 'save', + 'dbOperation' => 'replaceOne', + 'eventName' => 'saving', + ], + 'Inserting event' => [ + 'operation' => 'insert', + 'dbOperation' => 'insertOne', + 'eventName' => 'inserting', + ], + 'Updating event' => [ + 'operation' => 'update', + 'dbOperation' => 'updateOne', + 'eventName' => 'updating', + ], + 'Deleting event' => [ + 'operation' => 'delete', + 'dbOperation' => 'deleteOne', + 'eventName' => 'deleting', + ], + ]; + } + + public function queryValueScenarios(): array + { + return [ + 'An array' => [ + 'value' => ['age' => ['$gt' => 25]], + 'expectation' => ['age' => ['$gt' => 25]], + ], + 'An ObjectId string' => [ + 'value' => '507f1f77bcf86cd799439011', + 'expectation' => ['_id' => new ObjectId('507f1f77bcf86cd799439011')], + ], + 'An ObjectId string within a query' => [ + 'value' => ['_id' => '507f1f77bcf86cd799439011'], + 'expectation' => ['_id' => new ObjectId('507f1f77bcf86cd799439011')], + ], + 'Other type of _id, sequence for example' => [ + 'value' => 7, + 'expectation' => ['_id' => 7], + ], + 'Series of string _ids as the $in parameter' => [ + 'value' => ['_id' => ['$in' => ['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012']]], + 'expectation' => [ + '_id' => [ + '$in' => [ + new ObjectId('507f1f77bcf86cd799439011'), + new ObjectId('507f1f77bcf86cd799439012'), + ], + ], + ], + ], + 'Series of string _ids as the $nin parameter' => [ + 'value' => ['_id' => ['$nin' => ['507f1f77bcf86cd799439011']]], + 'expectation' => ['_id' => ['$nin' => [new ObjectId('507f1f77bcf86cd799439011')]]], + ], + ]; + } + + public function getWriteConcernVariations(): array + { + $model = new ReplaceCollectionModel(); + $model2 = new ReplaceCollectionModel(); + $model->_id = 123; + $model2->_id = 123; + + return [ + 'acknowledged write concern' => [ + 'object' => $model, + 'writeConcern' => 1, + 'shouldFireEventAfter' => true, + 'expected' => true, + ], + 'unacknowledged write concern' => [ + 'object' => $model2, + 'writeConcern' => 0, + 'shouldFireEventAfter' => false, + 'expected' => false, + ], + ]; + } + + /** + * Retrieves projections that should be replaced by mapper. + */ + public function getProjections(): array + { + return [ + 'Should return self array' => [ + 'projection' => ['some' => true, 'fields' => false], + 'expected' => ['some' => true, 'fields' => false, '__pclass' => true], + ], + 'Should convert number' => [ + 'projection' => ['some' => 1, 'fields' => -1], + 'expected' => ['some' => true, 'fields' => false, '__pclass' => true], + ], + 'Should add true in fields' => [ + 'projection' => ['some', 'fields'], + 'expected' => ['some' => true, 'fields' => true, '__pclass' => true], + ], + 'Should add boolean values according to key value' => [ + 'projection' => ['-some', 'fields'], + 'expected' => ['some' => false, 'fields' => true, '__pclass' => true], + ], + 'Should not exclude __pclass from projection' => [ + 'projection' => ['fields' => true, '__pclass' => false], + 'expected' => ['fields' => true, '__pclass' => true], + ], + 'Empty should not include __pclass' => [ + 'projection' => [], + 'expected' => [], + ], + ]; + } + + protected function getEventService(): EventTriggerService + { + if (!Container::has(EventTriggerService::class)) { + Container::instance(EventTriggerService::class, m::mock(EventTriggerService::class)); + } + + return Container::make(EventTriggerService::class); + } + + protected function expectEventToBeFired(string $event, ModelInterface $model, bool $halt, bool $return = true): void + { + $event = 'mongolid.'.$event.': '.get_class($model); + + $this->getEventService() + ->expects() + ->fire($event, $model, $halt) + ->andReturn($return); + } + + protected function expectEventNotToBeFired(string $event, ModelInterface $model): void + { + $event = 'mongolid.'.$event.': '.get_class($model); + + $this->getEventService() + ->expects() + ->fire($event, $model, m::any()) + ->never(); + } +} diff --git a/tests/Unit/Query/ModelMapperTest.php b/tests/Unit/Query/ModelMapperTest.php new file mode 100644 index 00000000..135dba5d --- /dev/null +++ b/tests/Unit/Query/ModelMapperTest.php @@ -0,0 +1,215 @@ +_id = 1; + $model->name = 'John'; + $model->age = 23; + $model->location = 'Brazil'; + $model->created_at = new UTCDateTime(); // `$model->timestamps` is false! + + // Actions + $result = $modelMapper->map($model, ['name', 'age'], false, false); + + // Assertions + $this->assertSame( + [ + '_id' => 1, + 'name' => 'John', + 'age' => 23, + ], + $result + ); + } + + public function testShouldClearDynamicFieldsIfModelIsNotDynamicCheckingTimestamps() + { + // Set + $model = new class extends AbstractModel + { + }; + $modelMapper = new ModelMapper(); + $model->_id = 1; + $model->name = 'John'; + $model->age = 23; + $model->location = 'Brazil'; + $dateTime = new UTCDateTime(); + $model->created_at = $dateTime; // `$model->timestamps` is false! + + // Actions + $result = $modelMapper->map($model, ['name', 'age'], false, true); + + // Assertions + $this->assertSame( + [ + '_id' => 1, + 'name' => 'John', + 'age' => 23, + 'created_at' => $dateTime, + 'updated_at' => $model->updated_at, + ], + $result + ); + } + + public function testShouldNotClearDynamicFieldsIfModelIsDynamic() + { + // Set + $model = new class extends AbstractModel + { + }; + + $modelMapper = new ModelMapper(); + $model->_id = 1; + $model->name = 'John'; + $model->age = 23; + $model->location = 'Brazil'; + + // Actions + $result = $modelMapper->map($model, ['name', 'age'], true, false); + + // Assertions + $this->assertSame( + [ + '_id' => 1, + 'name' => 'John', + 'age' => 23, + 'location' => 'Brazil', + ], + $result + ); + } + + public function testShouldClearNullFields() + { + // Set + $model = new class extends AbstractModel + { + }; + + $modelMapper = new ModelMapper(); + $model->_id = 1; + $model->name = 'John'; + $model->age = null; + $model->location = null; + + // Actions + $result = $modelMapper->map($model, ['name', 'age'], true, false); + + // Assertions + $this->assertSame( + [ + '_id' => 1, + 'name' => 'John', + ], + $result + ); + } + + public function testShouldGenerateAnIdIfModelDoesNotHaveOne() + { + // Set + $model = new class extends AbstractModel + { + }; + + $modelMapper = new ModelMapper(); + $model->name = 'John'; + + // Actions + $result = $modelMapper->map($model, [], true, true); + + // Assertions + $this->assertSame('John', $result['name']); + $this->assertInstanceOf(ObjectId::class, $result['_id']); + $this->assertInstanceOf(ObjectId::class, $model->_id); + } + + public function testShouldCastObjectId() + { + // Set + $model = new class extends AbstractModel + { + }; + + $modelMapper = new ModelMapper(); + $id = '5bfd396038b5fa0001462681'; + $model->_id = $id; + + // Actions + $result = $modelMapper->map($model, [], true, true); + + // Assertions + $this->assertInstanceOf(ObjectId::class, $result['_id']); + $this->assertSame($model->_id, $result['_id']); + $this->assertEquals(new ObjectId($id), $model->_id); + } + + public function testShouldHandleTimestampsCreatingCreatedAtField() + { + // Set + $model = new class extends AbstractModel + { + }; + + $modelMapper = new ModelMapper(); + $model->_id = 1; + $model->name = 'John'; + + // Actions + $result = $modelMapper->map($model, ['name', 'age'], true, true); + + // Assertions + $this->assertSame('John', $result['name']); + $this->assertSame(1, $result['_id']); + $this->assertInstanceOf(UTCDateTime::class, $result['created_at']); + $this->assertInstanceOf(UTCDateTime::class, $result['updated_at']); + $this->assertSame($model->created_at, $result['created_at']); + $this->assertSame($model->updated_at, $result['updated_at']); + $this->assertSame($model->updated_at, $model->created_at); + } + + public function testShouldHandleTimestampsOnlyUpdatingUpdatedAtField() + { + // Set + $model = new class extends AbstractModel + { + }; + + $modelMapper = new ModelMapper(); + $model->_id = 1; + $model->name = 'John'; + $createdAt = new UTCDateTime(new DateTime('-2 hour')); + $updatedAt = new UTCDateTime(new DateTime('-1 hour')); + $model->created_at = $createdAt; + $model->updated_at = $updatedAt; + + // Actions + $result = $modelMapper->map($model, ['name', 'age'], true, true); + + // Assertions + $this->assertSame('John', $result['name']); + $this->assertSame(1, $result['_id']); + $this->assertInstanceOf(UTCDateTime::class, $result['created_at']); + $this->assertInstanceOf(UTCDateTime::class, $result['updated_at']); + $this->assertSame($model->created_at, $result['created_at']); + $this->assertSame($model->updated_at, $result['updated_at']); + $this->assertSame($createdAt, $model->created_at); + $this->assertNotSame($updatedAt, $model->updated_at); + $this->assertGreaterThan($updatedAt, $model->updated_at); + } +} diff --git a/tests/Unit/TestCase.php b/tests/Unit/TestCase.php new file mode 100644 index 00000000..f42fb490 --- /dev/null +++ b/tests/Unit/TestCase.php @@ -0,0 +1,88 @@ +setAccessible(true); + + return $methodObj->invokeArgs($obj, $args); + } + + /** + * Set a protected property of an object. + * + * @param mixed $obj object Instance + * @param string $property property name + * @param mixed $value value to be set + */ + protected function setProtected($obj, $property, $value): void + { + $class = new ReflectionClass($obj); + $property = $class->getProperty($property); + $property->setAccessible(true); + $property->setValue($obj, $value); + } + + /** + * Get a protected property of an object. + * + * @param mixed $obj object Instance + * @param string $property property name + * + * @return mixed property value + */ + protected function getProtected($obj, $property) + { + $class = new ReflectionClass($obj); + $property = $class->getProperty($property); + $property->setAccessible(true); + return $property->getValue($obj); + } + + /** + * Replace instance on Ioc + */ + protected function instance(string $abstract, $instance) + { + Container::bind( + $abstract, + function () use ($instance) { + return $instance; + } + ); + + return $instance; + } +} diff --git a/tests/Unit/Util/LocalDateTimeTest.php b/tests/Unit/Util/LocalDateTimeTest.php new file mode 100644 index 00000000..50430a38 --- /dev/null +++ b/tests/Unit/Util/LocalDateTimeTest.php @@ -0,0 +1,97 @@ +date = new DateTime('01/05/2017 15:40:00'); + $this->date->setTimezone(new DateTimeZone('UTC')); + + date_default_timezone_set('America/Sao_Paulo'); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() + { + unset($this->date); + parent::tearDown(); + } + + public function testGetShouldRetrievesDateUsingTimezone() + { + // Set + $date = new UTCDateTime($this->date); + + // Actions + $result = LocalDateTime::get($date); + + // Assertions + $this->assertEquals($this->date, $result); + } + + public function testFormatShouldRetrievesDateWithDefaultFormat() + { + // Set + $timezone = new DateTimeZone(date_default_timezone_get()); + $this->date->setTimezone($timezone); + + // Actions + $result = LocalDateTime::format(new UTCDateTime($this->date)); + + // Assertions + $this->assertSame($this->date->format($this->format), $result); + } + + public function testFormatShouldRetrieveDateUsingGivenFormat() + { + // Set + $timezone = new DateTimeZone(date_default_timezone_get()); + $this->date->setTimezone($timezone); + $format = 'Y-m-d H:i:s'; + + // Actions + $result = LocalDateTime::format(new UTCDateTime($this->date), $format); + + // Assertions + $this->assertSame($this->date->format($format), $result); + } + + public function testTimestampShouldRetrievesTimestampUsingTimezone() + { + // Set + $timestamp = $this->date->getTimestamp(); + $date = new UTCDateTime($this->date); + + // Actions + $mongoDateTimestamp = LocalDateTime::timestamp($date); + + // Assertions + $this->assertSame( + DateTime::createFromFormat($timestamp, $this->format), + DateTime::createFromFormat($mongoDateTimestamp, $this->format) + ); + } +} diff --git a/tests/Unit/Util/ObjectIdUtilsTest.php b/tests/Unit/Util/ObjectIdUtilsTest.php new file mode 100644 index 00000000..580e8b76 --- /dev/null +++ b/tests/Unit/Util/ObjectIdUtilsTest.php @@ -0,0 +1,53 @@ +assertSame($expectation, $result); + } + + public function objectIdStringScenarios(): array + { + $object = new class { + public function __toString() + { + return '577a68c44d3cec1f6c7796a2'; + } + }; + + return [ + ['value' => '577a68c44d3cec1f6c7796a2', 'expectation' => true], + ['value' => '577a68d24d3cec1f817796a5', 'expectation' => true], + ['value' => '577a68d14d3cec1f6d7796a3', 'expectation' => true], + ['value' => '507f1f77bcf86cd799439011', 'expectation' => true], + ['value' => '507f191e810c19729de860ea', 'expectation' => true], + ['value' => $object, 'expectation' => true], + ['value' => new ObjectId(), 'expectation' => true], + ['value' => new ObjectId('577a68c44d3cec1f6c7796a2'), 'expectation' => true], + ['value' => 1, 'expectation' => false], + ['value' => '507f191e810c197', 'expectation' => false], + ['value' => 123456, 'expectation' => false], + ['value' => 'abcdefgh1234567890123456', 'expectation' => false], + ['value' => '+07f191e810c19729de860ea', 'expectation' => false], + ['value' => 1234567, 'expectation' => false], + ['value' => 0.5, 'expectation' => false], + ['value' => null, 'expectation' => false], + ['value' => true, 'expectation' => false], + ['value' => false, 'expectation' => false], + ['value' => ['key' => 'value'], 'expectation' => false], + ['value' => ['577a68c44d3cec1f6c7796a2'], 'expectation' => false], + ]; + } +} diff --git a/tests/Unit/Util/SequenceServiceTest.php b/tests/Unit/Util/SequenceServiceTest.php new file mode 100644 index 00000000..e0101e5c --- /dev/null +++ b/tests/Unit/Util/SequenceServiceTest.php @@ -0,0 +1,92 @@ +shouldAllowMockingProtectedMethods(); + $rawCollection = m::mock(Collection::class); + + // Expectations + $sequenceService->expects() + ->rawCollection() + ->andReturn($rawCollection); + + $rawCollection->expects() + ->findOneAndUpdate( + ['_id' => $sequenceName], + ['$inc' => ['seq' => 1]], + ['upsert' => true] + )->andReturn( + $currentValue ? (object) ['seq' => $currentValue] : null + ); + + // Actions + $result = $sequenceService->getNextValue($sequenceName); + + // Assertions + $this->assertSame($expectation, $result); + } + + public function testShouldGetClient() + { + // Set + $connection = m::mock(Connection::class); + $connection->defaultDatabase = 'production'; + + $sequenceService = new SequenceService($connection, 'foobar'); + $collection = m::mock(Collection::class); + + $client = m::mock(Client::class); + $database = m::mock(Database::class); + + // Expectations + $connection->expects() + ->getClient() + ->andReturn($client); + + $client->expects() + ->selectDatabase('production') + ->andReturn($database); + + $database->expects() + ->selectCollection('foobar') + ->andReturn($collection); + + // Actions + $result = $this->callProtected($sequenceService, 'rawCollection'); + + // Assertions + $this->assertSame($collection, $result); + } + + public function sequenceScenarios(): array + { + return [ + 'New sequence in collection "products"' => [ + 'sequenceName' => 'products', + 'currentValue' => 0, + 'expectation' => 1, + ], + 'Existing sequence in collection "unicorns"' => [ + 'sequenceName' => 'unicorns', + 'currentValue' => 7, + 'expectation' => 8, + ], + ]; + } +} diff --git a/tests/Util/DropDatabaseTrait.php b/tests/Util/DropDatabaseTrait.php new file mode 100644 index 00000000..1201f700 --- /dev/null +++ b/tests/Util/DropDatabaseTrait.php @@ -0,0 +1,16 @@ +getClient() + ->dropDatabase($connection->defaultDatabase); + } +} diff --git a/tests/Util/SetupConnectionTrait.php b/tests/Util/SetupConnectionTrait.php new file mode 100644 index 00000000..19a2582f --- /dev/null +++ b/tests/Util/SetupConnectionTrait.php @@ -0,0 +1,21 @@ +defaultDatabase = $database; + + return $connection; + } + ); + } +}