Repository: mongodb/laravel-mongodb
Branch: 5.x
Commit: 0071a45e7974
Files: 171
Total size: 872.5 KB
Directory structure:
gitextract_2jedx5y6/
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── BUG_REPORT.md
│ │ ├── FEATURE-REQUEST.md
│ │ └── config.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── dependabot.yml
│ ├── labeler.yml
│ ├── release.yml
│ └── workflows/
│ ├── build-ci-atlas.yml
│ ├── build-ci.yml
│ ├── coding-standards.yml
│ ├── labeler.yml
│ ├── merge-up.yml
│ ├── release.yml
│ └── static-analysis.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── RELEASING.md
├── composer.json
├── docker-compose.yml
├── phpcs.xml.dist
├── phpstan-baseline.neon
├── phpstan.neon.dist
├── phpunit.xml.dist
├── rector.php
├── sbom.json
├── src/
│ ├── Auth/
│ │ └── User.php
│ ├── Bus/
│ │ └── MongoBatchRepository.php
│ ├── Cache/
│ │ ├── MongoLock.php
│ │ └── MongoStore.php
│ ├── CommandSubscriber.php
│ ├── Concerns/
│ │ └── ManagesTransactions.php
│ ├── Connection.php
│ ├── Eloquent/
│ │ ├── Builder.php
│ │ ├── Casts/
│ │ │ ├── BinaryUuid.php
│ │ │ └── ObjectId.php
│ │ ├── DocumentModel.php
│ │ ├── EmbedsRelations.php
│ │ ├── HasSchemaVersion.php
│ │ ├── HybridRelations.php
│ │ ├── MassPrunable.php
│ │ ├── Model.php
│ │ └── SoftDeletes.php
│ ├── Helpers/
│ │ ├── EloquentBuilder.php
│ │ └── QueriesRelationships.php
│ ├── MongoDBBusServiceProvider.php
│ ├── MongoDBServiceProvider.php
│ ├── Query/
│ │ ├── AggregationBuilder.php
│ │ ├── Builder.php
│ │ ├── BuilderTimeout.php
│ │ ├── Grammar.php
│ │ └── Processor.php
│ ├── Queue/
│ │ ├── MongoConnector.php
│ │ ├── MongoJob.php
│ │ └── MongoQueue.php
│ ├── Relations/
│ │ ├── BelongsTo.php
│ │ ├── BelongsToMany.php
│ │ ├── EmbedsMany.php
│ │ ├── EmbedsOne.php
│ │ ├── EmbedsOneOrMany.php
│ │ ├── HasMany.php
│ │ ├── HasOne.php
│ │ ├── MorphMany.php
│ │ ├── MorphTo.php
│ │ └── MorphToMany.php
│ ├── Schema/
│ │ ├── Blueprint.php
│ │ ├── BlueprintLaravelCompatibility.php
│ │ ├── Builder.php
│ │ └── Grammar.php
│ ├── Scout/
│ │ └── ScoutEngine.php
│ ├── Session/
│ │ └── MongoDbSessionHandler.php
│ └── Validation/
│ ├── DatabasePresenceVerifier.php
│ └── ValidationServiceProvider.php
└── tests/
├── AtlasSearchIndexManagement.php
├── AtlasSearchTest.php
├── AuthTest.php
├── Bus/
│ ├── Fixtures/
│ │ ├── ChainHeadJob.php
│ │ ├── SecondTestJob.php
│ │ └── ThirdTestJob.php
│ └── MongoBatchRepositoryTest.php
├── Cache/
│ ├── MongoCacheStoreTest.php
│ └── MongoLockTest.php
├── Casts/
│ ├── BinaryUuidTest.php
│ ├── BooleanTest.php
│ ├── CollectionTest.php
│ ├── DateTest.php
│ ├── DatetimeTest.php
│ ├── DecimalTest.php
│ ├── EncryptionTest.php
│ ├── FloatTest.php
│ ├── IntegerTest.php
│ ├── JsonTest.php
│ ├── ObjectIdTest.php
│ ├── ObjectTest.php
│ └── StringTest.php
├── ConnectionTest.php
├── DateTimeImmutableTest.php
├── Eloquent/
│ ├── CallBuilderTest.php
│ ├── MassPrunableTest.php
│ └── ModelTest.php
├── EmbeddedRelationsTest.php
├── ExternalPackageTest.php
├── FilesystemsTest.php
├── GeospatialTest.php
├── HybridRelationsTest.php
├── ModelTest.php
├── Models/
│ ├── Address.php
│ ├── Anniversary.php
│ ├── Birthday.php
│ ├── Book.php
│ ├── CastObjectId.php
│ ├── Casting.php
│ ├── Client.php
│ ├── Experience.php
│ ├── Group.php
│ ├── Guarded.php
│ ├── HiddenAnimal.php
│ ├── IdIsBinaryUuid.php
│ ├── IdIsInt.php
│ ├── IdIsString.php
│ ├── Item.php
│ ├── Label.php
│ ├── Location.php
│ ├── MemberStatus.php
│ ├── NonIncrementing.php
│ ├── Photo.php
│ ├── Role.php
│ ├── SchemaVersion.php
│ ├── Scoped.php
│ ├── Skill.php
│ ├── Soft.php
│ ├── SqlBook.php
│ ├── SqlRole.php
│ ├── SqlUser.php
│ └── User.php
├── PHPStan/
│ └── SarifErrorFormatter.php
├── PropertyTest.php
├── Query/
│ ├── AggregationBuilderTest.php
│ └── BuilderTest.php
├── QueryBuilderTest.php
├── QueryTest.php
├── Queue/
│ └── Failed/
│ └── DatabaseFailedJobProviderTest.php
├── QueueTest.php
├── RelationsTest.php
├── SchemaTest.php
├── SchemaVersionTest.php
├── Scout/
│ ├── Models/
│ │ ├── ScoutUser.php
│ │ ├── SearchableInSameNamespace.php
│ │ └── SearchableModel.php
│ ├── ScoutEngineTest.php
│ └── ScoutIntegrationTest.php
├── Seeder/
│ ├── DatabaseSeeder.php
│ └── UserTableSeeder.php
├── SeederTest.php
├── SessionTest.php
├── TestCase.php
├── Ticket/
│ ├── GH2489Test.php
│ ├── GH2783Test.php
│ ├── GH3326Test.php
│ ├── GH3328Test.php
│ └── GH3335Test.php
├── TransactionTest.php
├── ValidationTest.php
└── config/
├── database.php
└── queue.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.yml]
indent_size = 2
================================================
FILE: .gitattributes
================================================
/.github export-ignore
/.phpunit.cache export-ignore
/tests export-ignore
*.md export-ignore
*.dist export-ignore
.editorconfig export-ignore
.gitattributes export-ignore
.gitignore export-ignore
docker-compose.yml export-ignore
Dockerfile export-ignore
phpstan-baseline.neon export-ignore
================================================
FILE: .github/CODEOWNERS
================================================
* @mongodb/dbx-php
================================================
FILE: .github/ISSUE_TEMPLATE/BUG_REPORT.md
================================================
---
name: "Bug report"
about: 'Report errors or unexpected behavior.'
---
- Laravel-mongodb Version: #.#.#
- PHP Version: #.#.#
- Database Driver & Version:
### Description:
### Steps to reproduce
1.
2.
3.
### Expected behaviour
Tell us what should happen
### Actual behaviour
Tell us what happens instead
Logs :
Insert log.txt here (if necessary)
================================================
FILE: .github/ISSUE_TEMPLATE/FEATURE-REQUEST.md
================================================
---
name: Feature request
about: Suggest an idea.
title: "[Feature Request] "
---
### Is your feature request related to a problem?
A clear and concise description of what the problem is.
### Describe the solution you'd like
A clear and concise description of what you want to happen.
### Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
### Additional context
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Discussions
url: https://github.com/mongodb/laravel-mongodb/discussions/new/choose
about: For questions, discussions, or general technical support from other Laravel users, visit the Discussions page.
- name: MongoDB Developer Community Forums
url: https://developer.mongodb.com/community/forums/
about: For questions, discussions, or general technical support, visit the MongoDB Community Forums. The MongoDB Community Forums are a centralized place to connect with other MongoDB users, ask questions, and get answers.
- name: Report a Security Vulnerability
url: https://mongodb.com/docs/manual/tutorial/create-a-vulnerability-report
about: If you believe you have discovered a vulnerability in MongoDB products or have experienced a security incident related to MongoDB products, please report the issue to aid in its resolution.
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
### Checklist
- [ ] Add tests and ensure they pass
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
================================================
FILE: .github/labeler.yml
================================================
# https://github.com/actions/labeler
github:
- changed-files:
- any-glob-to-any-file: '.github/**'
================================================
FILE: .github/release.yml
================================================
changelog:
exclude:
labels:
- ignore-for-release
- minor
authors:
- mongodb-php-bot
categories:
- title: Breaking Changes 🛠
labels:
- breaking-change
- title: New Features
labels:
- enhancement
- feature
- title: Fixed
labels:
- bug
- fixed
- title: Other Changes
labels:
- '*'
================================================
FILE: .github/workflows/build-ci-atlas.yml
================================================
name: Atlas CI
on:
push:
branches:
- '[0-9]+.[0-9x]+'
pull_request:
branches:
- '[0-9]+.[0-9x]+'
- feature/*
env:
MONGODB_EXT_V1: mongodb-1.21.0
MONGODB_EXT_V2: stable
jobs:
build:
runs-on: ubuntu-latest
name: PHP/${{ matrix.php }} Laravel/${{ matrix.laravel }} Driver/${{ matrix.driver }}
strategy:
matrix:
php: ['8.2', '8.3', '8.4', '8.5']
laravel: ['12.*', '13.*']
driver: [2]
include:
- php: '8.1'
laravel: 10.*
driver: 1
- php: '8.2'
laravel: 11.*
driver: 1
- php: '8.4'
laravel: 12.*
driver: 1
exclude:
- laravel: 13.*
php: '8.2'
steps:
- uses: actions/checkout@v6
- name: Create MongoDB Atlas Local
run: |
docker run --name mongodb -p 27017:27017 --detach mongodb/mongodb-atlas-local:latest
until docker exec --tty mongodb mongosh --eval "db.runCommand({ ping: 1 })"; do
sleep 1
done
until docker exec --tty mongodb mongosh --eval "db.createCollection('connection_test') && db.getCollection('connection_test').createSearchIndex({mappings:{dynamic: true}})"; do
sleep 1
done
- name: Show MongoDB server status
run: |
docker exec --tty mongodb mongosh --eval "db.runCommand({ serverStatus: 1 })"
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: ${{ matrix.php }}
extensions: ${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }}
key: extcache-v1
- name: Installing php
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: curl,mbstring,xdebug,${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }}
coverage: xdebug
tools: composer
- name: Show Docker version
if: ${{ runner.debug }}
run: docker version && env
- name: Restrict Laravel version
run: composer require --dev --no-update 'laravel/framework:${{ matrix.laravel }}'
- name: Download Composer cache dependencies from cache
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ matrix.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ matrix.os }}-composer-
- name: Install dependencies
run: |
composer update --no-interaction
- name: Run tests
run: |
export MONGODB_URI="mongodb://127.0.0.1:27017/?directConnection=true"
php -d zend.assertions=1 ./vendor/bin/phpunit --coverage-clover coverage.xml --group atlas-search
================================================
FILE: .github/workflows/build-ci.yml
================================================
name: CI
on:
push:
branches:
- '[0-9]+.[0-9x]+'
pull_request:
branches:
- '[0-9]+.[0-9x]+'
- feature/*
env:
MONGODB_EXT_V1: mongodb-1.21.0
MONGODB_EXT_V2: stable
jobs:
build:
runs-on: ubuntu-latest
name: PHP/${{ matrix.php }} Laravel/${{ matrix.laravel }} Driver/${{ matrix.driver }} Server/${{ matrix.mongodb }} ${{ matrix.mode }}
strategy:
matrix:
mongodb: ['8.0']
php: ['8.2', '8.3', '8.4', '8.5']
laravel: ['12.*', '13.*']
driver: [2]
include:
- php: '8.1'
laravel: 10.*
mongodb: '5.0'
mode: low-deps
driver: 1
- php: '8.3'
laravel: 10.*
mongodb: '4.4'
driver: 1
- php: '8.4'
laravel: 12.*
mongodb: '8.0'
driver: 1
- php: '8.5'
laravel: 10.*
mongodb: '8.0'
- php: '8.5'
laravel: 11.*
mongodb: '8.0'
- php: '8.2'
laravel: 12.*
mongodb: '4.4'
- php: '8.2'
laravel: 12.*
mongodb: '5.0'
- php: '8.2'
laravel: 12.*
mongodb: '6.0'
- php: '8.2'
laravel: 12.*
mongodb: '7.0'
- php: '8.2'
laravel: 12.*
mongodb: '8.0'
exclude:
- laravel: 13.*
php: '8.2'
steps:
- uses: actions/checkout@v6
- name: Create MongoDB Replica Set
run: |
docker run --name mongodb -p 27017:27017 -e MONGO_INITDB_DATABASE=unittest --detach mongo:${{ matrix.mongodb }} mongod --replSet rs --setParameter transactionLifetimeLimitSeconds=5
if [ "${{ matrix.mongodb }}" = "4.4" ]; then MONGOSH_BIN="mongo"; else MONGOSH_BIN="mongosh"; fi
until docker exec --tty mongodb $MONGOSH_BIN --eval "db.runCommand({ ping: 1 })"; do
sleep 1
done
sudo docker exec --tty mongodb $MONGOSH_BIN --eval "rs.initiate({\"_id\":\"rs\",\"members\":[{\"_id\":0,\"host\":\"127.0.0.1:27017\" }]})"
- name: Show MongoDB server status
run: |
if [ "${{ matrix.mongodb }}" = "4.4" ]; then MONGOSH_BIN="mongo"; else MONGOSH_BIN="mongosh"; fi
docker exec --tty mongodb $MONGOSH_BIN --eval "db.runCommand({ serverStatus: 1 })"
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: ${{ matrix.php }}
extensions: ${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }}
key: extcache-v1
- name: Installing php
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: curl,mbstring,xdebug,${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }}
coverage: xdebug
tools: composer
- name: Show Docker version
if: ${{ runner.debug }}
run: docker version && env
- name: Restrict Laravel version
run: composer require --dev --no-update 'laravel/framework:${{ matrix.laravel }}'
- name: Download Composer cache dependencies from cache
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ matrix.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ matrix.os }}-composer-
- name: Install dependencies
run: |
composer update --no-interaction \
$([[ "${{ matrix.mode }}" == low-deps ]] && echo ' --prefer-lowest') \
$([[ "${{ matrix.mode }}" == ignore-php-req ]] && echo ' --ignore-platform-req=php+')
- name: Run tests
run: |
export MONGODB_URI="mongodb://127.0.0.1:27017/?replicaSet=rs"
php -d zend.assertions=1 ./vendor/bin/phpunit --coverage-clover coverage.xml --exclude-group atlas-search --testdox
================================================
FILE: .github/workflows/coding-standards.yml
================================================
name: Coding Standards
on:
push:
branches:
- '[0-9]+.[0-9x]+'
pull_request:
branches:
- '[0-9]+.[0-9x]+'
- feature/*
env:
PHP_VERSION: '8.4'
DRIVER_VERSION: stable
jobs:
phpcs:
name: phpcs
runs-on: ubuntu-22.04
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push the
# added or changed files to the repository.
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: ${{ env.PHP_VERSION }}
extensions: mongodb-${{ env.DRIVER_VERSION }}
key: extcache-v1
- name: Cache extensions
uses: actions/cache@v5
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
coverage: none
extensions: mongodb-${{ env.DRIVER_VERSION }}
php-version: ${{ env.PHP_VERSION }}
tools: cs2pr
- name: Show driver information
run: php --ri mongodb
- name: Install dependencies with Composer
uses: ramsey/composer-install@4.0.0
with:
composer-options: --no-suggest
- name: Validate PSR class names
run: composer dump-autoload --optimize --strict-psr
# The -q option is required until phpcs v4 is released
- name: Run PHP_CodeSniffer
run: |
mkdir .cache
vendor/bin/phpcs -q --no-colors --report=checkstyle | cs2pr
================================================
FILE: .github/workflows/labeler.yml
================================================
name: Pull Request Labeler
on:
- pull_request_target
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v6
================================================
FILE: .github/workflows/merge-up.yml
================================================
name: Merge up
on:
push:
branches:
- '[0-9]+.[0-9x]+'
env:
GH_TOKEN: ${{ secrets.MERGE_UP_TOKEN }}
jobs:
merge-up:
name: Create merge up pull request
runs-on: ubuntu-latest
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v6
with:
# fetch-depth 0 is required to fetch all branches, not just the branch being built
fetch-depth: 0
token: ${{ secrets.MERGE_UP_TOKEN }}
- name: Create pull request
id: create-pull-request
uses: alcaeus/automatic-merge-up-action@main
with:
ref: ${{ github.ref_name }}
branchNamePattern: '.'
devBranchNamePattern: '.x'
enableAutoMerge: true
================================================
FILE: .github/workflows/release.yml
================================================
name: "Release New Version"
run-name: "Release ${{ inputs.version }}"
on:
workflow_dispatch:
inputs:
version:
description: "The version to be released. This is checked for consistency with the branch name and configuration"
required: true
type: "string"
jobs:
prepare-release:
environment: release
name: "Prepare release"
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
steps:
- name: "Create release output"
run: echo '🎬 Release process for version ${{ inputs.version }} started by @${{ github.triggering_actor }}' >> $GITHUB_STEP_SUMMARY
- name: "Generate token and checkout repository"
uses: mongodb-labs/drivers-github-tools/secure-checkout@v3
with:
app_id: ${{ vars.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- name: "Store version numbers in env variables"
run: |
echo RELEASE_VERSION=${{ inputs.version }} >> $GITHUB_ENV
echo RELEASE_BRANCH=$(echo ${{ inputs.version }} | cut -d '.' -f-2) >> $GITHUB_ENV
echo DEV_BRANCH=$(echo ${{ inputs.version }} | cut -d '.' -f-1).x >> $GITHUB_ENV
- name: "Ensure release tag does not already exist"
run: |
if [[ $(git tag -l ${RELEASE_VERSION}) == ${RELEASE_VERSION} ]]; then
echo '❌ Release failed: tag for version ${{ inputs.version }} already exists' >> $GITHUB_STEP_SUMMARY
exit 1
fi
# For patch releases (A.B.C where C != 0), we expect the release to be
# triggered from the A.B maintenance branch
- name: "Fail if patch release is created from wrong release branch"
if: ${{ !endsWith(inputs.version, '.0') && env.RELEASE_BRANCH != github.ref_name }}
run: |
echo '❌ Release failed due to branch mismatch: expected ${{ inputs.version }} to be released from ${{ env.RELEASE_BRANCH }}, got ${{ github.ref_name }}' >> $GITHUB_STEP_SUMMARY
exit 1
# For non-patch releases (A.B.C where C == 0), we expect the release to
# be triggered from the A.x maintenance branch or A.x development branch
- name: "Fail if non-patch release is created from wrong release branch"
if: ${{ endsWith(inputs.version, '.0') && env.RELEASE_BRANCH != github.ref_name && env.DEV_BRANCH != github.ref_name }}
run: |
echo '❌ Release failed due to branch mismatch: expected ${{ inputs.version }} to be released from ${{ env.RELEASE_BRANCH }} or ${{ env.DEV_BRANCH }}, got ${{ github.ref_name }}' >> $GITHUB_STEP_SUMMARY
exit 1
# If a non-patch release is created from its A.x development branch,
# create the A.B maintenance branch from the current one and push it
- name: "Create and push new release branch for non-patch release"
if: ${{ endsWith(inputs.version, '.0') && env.DEV_BRANCH == github.ref_name }}
run: |
echo '🆕 Creating new release branch ${RELEASE_BRANCH} from ${{ github.ref_name }}' >> $GITHUB_STEP_SUMMARY
git checkout -b ${RELEASE_BRANCH}
git push origin ${RELEASE_BRANCH}
#
# Preliminary checks done - commence the release process
#
- name: "Set up drivers-github-tools"
uses: mongodb-labs/drivers-github-tools/setup@v3
with:
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
aws_region_name: ${{ vars.AWS_REGION_NAME }}
aws_secret_id: ${{ secrets.AWS_SECRET_ID }}
# Create draft release with release notes
- name: "Create draft release"
run: echo "RELEASE_URL=$(gh release create ${{ inputs.version }} --target ${{ github.ref_name }} --title "${{ inputs.version }}" --generate-notes --draft)" >> "$GITHUB_ENV"
- name: "Create release tag"
uses: mongodb-labs/drivers-github-tools/tag-version@v3
with:
version: ${{ inputs.version }}
tag_message_template: 'Release ${VERSION}'
# TODO: Manually merge using ours strategy. This avoids merge-up pull requests being created
# Process is:
# 1. switch to next branch (according to merge-up action)
# 2. merge release branch using --strategy=ours
# 3. push next branch
# 4. switch back to release branch, then push
- name: "Set summary"
run: |
echo '🚀 Created tag and drafted release for version [${{ inputs.version }}](${{ env.RELEASE_URL }})' >> $GITHUB_STEP_SUMMARY
echo '✍️ You may now update the release notes and publish the release when ready' >> $GITHUB_STEP_SUMMARY
static-analysis:
needs: prepare-release
name: "Run Static Analysis"
uses: ./.github/workflows/static-analysis.yml
with:
ref: refs/tags/${{ inputs.version }}
permissions:
security-events: write
id-token: write
publish-ssdlc-assets:
needs: static-analysis
environment: release
name: "Publish SSDLC Assets"
runs-on: ubuntu-latest
permissions:
security-events: read
id-token: write
contents: write
steps:
- name: "Generate token and checkout repository"
uses: mongodb-labs/drivers-github-tools/secure-checkout@v3
with:
app_id: ${{ vars.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
ref: refs/tags/${{ inputs.version }}
# Sets the S3_ASSETS environment variable used later
- name: "Set up drivers-github-tools"
uses: mongodb-labs/drivers-github-tools/setup@v3
with:
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
aws_region_name: ${{ vars.AWS_REGION_NAME }}
aws_secret_id: ${{ secrets.AWS_SECRET_ID }}
- name: "Generate SSDLC Reports"
uses: mongodb-labs/drivers-github-tools/full-report@v3
with:
product_name: "MongoDB Laravel Integration"
release_version: ${{ inputs.version }}
silk_asset_group: mongodb-laravel-integration
- name: "Upload SBOM as release artifact"
run: gh release upload ${{ inputs.version }} ${{ env.S3_ASSETS }}/cyclonedx.sbom.json
continue-on-error: true
- name: Upload S3 assets
uses: mongodb-labs/drivers-github-tools/upload-s3-assets@v3
with:
version: ${{ inputs.version }}
product_name: laravel-mongodb
================================================
FILE: .github/workflows/static-analysis.yml
================================================
name: Static Analysis
on:
push:
branches:
- '[0-9]+.[0-9x]+'
pull_request:
branches:
- '[0-9]+.[0-9x]+'
- feature/*
workflow_call:
inputs:
ref:
description: The git ref to check
type: string
required: true
env:
PHP_VERSION: '8.5'
DRIVER_VERSION: stable
MONGODB_EXT_V1: mongodb-1.21.0
MONGODB_EXT_V2: mongodb-mongodb/mongo-php-driver@v2.x
jobs:
phpstan:
name: PHP/${{ matrix.php }} Driver/${{ matrix.driver }}
runs-on: ubuntu-22.04
continue-on-error: true
strategy:
matrix:
php: ['8.1', '8.2', '8.3', '8.4', '8.5']
driver: [2]
include:
- php: '8.4'
driver: 1
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
- name: Get SHA hash of checked out ref
if: ${{ github.event_name == 'workflow_dispatch' }}
run: |
echo CHECKED_OUT_SHA=$(git rev-parse HEAD) >> $GITHUB_ENV
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: ${{ matrix.php }}
extensions: ${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }}
key: extcache-v1
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: curl,mbstring,${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }}
tools: composer:v2
coverage: none
- name: Cache dependencies
id: composer-cache
uses: actions/cache@v5
with:
path: ./vendor
key: composer-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install
- name: Restore cache PHPStan results
id: phpstan-cache-restore
uses: actions/cache/restore@v5
with:
path: .cache
key: phpstan-result-cache-${{ matrix.php }}-${{ github.run_id }}
restore-keys: |
phpstan-result-cache-
- name: Run PHPStan
run: ./vendor/bin/phpstan analyse --no-interaction --no-progress --ansi --error-format=sarif > phpstan.sarif
continue-on-error: true
- name: Upload SARIF report
if: ${{ github.event_name != 'workflow_dispatch' }}
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: phpstan.sarif
- name: Upload SARIF report
if: ${{ github.event_name == 'workflow_dispatch' }}
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: phpstan.sarif
ref: ${{ inputs.ref }}
sha: ${{ env.CHECKED_OUT_SHA }}
- name: Save cache PHPStan results
id: phpstan-cache-save
if: always()
uses: actions/cache/save@v5
with:
path: .cache
key: ${{ steps.phpstan-cache-restore.outputs.cache-primary-key }}
================================================
FILE: .gitignore
================================================
*.project
*.sublime-project
*.sublime-workspace
.DS_Store
.idea/
/vendor
composer.lock
composer.phar
phpunit.xml
phpstan.neon
/.cache/
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# MongoDB Code of Conduct
The Code of Conduct outlines the expectations for our behavior as members of the MongoDB community.
We value the participation of each member of the MongoDB community and want all participants to have an enjoyable and fulfilling experience.
Thanks for reading the [MongoDB Code of Conduct](https://www.mongodb.com/community-code-of-conduct).
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
Contributions are **welcome** and will be fully **credited**.
Please read and understand the contribution guide before creating an issue or pull request.
## Etiquette
This project is open source, and as such, the maintainers give their free time to build and maintain the source code
held within. They make the code freely available in the hope that it will be of use to other developers. It would be
extremely unfair for them to suffer abuse or anger for their hard work.
Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the
world that developers are civilized and selfless people.
It's the duty of the maintainer to ensure that all submissions to the project are of sufficient
quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used.
## Viability
When requesting or submitting new features, first consider whether it might be useful to others. Open
source projects are used by many developers, who may have entirely different needs to your own. Think about
whether or not your feature is likely to be used by other users of the project.
## Procedure
Before filing an issue:
- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident.
- Check to make sure your feature suggestion isn't already present within the project.
- Check the pull requests tab to ensure that the bug doesn't have a fix in progress.
- Check the pull requests tab to ensure that the feature isn't already in progress.
Before submitting a pull request:
- Check the codebase to ensure that your feature doesn't already exist.
- Check the pull requests to ensure that another person hasn't already submitted the feature or fix.
## Run Tests
The full test suite requires PHP cli with mongodb extension and a running MongoDB server. A replica set is required for
testing transactions.
Duplicate the `phpunit.xml.dist` file to `phpunit.xml` and edit the environment variables to match your setup.
```bash
$ docker-compose run app
```
Docker can be slow to start. You can run the command `composer run test` locally or in a docker container.
```bash
$ docker-compose run -it tests bash
# Inside the container
$ composer install
$ composer run test
```
For fixing style issues, you can run the PHP Code Beautifier and Fixer, some issues can't be fixed automatically:
```bash
$ composer run cs:fix
$ composer run cs
```
## Requirements
If the project maintainer has any additional requirements, you will find them listed here.
- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer).
- **Add tests!** - Your patch won't be accepted if it doesn't have tests.
- **Document any change in behaviour** - Make sure the documentation is kept up-to-date.
- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option.
- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
Happy coding!
## Releasing
The releases are created by the maintainers of the library. The process is documented in
the [RELEASING.md](RELEASING.md) file.
================================================
FILE: Dockerfile
================================================
ARG PHP_VERSION=8.2
FROM php:${PHP_VERSION}-cli
# Install extensions
RUN apt-get update && \
apt-get install -y autoconf pkg-config libssl-dev git unzip libzip-dev zlib1g-dev && \
pecl install mongodb && docker-php-ext-enable mongodb && \
pecl install xdebug && docker-php-ext-enable xdebug && \
docker-php-ext-install -j$(nproc) zip
# Create php.ini
RUN cp "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
# Install Composer
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 MongoDB, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
Laravel MongoDB
===============
[](https://packagist.org/packages/mongodb/laravel-mongodb)
[](https://packagist.org/packages/mongodb/laravel-mongodb)
[](https://github.com/mongodb/laravel-mongodb/actions/workflows/build-ci.yml)
This package adds functionalities to the Eloquent model and Query builder for MongoDB, using the original Laravel API.
*This library extends the original Laravel classes, so it uses exactly the same methods.*
This package was renamed to `mongodb/laravel-mongodb` because of a transfer of ownership to MongoDB, Inc.
It is compatible with Laravel 10.x. For older versions of Laravel, please refer to the
[old versions](https://github.com/mongodb/laravel-mongodb/tree/3.9#laravel-version-compatibility).
## Documentation
- https://www.mongodb.com/docs/drivers/php/laravel-mongodb/
- https://www.mongodb.com/docs/drivers/php/
## Release Integrity
Releases are created automatically and the resulting release tag is signed using
the [PHP team's GPG key](https://pgp.mongodb.com/php-driver.asc). To verify the
tag signature, download the key and import it using `gpg`:
```shell
gpg --import php-driver.asc
```
Then, in a local clone, verify the signature of a given tag (e.g. `4.4.0`):
```shell
git show --show-signature 4.4.0
```
> [!NOTE]
> Composer does not support verifying signatures as part of its installation
> process.
## Reporting Issues
Think you’ve found a bug in the library? Want to see a new feature? Please open a case in our issue management tool, JIRA:
- [Create an account and login.](https://jira.mongodb.org/)
- Navigate to the [PHPORM](https://jira.mongodb.org/browse/PHPORM) project.
- Click Create - Please provide as much information as possible about the issue type and how to reproduce it.
Note: All reported issues in JIRA project are public.
For general questions and support requests, please use one of MongoDB's
[Technical Support](https://mongodb.com/docs/manual/support/) channels.
### Security Vulnerabilities
If you've identified a security vulnerability in a driver or any other MongoDB
project, please report it according to the instructions in
[Create a Vulnerability Report](https://mongodb.com/docs/manual/tutorial/create-a-vulnerability-report).
## Development
Development is tracked in the
[PHPORM](https://jira.mongodb.org/projects/PHPORM/summary) project in MongoDB's
JIRA. Documentation for contributing to this project may be found in
[CONTRIBUTING.md](CONTRIBUTING.md).
================================================
FILE: RELEASING.md
================================================
# Releasing
The following steps outline the release process for both new minor versions and
patch versions.
The command examples below assume that the canonical "mongodb" repository has
the remote name "mongodb". You may need to adjust these commands if you've given
the remote another name (e.g. "upstream"). The "origin" remote name was not used
as it likely refers to your personal fork.
It helps to keep your own fork in sync with the "mongodb" repository (i.e. any
branches and tags on the main repository should also exist in your fork). This
is left as an exercise to the reader.
## Ensure PHP version compatibility
Ensure that the test suite completes on supported versions of PHP.
## Transition JIRA issues and version
All issues associated with the release version should be in the "Closed" state
and have a resolution of "Fixed". Issues with other resolutions (e.g.
"Duplicate", "Works as Designed") should be removed from the release version so
that they do not appear in the release notes.
Check the corresponding "laravel-*.x" fix version to see if it contains any
issues that are resolved as "Fixed" and should be included in this release
version.
Update the version's release date and status from the
[Manage Versions](https://jira.mongodb.org/plugins/servlet/project-config/PHPORM/versions)
page.
## Trigger the release workflow
Releases are done automatically through a GitHub Action. Visit the corresponding
[Release New Version](https://github.com/mongodb/laravel-mongodb/actions/workflows/release.yml)
workflow page to trigger a new build. Select the correct branch (e.g. `v4.5`)
and trigger a new run using the "Run workflow" button. In the following prompt,
enter the version number.
The automation will then create and push the necessary commits and tag, and create
a draft release. The release is created in a draft state and can be published
once the release notes have been updated.
## Branch management
# Creating a maintenance branch and updating default branch name
When releasing a new major or minor version (e.g. 4.0.0), the default branch
should be renamed to the next version (e.g. 4.1). Renaming the default branch
using GitHub's UI ensures that all open pull request are changed to target the
new version.
Once the default branch has been renamed, create the maintenance branch for the
version to be released (e.g. 4.0):
```console
$ git checkout -b X.Y
$ git push mongodb X.Y
```
### After releasing a patch version
If this was a patch release, the maintenance branch must be merged up to the
default branch (e.g. 4.1):
```console
$ git checkout 4.1
$ git pull mongodb 4.1
$ git merge 4.0 --strategy=ours
$ git push mongodb
```
The `--strategy=ours` option ensures that all changes from the merged commits
will be ignored. This is OK because we previously ensured that the `4.1`
branch was up-to-date with all code changes in this maintenance branch before
tagging.
## Publish release notes
Use the generated release note in [this form](https://github.com/mongodb/laravel-mongodb/releases/new).
Release announcements should also be posted in the [MongoDB Product & Driver Announcements: Driver Releases](https://mongodb.com/community/forums/tags/c/announcements/driver-releases/110/php) forum and shared on Twitter.
================================================
FILE: composer.json
================================================
{
"name": "mongodb/laravel-mongodb",
"description": "A MongoDB based Eloquent model and Query builder for Laravel",
"keywords": [
"laravel",
"eloquent",
"mongodb",
"mongo",
"database",
"model"
],
"homepage": "https://github.com/mongodb/laravel-mongodb",
"support": {
"issues": "https://www.mongodb.com/support",
"security": "https://www.mongodb.com/security"
},
"authors": [
{
"name": "Andreas Braun",
"email": "andreas.braun@mongodb.com",
"role": "Leader"
},
{
"name": "Pauline Vos",
"email": "pauline.vos@mongodb.com",
"role": "Maintainer"
},
{
"name": "Jérôme Tamarelle",
"email": "jerome.tamarelle@mongodb.com",
"role": "Maintainer"
},
{
"name": "Jeremy Mikola",
"email": "jmikola@gmail.com",
"role": "Maintainer"
},
{
"name": "Jens Segers",
"homepage": "https://jenssegers.com",
"role": "Creator"
}
],
"license": "MIT",
"require": {
"php": "^8.1",
"ext-mongodb": "^1.21|^2",
"composer-runtime-api": "^2.0.0",
"illuminate/cache": "^10.36|^11|^12|^13.0",
"illuminate/container": "^10.0|^11|^12|^13.0",
"illuminate/database": "^10.30|^11|^12|^13.0",
"illuminate/events": "^10.0|^11|^12|^13.0",
"illuminate/support": "^10.0|^11|^12|^13.0",
"mongodb/mongodb": "^1.21|^2"
},
"require-dev": {
"laravel/scout": "^10.3",
"league/flysystem-gridfs": "^3.28",
"league/flysystem-read-only": "^3.0",
"phpunit/phpunit": "^10.3|^11.5.3|^12.5.12",
"orchestra/testbench": "^8.0|^9.0|^10.0|^11.0",
"mockery/mockery": "^1.4.4",
"doctrine/coding-standard": "^12.0",
"spatie/laravel-query-builder": "^5.6|^6",
"phpstan/phpstan": "^1.10|^2.1",
"rector/rector": "^1.2|^2.3"
},
"conflict": {
"illuminate/bus": "< 10.37.2"
},
"suggest": {
"league/flysystem-gridfs": "Filesystem storage in MongoDB with GridFS"
},
"minimum-stability": "dev",
"prefer-stable": true,
"replace": {
"jenssegers/mongodb": "self.version"
},
"autoload": {
"psr-4": {
"MongoDB\\Laravel\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"MongoDB\\Laravel\\Tests\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"MongoDB\\Laravel\\MongoDBServiceProvider",
"MongoDB\\Laravel\\MongoDBBusServiceProvider"
]
}
},
"scripts": {
"test": "phpunit",
"test:coverage": "phpunit --coverage-clover ./coverage.xml",
"cs": "phpcs",
"cs:fix": "phpcbf",
"rector": "rector"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}
================================================
FILE: docker-compose.yml
================================================
services:
app:
tty: true
build: .
working_dir: /var/www/laravel-mongodb
command: "bash -c 'composer install && composer run test'"
environment:
MONGODB_URI: 'mongodb://mongodb/'
volumes:
- .:/var/www/laravel-mongodb
depends_on:
mongodb:
condition: service_healthy
mongodb:
container_name: mongodb
image: mongodb/mongodb-atlas-local:8
ports:
- "27017:27017"
healthcheck:
test: mongosh --quiet --eval 'db.runCommand("ping").ok'
interval: 10s
timeout: 10s
retries: 5
================================================
FILE: phpcs.xml.dist
================================================
src
tests
/tests
/tests
tests/Ticket/*.php
src/Query/BuilderTimeout.php
src/Schema/Blueprint.php
================================================
FILE: phpstan-baseline.neon
================================================
parameters:
ignoreErrors:
-
message: "#^Class MongoDB\\\\Laravel\\\\Query\\\\Grammar does not have a constructor and must be instantiated without any parameters\\.$#"
count: 1
path: src/Connection.php
-
message: "#^Class MongoDB\\\\Laravel\\\\Schema\\\\Grammar does not have a constructor and must be instantiated without any parameters\\.$#"
count: 1
path: src/Connection.php
-
message: "#^Access to an undefined property Illuminate\\\\Container\\\\Container\\:\\:\\$config\\.$#"
count: 3
path: src/MongoDBBusServiceProvider.php
-
message: "#^Access to an undefined property Illuminate\\\\Foundation\\\\Application\\:\\:\\$config\\.$#"
count: 4
path: src/MongoDBServiceProvider.php
-
message: "#^Call to an undefined method TDeclaringModel of Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:pull\\(\\)\\.$#"
count: 1
path: src/Relations/BelongsToMany.php
-
message: "#^Method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:push\\(\\) invoked with 3 parameters, 0 required\\.$#"
count: 3
path: src/Relations/BelongsToMany.php
-
message: "#^Call to an undefined method MongoDB\\\\Laravel\\\\Relations\\\\EmbedsMany\\\\:\\:contains\\(\\)\\.$#"
count: 1
path: src/Relations/EmbedsMany.php
-
message: "#^Call to an undefined method TDeclaringModel of Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:getParentRelation\\(\\)\\.$#"
count: 1
path: src/Relations/EmbedsOneOrMany.php
-
message: "#^Call to an undefined method TDeclaringModel of Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:setParentRelation\\(\\)\\.$#"
count: 1
path: src/Relations/EmbedsOneOrMany.php
-
message: "#^Call to an undefined method TRelatedModel of Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:setParentRelation\\(\\)\\.$#"
count: 2
path: src/Relations/EmbedsOneOrMany.php
-
message: "#^Call to an undefined method TDeclaringModel of Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:pull\\(\\)\\.$#"
count: 2
path: src/Relations/MorphToMany.php
-
message: "#^Method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:push\\(\\) invoked with 3 parameters, 0 required\\.$#"
count: 6
path: src/Relations/MorphToMany.php
-
message: "#^Method Illuminate\\\\Database\\\\Schema\\\\Blueprint\\:\\:create\\(\\) invoked with 1 parameter, 0 required\\.$#"
count: 1
path: src/Schema/Builder.php
-
message: "#^Call to an undefined method Illuminate\\\\Support\\\\HigherOrderCollectionProxy\\<\\(int\\|string\\), Illuminate\\\\Database\\\\Eloquent\\\\Model\\>\\:\\:pushSoftDeleteMetadata\\(\\)\\.$#"
count: 1
path: src/Scout/ScoutEngine.php
================================================
FILE: phpstan.neon.dist
================================================
includes:
- ./phpstan-baseline.neon
parameters:
tmpDir: .cache/phpstan
paths:
- src
level: 2
editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%'
universalObjectCratesClasses:
- MongoDB\BSON\Document
ignoreErrors:
- '#Unsafe usage of new static#'
- '#Call to an undefined method [a-zA-Z0-9\\_\<\>\(\)]+::[a-zA-Z]+\(\)#'
services:
errorFormatter.sarif:
class: MongoDB\Laravel\Tests\PHPStan\SarifErrorFormatter
arguments:
relativePathHelper: @simpleRelativePathHelper
currentWorkingDirectory: %currentWorkingDirectory%
pretty: true
================================================
FILE: phpunit.xml.dist
================================================
tests/
./src
================================================
FILE: rector.php
================================================
withPaths([
__FILE__,
__DIR__ . '/src',
__DIR__ . '/tests',
])
->withPhpSets()
->withTypeCoverageLevel(0)
->withSkip([
RemoveExtraParametersRector::class,
ClosureToArrowFunctionRector::class,
NullToStrictStringFuncCallArgRector::class,
MixedTypeRector::class,
AddClosureVoidReturnTypeWhereNoReturnRector::class,
]);
================================================
FILE: sbom.json
================================================
{
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"serialNumber": "urn:uuid:0b622e40-f57d-4c6f-9f63-db415c1a1271",
"version": 1,
"metadata": {
"timestamp": "2024-05-08T09:52:55Z",
"tools": [
{
"name": "composer",
"version": "2.7.6"
},
{
"vendor": "cyclonedx",
"name": "cyclonedx-php-composer",
"version": "v5.2.0",
"externalReferences": [
{
"type": "distribution",
"url": "https://api.github.com/repos/CycloneDX/cyclonedx-php-composer/zipball/f3a3cdc1a9e34bf1d5748e4279a24569cbf31fed",
"comment": "dist reference: f3a3cdc1a9e34bf1d5748e4279a24569cbf31fed"
},
{
"type": "vcs",
"url": "https://github.com/CycloneDX/cyclonedx-php-composer.git",
"comment": "source reference: f3a3cdc1a9e34bf1d5748e4279a24569cbf31fed"
},
{
"type": "website",
"url": "https://github.com/CycloneDX/cyclonedx-php-composer/#readme",
"comment": "as detected from Composer manifest 'homepage'"
},
{
"type": "issue-tracker",
"url": "https://github.com/CycloneDX/cyclonedx-php-composer/issues",
"comment": "as detected from Composer manifest 'support.issues'"
},
{
"type": "vcs",
"url": "https://github.com/CycloneDX/cyclonedx-php-composer/",
"comment": "as detected from Composer manifest 'support.source'"
}
]
},
{
"vendor": "cyclonedx",
"name": "cyclonedx-library",
"version": "3.x-dev cad0f92",
"externalReferences": [
{
"type": "distribution",
"url": "https://api.github.com/repos/CycloneDX/cyclonedx-php-library/zipball/cad0f92b36c85f36b3d3c11ff96002af5f20cd10",
"comment": "dist reference: cad0f92b36c85f36b3d3c11ff96002af5f20cd10"
},
{
"type": "vcs",
"url": "https://github.com/CycloneDX/cyclonedx-php-library.git",
"comment": "source reference: cad0f92b36c85f36b3d3c11ff96002af5f20cd10"
},
{
"type": "website",
"url": "https://github.com/CycloneDX/cyclonedx-php-library/#readme",
"comment": "as detected from Composer manifest 'homepage'"
},
{
"type": "documentation",
"url": "https://cyclonedx-php-library.readthedocs.io",
"comment": "as detected from Composer manifest 'support.docs'"
},
{
"type": "issue-tracker",
"url": "https://github.com/CycloneDX/cyclonedx-php-library/issues",
"comment": "as detected from Composer manifest 'support.issues'"
},
{
"type": "vcs",
"url": "https://github.com/CycloneDX/cyclonedx-php-library/",
"comment": "as detected from Composer manifest 'support.source'"
}
]
}
]
}
}
================================================
FILE: src/Auth/User.php
================================================
collection = $connection->getCollection($collection);
parent::__construct($factory, $connection, $collection);
}
#[Override]
public function get($limit = 50, $before = null): array
{
if (is_string($before)) {
$before = new ObjectId($before);
}
return $this->collection->find(
$before ? ['_id' => ['$lt' => $before]] : [],
[
'limit' => $limit,
'sort' => ['_id' => -1],
'typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array'],
],
)->toArray();
}
#[Override]
public function find(string $batchId): ?Batch
{
$batchId = new ObjectId($batchId);
$batch = $this->collection->findOne(
['_id' => $batchId],
[
// If the select query is executed faster than the database replication takes place,
// then no batch is found. In that case an exception is thrown because jobs are added
// to a null batch.
'readPreference' => new ReadPreference(ReadPreference::PRIMARY),
'typeMap' => ['root' => 'array', 'array' => 'array', 'document' => 'array'],
],
);
return $batch ? $this->toBatch($batch) : null;
}
#[Override]
public function store(PendingBatch $batch): Batch
{
$batch = [
'name' => $batch->name,
'total_jobs' => 0,
'pending_jobs' => 0,
'failed_jobs' => 0,
'failed_job_ids' => [],
// Serialization is required for Closures
'options' => serialize($batch->options),
'created_at' => $this->getUTCDateTime(),
'cancelled_at' => null,
'finished_at' => null,
];
$result = $this->collection->insertOne($batch);
return $this->toBatch(['_id' => $result->getInsertedId()] + $batch);
}
#[Override]
public function incrementTotalJobs(string $batchId, int $amount): void
{
$batchId = new ObjectId($batchId);
$this->collection->updateOne(
['_id' => $batchId],
[
'$inc' => [
'total_jobs' => $amount,
'pending_jobs' => $amount,
],
'$set' => ['finished_at' => null],
],
);
}
#[Override]
public function decrementPendingJobs(string $batchId, string $jobId): UpdatedBatchJobCounts
{
$batchId = new ObjectId($batchId);
$values = $this->collection->findOneAndUpdate(
['_id' => $batchId],
[
'$inc' => ['pending_jobs' => -1],
'$pull' => ['failed_job_ids' => $jobId],
],
[
'projection' => ['pending_jobs' => 1, 'failed_jobs' => 1],
'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER,
],
);
return new UpdatedBatchJobCounts(
$values['pending_jobs'],
$values['failed_jobs'],
);
}
#[Override]
public function incrementFailedJobs(string $batchId, string $jobId): UpdatedBatchJobCounts
{
$batchId = new ObjectId($batchId);
$values = $this->collection->findOneAndUpdate(
['_id' => $batchId],
[
'$inc' => ['failed_jobs' => 1],
'$push' => ['failed_job_ids' => $jobId],
],
[
'projection' => ['pending_jobs' => 1, 'failed_jobs' => 1],
'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER,
],
);
return new UpdatedBatchJobCounts(
$values['pending_jobs'],
$values['failed_jobs'],
);
}
#[Override]
public function markAsFinished(string $batchId): void
{
$batchId = new ObjectId($batchId);
$this->collection->updateOne(
['_id' => $batchId],
['$set' => ['finished_at' => $this->getUTCDateTime()]],
);
}
#[Override]
public function cancel(string $batchId): void
{
$batchId = new ObjectId($batchId);
$this->collection->updateOne(
['_id' => $batchId],
[
'$set' => [
'cancelled_at' => $this->getUTCDateTime(),
'finished_at' => $this->getUTCDateTime(),
],
],
);
}
#[Override]
public function delete(string $batchId): void
{
$batchId = new ObjectId($batchId);
$this->collection->deleteOne(['_id' => $batchId]);
}
/** Execute the given Closure within a storage specific transaction. */
#[Override]
public function transaction(Closure $callback): mixed
{
return $this->connection->transaction($callback);
}
/** Rollback the last database transaction for the connection. */
#[Override]
public function rollBack(): void
{
$this->connection->rollBack();
}
/** Prune the entries older than the given date. */
#[Override]
public function prune(DateTimeInterface $before): int
{
$result = $this->collection->deleteMany(
['finished_at' => ['$ne' => null, '$lt' => new UTCDateTime($before)]],
);
return $result->getDeletedCount();
}
/** Prune all the unfinished entries older than the given date. */
#[Override]
public function pruneUnfinished(DateTimeInterface $before): int
{
$result = $this->collection->deleteMany(
[
'finished_at' => null,
'created_at' => ['$lt' => new UTCDateTime($before)],
],
);
return $result->getDeletedCount();
}
/** Prune all the cancelled entries older than the given date. */
#[Override]
public function pruneCancelled(DateTimeInterface $before): int
{
$result = $this->collection->deleteMany(
[
'cancelled_at' => ['$ne' => null],
'created_at' => ['$lt' => new UTCDateTime($before)],
],
);
return $result->getDeletedCount();
}
/** @param array $batch */
#[Override]
protected function toBatch($batch): Batch
{
return $this->factory->make(
$this,
$batch['_id'],
$batch['name'],
$batch['total_jobs'],
$batch['pending_jobs'],
$batch['failed_jobs'],
$batch['failed_job_ids'],
unserialize($batch['options']),
$this->toCarbon($batch['created_at']),
$this->toCarbon($batch['cancelled_at']),
$this->toCarbon($batch['finished_at']),
);
}
private function getUTCDateTime(): UTCDateTime
{
// Using Carbon so the current time can be modified for tests
return new UTCDateTime(Carbon::now());
}
/** @return ($date is null ? null : CarbonImmutable) */
private function toCarbon(?UTCDateTime $date): ?CarbonImmutable
{
if ($date === null) {
return null;
}
return CarbonImmutable::createFromTimestampMsUTC((string) $date);
}
}
================================================
FILE: src/Cache/MongoLock.php
================================================
lottery[0] ?? null) || ! is_numeric($this->lottery[1] ?? null) || $this->lottery[0] > $this->lottery[1]) {
throw new InvalidArgumentException('Lock lottery must be a couple of integers [$chance, $total] where $chance <= $total. Example [2, 100]');
}
parent::__construct($name, $seconds, $owner);
}
/**
* Attempt to acquire the lock.
*/
#[Override]
public function acquire(): bool
{
// The lock can be acquired if: it doesn't exist, it has expired,
// or it is already owned by the same lock instance.
$isExpiredOrAlreadyOwned = [
'$or' => [
['$lte' => ['$expires_at', $this->getUTCDateTime()]],
['$eq' => ['$owner', $this->owner]],
],
];
$result = $this->collection->findOneAndUpdate(
['_id' => $this->name],
[
[
'$set' => [
'owner' => [
'$cond' => [
'if' => $isExpiredOrAlreadyOwned,
'then' => $this->owner,
'else' => '$owner',
],
],
'expires_at' => [
'$cond' => [
'if' => $isExpiredOrAlreadyOwned,
'then' => $this->getUTCDateTime($this->seconds),
'else' => '$expires_at',
],
],
],
],
],
[
'upsert' => true,
'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER,
'projection' => ['owner' => 1],
],
);
if ($this->lottery[0] <= 0 && random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
$this->collection->deleteMany(['expires_at' => ['$lte' => $this->getUTCDateTime()]]);
}
// Compare the owner to check if the lock is owned. Acquiring the same lock
// with the same owner at the same instant would lead to not update the document
return $result['owner'] === $this->owner;
}
/**
* Release the lock.
*/
#[Override]
public function release(): bool
{
$result = $this->collection
->deleteOne([
'_id' => $this->name,
'owner' => $this->owner,
]);
return $result->getDeletedCount() > 0;
}
/**
* Releases this lock in disregard of ownership.
*/
#[Override]
public function forceRelease(): void
{
$this->collection->deleteOne([
'_id' => $this->name,
]);
}
/** Creates a TTL index that automatically deletes expired objects. */
public function createTTLIndex(): void
{
$this->collection->createIndex(
// UTCDateTime field that holds the expiration date
['expires_at' => 1],
// Delay to remove items after expiration
['expireAfterSeconds' => 0],
);
}
/**
* Returns the owner value written into the driver for this lock.
*/
#[Override]
protected function getCurrentOwner(): ?string
{
return $this->collection->findOne(
[
'_id' => $this->name,
'expires_at' => ['$gte' => $this->getUTCDateTime()],
],
['projection' => ['owner' => 1]],
)['owner'] ?? null;
}
private function getUTCDateTime(int $additionalSeconds = 0): UTCDateTime
{
return new UTCDateTime(Carbon::now()->addSeconds($additionalSeconds));
}
}
================================================
FILE: src/Cache/MongoStore.php
================================================
collection = $this->connection->getCollection($this->collectionName);
}
/**
* Get a lock instance.
*
* @param string $name
* @param int $seconds
* @param string|null $owner
*/
#[Override]
public function lock($name, $seconds = 0, $owner = null): MongoLock
{
return new MongoLock(
($this->lockConnection ?? $this->connection)->getCollection($this->lockCollectionName),
$this->prefix . $name,
$seconds ?: $this->defaultLockTimeoutInSeconds,
$owner,
$this->lockLottery,
);
}
/**
* Restore a lock instance using the owner identifier.
*/
#[Override]
public function restoreLock($name, $owner): MongoLock
{
return $this->lock($name, 0, $owner);
}
/**
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param int $seconds
*/
#[Override]
public function put($key, $value, $seconds): bool
{
$result = $this->collection->updateOne(
[
'_id' => $this->prefix . $key,
],
[
'$set' => [
'value' => $this->serialize($value),
'expires_at' => $this->getUTCDateTime($seconds),
],
],
['upsert' => true],
);
return $result->getUpsertedCount() > 0 || $result->getModifiedCount() > 0;
}
/**
* Store an item in the cache if the key doesn't exist.
*
* @param string $key
* @param mixed $value
* @param int $seconds
*/
public function add($key, $value, $seconds): bool
{
$isExpired = ['$lte' => ['$expires_at', $this->getUTCDateTime()]];
$result = $this->collection->updateOne(
[
'_id' => $this->prefix . $key,
],
[
[
'$set' => [
'value' => [
'$cond' => [
'if' => $isExpired,
'then' => $this->serialize($value),
'else' => '$value',
],
],
'expires_at' => [
'$cond' => [
'if' => $isExpired,
'then' => $this->getUTCDateTime($seconds),
'else' => '$expires_at',
],
],
],
],
],
['upsert' => true],
);
return $result->getUpsertedCount() > 0 || $result->getModifiedCount() > 0;
}
/**
* Retrieve an item from the cache by key.
*
* @param string $key
*/
#[Override]
public function get($key): mixed
{
$result = $this->collection->findOne(
['_id' => $this->prefix . $key],
['projection' => ['value' => 1, 'expires_at' => 1]],
);
if (! $result) {
return null;
}
if ($result['expires_at'] <= $this->getUTCDateTime()) {
$this->forgetIfExpired($key);
return null;
}
return $this->unserialize($result['value']);
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param int|float $value
*/
#[Override]
public function increment($key, $value = 1): int|float|false
{
$result = $this->collection->findOneAndUpdate(
[
'_id' => $this->prefix . $key,
],
[
'$inc' => ['value' => $value],
],
[
'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER,
],
);
if (! $result) {
return false;
}
if ($result['expires_at'] <= $this->getUTCDateTime()) {
$this->forgetIfExpired($key);
return false;
}
return $result['value'];
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param int|float $value
*/
#[Override]
public function decrement($key, $value = 1): int|float|false
{
return $this->increment($key, -1 * $value);
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
*/
#[Override]
public function forever($key, $value): bool
{
return $this->put($key, $value, self::TEN_YEARS_IN_SECONDS);
}
/**
* Remove an item from the cache.
*
* @param string $key
*/
#[Override]
public function forget($key): bool
{
$result = $this->collection->deleteOne([
'_id' => $this->prefix . $key,
]);
return $result->getDeletedCount() > 0;
}
/**
* Remove an item from the cache if it is expired.
*
* @param string $key
*/
public function forgetIfExpired($key): bool
{
$result = $this->collection->deleteOne([
'_id' => $this->prefix . $key,
'expires_at' => ['$lte' => $this->getUTCDateTime()],
]);
return $result->getDeletedCount() > 0;
}
/**
* Extend the expiration time of an item in the cache.
*
* @param string $key
* @param int $seconds
*/
public function touch($key, $seconds): bool
{
$result = $this->collection->updateOne(
[
'_id' => $this->prefix . $key,
'expires_at' => ['$gt' => $this->getUTCDateTime()],
],
[
'$set' => ['expires_at' => $this->getUTCDateTime($seconds)],
],
);
return $result->getModifiedCount() > 0;
}
public function flush(): bool
{
$this->collection->deleteMany([]);
return true;
}
public function getPrefix(): string
{
return $this->prefix;
}
/** Creates a TTL index that automatically deletes expired objects. */
public function createTTLIndex(): void
{
$this->collection->createIndex(
// UTCDateTime field that holds the expiration date
['expires_at' => 1],
// Delay to remove items after expiration
['expireAfterSeconds' => 0],
);
}
private function serialize($value): string|int|float
{
// Don't serialize numbers, so they can be incremented
if (is_int($value) || is_float($value)) {
return $value;
}
return serialize($value);
}
private function unserialize($value): mixed
{
if (! is_string($value) || ! str_contains($value, ';')) {
return $value;
}
return unserialize($value);
}
private function getUTCDateTime(int $additionalSeconds = 0): UTCDateTime
{
return new UTCDateTime(Carbon::now()->addSeconds($additionalSeconds));
}
}
================================================
FILE: src/CommandSubscriber.php
================================================
*/
private array $commands = [];
public function __construct(private Connection $connection)
{
}
#[Override]
public function commandStarted(CommandStartedEvent $event): void
{
$this->commands[$event->getOperationId()] = $event;
}
#[Override]
public function commandFailed(CommandFailedEvent $event): void
{
$this->logQuery($event);
}
#[Override]
public function commandSucceeded(CommandSucceededEvent $event): void
{
$this->logQuery($event);
}
private function logQuery(CommandSucceededEvent|CommandFailedEvent $event): void
{
$startedEvent = $this->commands[$event->getOperationId()];
unset($this->commands[$event->getOperationId()]);
$command = [];
foreach (get_object_vars($startedEvent->getCommand()) as $key => $value) {
if ($key[0] !== '$' && ! in_array($key, ['lsid', 'txnNumber'])) {
$command[$key] = $value;
}
}
$this->connection->logQuery(Document::fromPHP($command)->toCanonicalExtendedJSON(), [], $event->getDurationMicros() / 1000);
}
}
================================================
FILE: src/Concerns/ManagesTransactions.php
================================================
session;
}
private function getSessionOrCreate(): Session
{
if ($this->session === null) {
$this->session = $this->getClient()->startSession();
}
return $this->session;
}
private function getSessionOrThrow(): Session
{
$session = $this->getSession();
if ($session === null) {
throw new RuntimeException('There is no active session.');
}
return $session;
}
/**
* Starts a transaction on the active session. An active session will be created if none exists.
*/
public function beginTransaction(array $options = []): void
{
$this->runCallbacksBeforeTransaction();
$this->getSessionOrCreate()->startTransaction($options);
$this->handleInitialTransactionState();
}
private function handleInitialTransactionState(): void
{
$this->transactions = 1;
$this->transactionsManager?->begin(
$this->getName(),
$this->transactions,
);
$this->fireConnectionEvent('beganTransaction');
}
/**
* Commit transaction in this session.
*/
public function commit(): void
{
$this->fireConnectionEvent('committing');
$this->getSessionOrThrow()->commitTransaction();
$this->handleCommitState();
}
private function handleCommitState(): void
{
[$levelBeingCommitted, $this->transactions] = [
$this->transactions,
max(0, $this->transactions - 1),
];
$this->transactionsManager?->commit(
$this->getName(),
$levelBeingCommitted,
$this->transactions,
);
$this->fireConnectionEvent('committed');
}
/**
* Abort transaction in this session.
*/
public function rollBack($toLevel = null): void
{
$session = $this->getSessionOrThrow();
if ($session->isInTransaction()) {
$session->abortTransaction();
}
$this->handleRollbackState();
}
private function handleRollbackState(): void
{
$this->transactions = 0;
$this->transactionsManager?->rollback(
$this->getName(),
$this->transactions,
);
$this->fireConnectionEvent('rollingBack');
}
private function runCallbacksBeforeTransaction(): void
{
// ToDo: remove conditional once we stop supporting Laravel 10.x
if (property_exists(Connection::class, 'beforeStartingTransaction')) {
foreach ($this->beforeStartingTransaction as $beforeTransactionCallback) {
$beforeTransactionCallback($this);
}
}
}
/**
* Static transaction function realize the with_transaction functionality provided by MongoDB.
*
* @param int $attempts
*
* @throws Throwable
*/
public function transaction(Closure $callback, $attempts = 1, array $options = []): mixed
{
$attemptsLeft = $attempts;
$callbackResult = null;
$throwable = null;
$callbackFunction = function (Session $session) use ($callback, &$attemptsLeft, &$callbackResult, &$throwable) {
$attemptsLeft--;
if ($attemptsLeft < 0) {
$session->abortTransaction();
$this->handleRollbackState();
return;
}
$this->runCallbacksBeforeTransaction();
$this->handleInitialTransactionState();
// Catch, store, and re-throw any exception thrown during execution
// of the callable. The last exception is re-thrown if the transaction
// was aborted because the number of callback attempts has been exceeded.
try {
$callbackResult = $callback($this);
$this->fireConnectionEvent('committing');
} catch (Throwable $throwable) {
throw $throwable;
}
};
with_transaction($this->getSessionOrCreate(), $callbackFunction, $options);
if ($attemptsLeft < 0 && $throwable) {
$this->handleRollbackState();
throw $throwable;
}
$this->handleCommitState();
return $callbackResult;
}
}
================================================
FILE: src/Connection.php
================================================
config = $config;
// Build the connection string
$dsn = $this->getDsn($config);
// You can pass options directly to the MongoDB constructor
$options = $config['options'] ?? [];
// Create the connection
$this->connection = $this->createConnection($dsn, $config, $options);
$this->database = $this->getDefaultDatabaseName($dsn, $config);
// Select database
$this->db = $this->connection->getDatabase($this->database);
$this->tablePrefix = $config['prefix'] ?? '';
$this->useDefaultPostProcessor();
$this->useDefaultSchemaGrammar();
$this->useDefaultQueryGrammar();
$this->renameEmbeddedIdField = $config['rename_embedded_id_field'] ?? true;
}
/**
* Begin a fluent query against a database collection.
*
* @param string $table The name of the MongoDB collection
* @param string|null $as Ignored. Not supported by MongoDB
*
* @return Query\Builder
*/
#[Override]
public function table($table, $as = null)
{
$query = new Query\Builder($this, $this->getQueryGrammar(), $this->getPostProcessor());
return $query->from($table);
}
/**
* Get a MongoDB collection.
*
* @param string $name
*
* @return Collection
*/
public function getCollection($name): Collection
{
return $this->db->selectCollection($this->tablePrefix . $name);
}
/** @inheritdoc */
#[Override]
public function getSchemaBuilder()
{
return new Schema\Builder($this);
}
/**
* Get the MongoDB database object.
*
* @deprecated since mongodb/laravel-mongodb:5.2, use getDatabase() instead
*
* @return Database
*/
public function getMongoDB()
{
trigger_error(sprintf('Since mongodb/laravel-mongodb:5.2, Method "%s()" is deprecated, use "getDatabase()" instead.', __FUNCTION__), E_USER_DEPRECATED);
return $this->db;
}
/**
* Get the MongoDB database object.
*
* @param string|null $name Name of the database, if not provided the default database will be returned.
*
* @return Database
*/
public function getDatabase(?string $name = null): Database
{
if ($name && $name !== $this->database) {
return $this->connection->getDatabase($name);
}
return $this->db;
}
/**
* Return MongoDB object.
*
* @deprecated since mongodb/laravel-mongodb:5.2, use getClient() instead
*
* @return Client
*/
public function getMongoClient()
{
trigger_error(sprintf('Since mongodb/laravel-mongodb:5.2, method "%s()" is deprecated, use "getClient()" instead.', __FUNCTION__), E_USER_DEPRECATED);
return $this->getClient();
}
/**
* Get the MongoDB client.
*/
public function getClient(): ?Client
{
return $this->connection;
}
/** @inheritdoc */
#[Override]
public function enableQueryLog()
{
parent::enableQueryLog();
if (! $this->commandSubscriber) {
$this->commandSubscriber = new CommandSubscriber($this);
$this->connection->addSubscriber($this->commandSubscriber);
}
}
#[Override]
public function disableQueryLog()
{
parent::disableQueryLog();
if ($this->commandSubscriber) {
$this->connection->removeSubscriber($this->commandSubscriber);
$this->commandSubscriber = null;
}
}
#[Override]
protected function withFreshQueryLog($callback)
{
try {
return parent::withFreshQueryLog($callback);
} finally {
// The parent method enable query log using enableQueryLog()
// but disables it by setting $loggingQueries to false. We need to
// remove the subscriber for performance.
if (! $this->loggingQueries) {
$this->disableQueryLog();
}
}
}
/**
* Get the name of the default database based on db config or try to detect it from dsn.
*
* @throws InvalidArgumentException
*/
protected function getDefaultDatabaseName(string $dsn, array $config): string
{
if (empty($config['database'])) {
if (! preg_match('/^mongodb(?:[+]srv)?:\\/\\/.+?\\/([^?&]+)/s', $dsn, $matches)) {
throw new InvalidArgumentException('Database is not properly configured.');
}
$config['database'] = $matches[1];
}
return $config['database'];
}
/**
* Create a new MongoDB connection.
*/
protected function createConnection(string $dsn, array $config, array $options): Client
{
// By default driver options is an empty array.
$driverOptions = [];
if (isset($config['driver_options']) && is_array($config['driver_options'])) {
$driverOptions = $config['driver_options'];
}
$driverOptions['driver'] = [
'name' => 'laravel-mongodb',
'version' => self::getVersion(),
];
// Check if the credentials are not already set in the options
if (! isset($options['username']) && ! empty($config['username'])) {
$options['username'] = $config['username'];
}
if (! isset($options['password']) && ! empty($config['password'])) {
$options['password'] = $config['password'];
}
if (isset($config['name'])) {
$driverOptions += ['connectionName' => $config['name']];
}
return new Client($dsn, $options, $driverOptions);
}
/**
* Check the connection to the MongoDB server
*
* @throws ConnectionException if connection to the server fails (for reasons other than authentication).
* @throws AuthenticationException if authentication is needed and fails.
* @throws RuntimeException if a server matching the read preference could not be found.
*/
public function ping(): void
{
$this->getClient()->getManager()->selectServer(new ReadPreference(ReadPreference::PRIMARY_PREFERRED));
}
/** @inheritdoc */
public function disconnect()
{
$this->disableQueryLog();
$this->connection = null;
}
/**
* Determine if the given configuration array has a dsn string.
*
* @deprecated
*/
protected function hasDsnString(array $config): bool
{
return ! empty($config['dsn']);
}
/**
* Get the DSN string form configuration.
*/
protected function getDsnString(array $config): string
{
return $config['dsn'];
}
/**
* Get the DSN string for a host / port configuration.
*/
protected function getHostDsn(array $config): string
{
// Treat host option as array of hosts
$hosts = is_array($config['host']) ? $config['host'] : [$config['host']];
foreach ($hosts as &$host) {
// ipv6
if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$host = '[' . $host . ']';
if (! empty($config['port'])) {
$host .= ':' . $config['port'];
}
} else {
// Check if we need to add a port to the host
if (! str_contains($host, ':') && ! empty($config['port'])) {
$host .= ':' . $config['port'];
}
}
}
// Check if we want to authenticate against a specific database.
$authDatabase = isset($config['options']) && ! empty($config['options']['database']) ? $config['options']['database'] : null;
return 'mongodb://' . implode(',', $hosts) . ($authDatabase ? '/' . $authDatabase : '');
}
/**
* Create a DSN string from a configuration.
*/
protected function getDsn(array $config): string
{
if (! empty($config['dsn'])) {
return $this->getDsnString($config);
}
if (! empty($config['host'])) {
return $this->getHostDsn($config);
}
throw new InvalidArgumentException('MongoDB connection configuration requires "dsn" or "host" key.');
}
/** @inheritdoc */
#[Override]
public function getDriverName()
{
return 'mongodb';
}
/** @inheritdoc */
public function getDriverTitle()
{
return 'MongoDB';
}
/** @inheritdoc */
#[Override]
protected function getDefaultPostProcessor()
{
return new Query\Processor();
}
/** @inheritdoc */
#[Override]
protected function getDefaultQueryGrammar()
{
// Argument added in Laravel 12
return new Query\Grammar($this);
}
/** @inheritdoc */
#[Override]
protected function getDefaultSchemaGrammar()
{
// Argument added in Laravel 12
return new Schema\Grammar($this);
}
/**
* Set database.
*/
public function setDatabase(Database $db)
{
$this->db = $db;
}
/** @inheritdoc */
public function threadCount()
{
$status = $this->db->command(['serverStatus' => 1])->toArray();
return $status[0]['connections']['current'];
}
/**
* Dynamically pass methods to the connection.
*
* @param string $method
* @param array $parameters
*
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->db->$method(...$parameters);
}
/** Set whether to rename "id" field into "_id" for embedded documents. */
public function setRenameEmbeddedIdField(bool $rename): void
{
$this->renameEmbeddedIdField = $rename;
}
/** Get whether to rename "id" field into "_id" for embedded documents. */
public function getRenameEmbeddedIdField(): bool
{
return $this->renameEmbeddedIdField;
}
/**
* Return the server version of one of the MongoDB servers: primary for
* replica sets and standalone, and the selected server for sharded clusters.
*
* @internal
*/
public function getServerVersion(): string
{
return $this->db->command(['buildInfo' => 1])->toArray()[0]['version'];
}
private static function getVersion(): string
{
return self::$version ?? self::lookupVersion();
}
private static function lookupVersion(): string
{
try {
try {
return self::$version = InstalledVersions::getPrettyVersion('mongodb/laravel-mongodb') ?? 'unknown';
} catch (OutOfBoundsException) {
return self::$version = InstalledVersions::getPrettyVersion('jenssegers/mongodb') ?? 'unknown';
}
} catch (Throwable) {
return self::$version = 'error';
}
}
}
================================================
FILE: src/Eloquent/Builder.php
================================================
toBase()->aggregate($function, $columns);
return $result ?: $this;
}
/**
* Performs a full-text search of the field or fields in an Atlas collection.
*
* @see https://www.mongodb.com/docs/atlas/atlas-search/aggregation-stages/search/
*
* @return Collection
*/
public function search(
SearchOperatorInterface|array $operator,
?string $index = null,
?array $highlight = null,
?bool $concurrent = null,
?string $count = null,
?string $searchAfter = null,
?string $searchBefore = null,
?bool $scoreDetails = null,
?array $sort = null,
?bool $returnStoredSource = null,
?array $tracking = null,
): Collection {
$results = $this->toBase()->search($operator, $index, $highlight, $concurrent, $count, $searchAfter, $searchBefore, $scoreDetails, $sort, $returnStoredSource, $tracking);
return $this->model->hydrate($results->all());
}
/**
* Performs a semantic search on data in your Atlas Vector Search index.
* NOTE: $vectorSearch is only available for MongoDB Atlas clusters, and is not available for self-managed deployments.
*
* @see https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/
*
* @return Collection
*/
public function vectorSearch(
string $index,
string $path,
array $queryVector,
int $limit,
bool $exact = false,
QueryInterface|array $filter = [],
int|null $numCandidates = null,
): Collection {
$results = $this->toBase()->vectorSearch($index, $path, $queryVector, $limit, $exact, $filter, $numCandidates);
return $this->model->hydrate($results->all());
}
/**
* @param array $options
*
* @inheritdoc
*/
#[Override]
public function update(array $values, array $options = [])
{
// Intercept operations on embedded models and delegate logic
// to the parent relation instance.
$relation = $this->model->getParentRelation();
if ($relation) {
$relation->performUpdate($this->model, $values);
return 1;
}
return $this->toBase()->update($this->addUpdatedAtColumn($values), $options);
}
/** @inheritdoc */
public function insert(array $values)
{
// Intercept operations on embedded models and delegate logic
// to the parent relation instance.
$relation = $this->model->getParentRelation();
if ($relation) {
$relation->performInsert($this->model, $values);
return true;
}
return parent::insert($values);
}
/** @inheritdoc */
public function insertGetId(array $values, $sequence = null)
{
// Intercept operations on embedded models and delegate logic
// to the parent relation instance.
$relation = $this->model->getParentRelation();
if ($relation) {
$relation->performInsert($this->model, $values);
return $this->model->getKey();
}
return parent::insertGetId($values, $sequence);
}
/** @inheritdoc */
public function delete()
{
// Intercept operations on embedded models and delegate logic
// to the parent relation instance.
$relation = $this->model->getParentRelation();
if ($relation) {
$relation->performDelete($this->model);
return $this->model->getKey();
}
return parent::delete();
}
/** @inheritdoc */
public function increment($column, $amount = 1, array $extra = [])
{
// Intercept operations on embedded models and delegate logic
// to the parent relation instance.
$relation = $this->model->getParentRelation();
if ($relation) {
$value = $this->model->{$column};
// When doing increment and decrements, Eloquent will automatically
// sync the original attributes. We need to change the attribute
// temporary in order to trigger an update query.
$this->model->{$column} = null;
$this->model->syncOriginalAttribute($column);
return $this->model->update([$column => $value]);
}
return parent::increment($column, $amount, $extra);
}
/** @inheritdoc */
public function decrement($column, $amount = 1, array $extra = [])
{
// Intercept operations on embedded models and delegate logic
// to the parent relation instance.
$relation = $this->model->getParentRelation();
if ($relation) {
$value = $this->model->{$column};
// When doing increment and decrements, Eloquent will automatically
// sync the original attributes. We need to change the attribute
// temporary in order to trigger an update query.
$this->model->{$column} = null;
$this->model->syncOriginalAttribute($column);
return $this->model->update([$column => $value]);
}
return parent::decrement($column, $amount, $extra);
}
/**
* @param (Closure():T)|Expression|null $value
*
* @return ($value is Closure ? T : ($value is null ? Collection : Expression))
*
* @template T
*/
public function raw($value = null)
{
// Get raw results from the query builder.
$results = $this->query->raw($value);
// Convert MongoCursor results to a collection of models.
if ($results instanceof CursorInterface) {
$results->setTypeMap(['root' => 'array', 'document' => 'array', 'array' => 'array']);
$results = array_map(fn ($document) => $this->query->aliasIdForResult($document), iterator_to_array($results));
return $this->model->hydrate($results);
}
// Convert MongoDB Document to a single object.
if (is_object($results) && (property_exists($results, '_id') || property_exists($results, 'id'))) {
$results = (array) match (true) {
$results instanceof BSONDocument => $results->getArrayCopy(),
$results instanceof Document => $results->toPHP(['root' => 'array', 'document' => 'array', 'array' => 'array']),
default => $results,
};
}
// The result is a single object.
if (is_array($results) && (array_key_exists('_id', $results) || array_key_exists('id', $results))) {
$results = $this->query->aliasIdForResult($results);
return $this->model->newFromBuilder($results);
}
return $results;
}
#[Override]
public function firstOrCreate(array $attributes = [], Closure|array $values = [])
{
$instance = (clone $this)->where($attributes)->first();
if ($instance !== null) {
return $instance;
}
// createOrFirst is not supported in transaction.
if ($this->getConnection()->getSession()?->isInTransaction()) {
return $this->create(array_replace($attributes, value($values)));
}
return $this->createOrFirst($attributes, $values);
}
#[Override]
public function createOrFirst(array $attributes = [], Closure|array $values = [])
{
// The duplicate key error would abort the transaction. Using the regular firstOrCreate in that case.
if ($this->getConnection()->getSession()?->isInTransaction()) {
return $this->firstOrCreate($attributes, $values);
}
try {
return $this->create(array_replace($attributes, value($values)));
} catch (BulkWriteException $e) {
if ($e->getCode() === self::DUPLICATE_KEY_ERROR) {
return $this->where($attributes)->first() ?? throw $e;
}
throw $e;
}
}
/**
* Add the "updated at" column to an array of values.
* TODO Remove if https://github.com/laravel/framework/commit/6484744326531829341e1ff886cc9b628b20d73e
* will be reverted
* Issue in laravel/frawework https://github.com/laravel/framework/issues/27791.
*/
#[Override]
protected function addUpdatedAtColumn(array $values)
{
if (! $this->model->usesTimestamps() || $this->model->getUpdatedAtColumn() === null) {
return $values;
}
$column = $this->model->getUpdatedAtColumn();
if (isset($values['$set'][$column])) {
return $values;
}
$values = array_replace(
[$column => $this->model->freshTimestampString()],
$values,
);
return $values;
}
public function getConnection(): Connection
{
return $this->query->getConnection();
}
/** @inheritdoc */
#[Override]
protected function ensureOrderForCursorPagination($shouldReverse = false)
{
if (empty($this->query->orders)) {
$this->enforceOrderBy();
}
if ($shouldReverse) {
$this->query->orders = collect($this->query->orders)
->map(static fn (int $direction) => $direction === 1 ? -1 : 1)
->toArray();
}
return collect($this->query->orders)
->map(static fn ($direction, $column) => [
'column' => $column,
'direction' => $direction === 1 ? 'asc' : 'desc',
])->values();
}
}
================================================
FILE: src/Eloquent/Casts/BinaryUuid.php
================================================
getType() !== Binary::TYPE_UUID) {
return $value;
}
$base16Uuid = bin2hex($value->getData());
return sprintf(
'%s-%s-%s-%s-%s',
substr($base16Uuid, 0, 8),
substr($base16Uuid, 8, 4),
substr($base16Uuid, 12, 4),
substr($base16Uuid, 16, 4),
substr($base16Uuid, 20, 12),
);
}
/**
* Prepare the given value for storage.
*
* @param Model $model
* @param mixed $value
*
* @return Binary
*/
public function set($model, string $key, $value, array $attributes)
{
if ($value instanceof Binary) {
return $value;
}
if (is_string($value) && strlen($value) === 16) {
return new Binary($value, Binary::TYPE_UUID);
}
return new Binary(hex2bin(str_replace('-', '', $value)), Binary::TYPE_UUID);
}
}
================================================
FILE: src/Eloquent/Casts/ObjectId.php
================================================
attributes['id'] ?? $this->attributes['_id'] ?? null;
// Convert ObjectID to string.
if ($value instanceof ObjectID) {
return (string) $value;
}
if ($value instanceof Binary) {
return (string) $value->getData();
}
return $value;
}
/** @inheritdoc */
public function getQualifiedKeyName()
{
return $this->getKeyName();
}
/**
* Convert a DateTimeInterface (including Carbon) to a storable UTCDateTime.
*
* @see HasAttributes::fromDateTime()
*
* @param mixed $value
*/
public function fromDateTime($value): UTCDateTime
{
// If the value is already a UTCDateTime instance, we don't need to parse it.
if ($value instanceof UTCDateTime) {
return $value;
}
// Let Eloquent convert the value to a DateTime instance.
if (! $value instanceof DateTimeInterface) {
$value = parent::asDateTime($value);
}
return new UTCDateTime($value);
}
/**
* Return a timestamp as Carbon object.
*
* @see HasAttributes::asDateTime()
*
* @param mixed $value
*/
protected function asDateTime($value): DateTimeInterface
{
// Convert UTCDateTime instances to Carbon.
if ($value instanceof UTCDateTime) {
return Date::instance($value->toDateTime())
->setTimezone(new DateTimeZone(date_default_timezone_get()));
}
return parent::asDateTime($value);
}
/** @inheritdoc */
public function getDateFormat()
{
return $this->dateFormat ?: 'Y-m-d H:i:s';
}
/** @inheritdoc */
public function freshTimestamp()
{
return new UTCDateTime(Date::now());
}
/** @inheritdoc */
public function getAttribute($key)
{
if (! $key) {
return null;
}
$key = (string) $key;
// An unset attribute is null or throw an exception.
if (isset($this->unset[$key])) {
return $this->throwMissingAttributeExceptionIfApplicable($key);
}
// Dot notation support.
if (str_contains($key, '.') && Arr::has($this->attributes, $key)) {
return $this->getAttributeValue($key);
}
// This checks for embedded relation support.
// Ignore methods defined in the class Eloquent Model or in this trait.
if (
method_exists($this, $key)
&& ! method_exists(Model::class, $key)
&& ! method_exists(DocumentModel::class, $key)
&& ! $this->hasAttributeGetMutator($key)
) {
return $this->getRelationValue($key);
}
return parent::getAttribute($key);
}
/** @inheritdoc */
protected function transformModelValue($key, $value)
{
$value = parent::transformModelValue($key, $value);
// Casting attributes to any of date types, will convert that attribute
// to a Carbon or CarbonImmutable instance.
// @see Model::setAttribute()
if ($this->hasCast($key) && $value instanceof CarbonInterface) {
$value->settings(array_replace($value->getSettings(), ['toStringFormat' => $this->getDateFormat()]));
// "date" cast resets the time to 00:00:00.
$castType = $this->getCasts()[$key];
if (str_starts_with($castType, 'date:') || str_starts_with($castType, 'immutable_date:')) {
$value = $value->startOfDay();
}
}
return $value;
}
/** @inheritdoc */
protected function getCastType($key)
{
$castType = $this->getCasts()[$key];
if ($this->isCustomDateTimeCast($castType) || $this->isImmutableCustomDateTimeCast($castType)) {
$this->setDateFormat(Str::after($castType, ':'));
}
return parent::getCastType($key);
}
/** @inheritdoc */
protected function getAttributeFromArray($key)
{
$key = (string) $key;
// Support keys in dot notation.
if (str_contains($key, '.')) {
return Arr::get($this->attributes, $key);
}
return parent::getAttributeFromArray($key);
}
/** @inheritdoc */
public function setAttribute($key, $value)
{
$key = (string) $key;
$casts = $this->getCasts();
if (array_key_exists($key, $casts)) {
$castType = $this->getCastType($key);
$castOptions = Str::after($casts[$key], ':');
// Can add more native mongo type casts here.
$value = match ($castType) {
'decimal' => $this->fromDecimal($value, $castOptions),
default => $value,
};
}
// Convert _id to ObjectID.
if (($key === '_id' || $key === 'id') && is_string($value) && strlen($value) === 24) {
$value = $this->newBaseQueryBuilder()->convertKey($value);
}
// Support keys in dot notation.
if (str_contains($key, '.')) {
// Store to a temporary key, then move data to the actual key
parent::setAttribute('__LARAVEL_TEMPORARY_KEY__', $value);
Arr::set($this->attributes, $key, $this->attributes['__LARAVEL_TEMPORARY_KEY__'] ?? null);
unset($this->attributes['__LARAVEL_TEMPORARY_KEY__']);
return $this;
}
// Setting an attribute cancels the unset operation.
unset($this->unset[$key]);
return parent::setAttribute($key, $value);
}
/**
* @param mixed $value
*
* @inheritdoc
*/
protected function asDecimal($value, $decimals)
{
// Convert BSON to string.
if ($this->isBSON($value)) {
if ($value instanceof Binary) {
$value = $value->getData();
} elseif ($value instanceof Stringable) {
$value = (string) $value;
} else {
throw new MathException('BSON type ' . $value::class . ' cannot be converted to string');
}
}
return parent::asDecimal($value, $decimals);
}
/**
* Change to mongo native for decimal cast.
*
* @param mixed $value
* @param int $decimals
*
* @return Decimal128
*/
protected function fromDecimal($value, $decimals)
{
return new Decimal128($this->asDecimal($value, $decimals));
}
/** @inheritdoc */
public function attributesToArray()
{
$attributes = parent::attributesToArray();
// Because the original Eloquent never returns objects, we convert
// MongoDB related objects to a string representation. This kind
// of mimics the SQL behaviour so that dates are formatted
// nicely when your models are converted to JSON.
foreach ($attributes as $key => &$value) {
if ($value instanceof ObjectID) {
$value = (string) $value;
} elseif ($value instanceof Binary) {
$value = (string) $value->getData();
}
}
return $attributes;
}
/** @inheritdoc */
public function getCasts()
{
return $this->casts;
}
/** @inheritdoc */
public function getDirty()
{
$dirty = parent::getDirty();
// The specified value in the $unset expression does not impact the operation.
if ($this->unset !== []) {
$dirty['$unset'] = $this->unset;
}
return $dirty;
}
/** @inheritdoc */
public function originalIsEquivalent($key)
{
if (! array_key_exists($key, $this->original)) {
return false;
}
// Calling unset on an attribute marks it as "not equivalent".
if (isset($this->unset[$key])) {
return false;
}
$attribute = Arr::get($this->attributes, $key);
$original = Arr::get($this->original, $key);
if ($attribute === $original) {
return true;
}
if ($attribute === null) {
return false;
}
if ($this->isDateAttribute($key)) {
$attribute = $attribute instanceof UTCDateTime ? $this->asDateTime($attribute) : $attribute;
$original = $original instanceof UTCDateTime ? $this->asDateTime($original) : $original;
// Comparison on DateTimeInterface values
// phpcs:disable SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator
return $attribute == $original;
}
if ($this->hasCast($key, static::$primitiveCastTypes)) {
return $this->castAttribute($key, $attribute) ===
$this->castAttribute($key, $original);
}
return is_numeric($attribute) && is_numeric($original)
&& strcmp((string) $attribute, (string) $original) === 0;
}
/** @inheritdoc */
public function offsetUnset($offset): void
{
$offset = (string) $offset;
if (str_contains($offset, '.')) {
// Update the field in the subdocument
Arr::forget($this->attributes, $offset);
} else {
parent::offsetUnset($offset);
// Force unsetting even if the attribute is not set.
// End user can optimize DB calls by checking if the attribute is set before unsetting it.
$this->unset[$offset] = true;
}
}
/** @inheritdoc */
public function offsetSet($offset, $value): void
{
parent::offsetSet($offset, $value);
// Setting an attribute cancels the unset operation.
unset($this->unset[$offset]);
}
/**
* Remove one or more fields.
*
* @deprecated Use unset() instead.
*
* @param string|string[] $columns
*
* @return void
*/
public function drop($columns)
{
$this->unset($columns);
}
/**
* Remove one or more fields.
*
* @param string|string[] $columns
*
* @return void
*/
public function unset($columns)
{
$columns = Arr::wrap($columns);
// Unset attributes
foreach ($columns as $column) {
$this->__unset($column);
}
}
/** @inheritdoc */
public function push()
{
$parameters = func_get_args();
if ($parameters) {
$unique = false;
if (count($parameters) === 3) {
[$column, $values, $unique] = $parameters;
} else {
[$column, $values] = $parameters;
}
// Do batch push by default.
$values = Arr::wrap($values);
$query = $this->setKeysForSaveQuery($this->newQuery());
$this->pushAttributeValues($column, $values, $unique);
return $query->push($column, $values, $unique);
}
return parent::push();
}
/**
* Remove one or more values from an array.
*
* @param string $column
* @param mixed $values
*
* @return mixed
*/
public function pull($column, $values)
{
// Do batch pull by default.
$values = Arr::wrap($values);
$query = $this->setKeysForSaveQuery($this->newQuery());
$this->pullAttributeValues($column, $values);
return $query->pull($column, $values);
}
/**
* Append one or more values to the underlying attribute value and sync with original.
*
* @param string $column
* @param bool $unique
*/
protected function pushAttributeValues($column, array $values, $unique = false)
{
$current = $this->getAttributeFromArray($column) ?: [];
foreach ($values as $value) {
// Don't add duplicate values when we only want unique values.
if ($unique && (! is_array($current) || in_array($value, $current))) {
continue;
}
$current[] = $value;
}
$this->attributes[$column] = $current;
$this->syncOriginalAttribute($column);
}
/**
* Remove one or more values to the underlying attribute value and sync with original.
*
* @param string $column
*/
protected function pullAttributeValues($column, array $values)
{
$current = $this->getAttributeFromArray($column) ?: [];
if (is_array($current)) {
foreach ($values as $value) {
$keys = array_keys($current, $value);
foreach ($keys as $key) {
unset($current[$key]);
}
}
}
$this->attributes[$column] = array_values($current);
$this->syncOriginalAttribute($column);
}
/** @inheritdoc */
public function getForeignKey()
{
return Str::snake(class_basename($this)) . '_' . ltrim($this->primaryKey, '_');
}
/**
* Set the parent relation.
*/
public function setParentRelation(Relation $relation)
{
$this->parentRelation = $relation;
}
/**
* Get the parent relation.
*/
public function getParentRelation(): ?Relation
{
return $this->parentRelation ?? null;
}
/** @inheritdoc */
public function newEloquentBuilder($query)
{
return new Builder($query);
}
/** @inheritdoc */
public function qualifyColumn($column)
{
return $column;
}
/** @inheritdoc */
protected function newBaseQueryBuilder()
{
$connection = $this->getConnection();
return new QueryBuilder($connection, $connection->getQueryGrammar(), $connection->getPostProcessor());
}
/** @inheritdoc */
protected function removeTableFromKey($key)
{
return $key;
}
/**
* Get the queueable relationships for the entity.
*
* @return array
*/
public function getQueueableRelations()
{
$relations = [];
foreach ($this->getRelationsWithoutParent() as $key => $relation) {
if (method_exists($this, $key)) {
$relations[] = $key;
}
if ($relation instanceof QueueableCollection) {
foreach ($relation->getQueueableRelations() as $collectionValue) {
$relations[] = $key . '.' . $collectionValue;
}
}
if ($relation instanceof QueueableEntity) {
foreach ($relation->getQueueableRelations() as $entityKey => $entityValue) {
$relations[] = $key . '.' . $entityValue;
}
}
}
return array_unique($relations);
}
/**
* Get loaded relations for the instance without parent.
*
* @return array
*/
protected function getRelationsWithoutParent()
{
$relations = $this->getRelations();
$parentRelation = $this->getParentRelation();
if ($parentRelation) {
unset($relations[$parentRelation->getQualifiedForeignKeyName()]);
}
return $relations;
}
/**
* Checks if column exists on a table. As this is a document model, just return true. This also
* prevents calls to non-existent function Grammar::compileColumnListing().
*
* @param string $key
*
* @return bool
*/
protected function isGuardableColumn($key)
{
return true;
}
/** @inheritdoc */
protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes)
{
foreach ($this->getCasts() as $key => $castType) {
if (! Arr::has($attributes, $key) || Arr::has($mutatedAttributes, $key)) {
continue;
}
$originalValue = Arr::get($attributes, $key);
// Here we will cast the attribute. Then, if the cast is a date or datetime cast
// then we will serialize the date for the array. This will convert the dates
// to strings based on the date format specified for these Eloquent models.
$castValue = $this->castAttribute(
$key,
$originalValue,
);
// If the attribute cast was a date or a datetime, we will serialize the date as
// a string. This allows the developers to customize how dates are serialized
// into an array without affecting how they are persisted into the storage.
if ($castValue !== null && in_array($castType, ['date', 'datetime', 'immutable_date', 'immutable_datetime'])) {
$castValue = $this->serializeDate($castValue);
}
if ($castValue !== null && ($this->isCustomDateTimeCast($castType) || $this->isImmutableCustomDateTimeCast($castType))) {
$castValue = $castValue->format(explode(':', $castType, 2)[1]);
}
if ($castValue instanceof DateTimeInterface && $this->isClassCastable($key)) {
$castValue = $this->serializeDate($castValue);
}
if ($castValue !== null && $this->isClassSerializable($key)) {
$castValue = $this->serializeClassCastableAttribute($key, $castValue);
}
if ($this->isEnumCastable($key) && (! $castValue instanceof Arrayable)) {
$castValue = $castValue !== null ? $this->getStorableEnumValueFromLaravel11($this->getCasts()[$key], $castValue) : null;
}
if ($castValue instanceof Arrayable) {
$castValue = $castValue->toArray();
}
Arr::set($attributes, $key, $castValue);
}
return $attributes;
}
/**
* Duplicate of {@see HasAttributes::getStorableEnumValue()} for Laravel 11 as the signature of the method has
* changed in a non-backward compatible way.
*
* @todo Remove this method when support for Laravel 10 is dropped.
*/
private function getStorableEnumValueFromLaravel11($expectedEnum, $value)
{
if (! $value instanceof $expectedEnum) {
throw new ValueError(sprintf('Value [%s] is not of the expected enum type [%s].', var_export($value, true), $expectedEnum));
}
return $value instanceof BackedEnum
? $value->value
: $value->name;
}
/**
* Is a value a BSON type?
*
* @param mixed $value
*
* @return bool
*/
protected function isBSON(mixed $value): bool
{
return $value instanceof Type;
}
/**
* {@inheritDoc}
*/
public function save(array $options = [])
{
// SQL databases would autoincrement the id field if set to null.
// Apply the same behavior to MongoDB with _id only, otherwise null would be stored.
if (array_key_exists('_id', $this->attributes) && $this->attributes['_id'] === null) {
unset($this->attributes['_id']);
}
if (array_key_exists('id', $this->attributes) && $this->attributes['id'] === null) {
unset($this->attributes['id']);
}
$saved = parent::save($options);
// Clear list of unset fields
$this->unset = [];
return $saved;
}
/**
* {@inheritDoc}
*/
public function refresh()
{
parent::refresh();
// Clear list of unset fields
$this->unset = [];
return $this;
}
}
================================================
FILE: src/Eloquent/EmbedsRelations.php
================================================
newQuery();
$instance = new $related();
return new EmbedsMany($query, $this, $instance, $localKey, $foreignKey, $relation);
}
/**
* Define an embedded one-to-many relationship.
*
* @param class-string $related
* @param string|null $localKey
* @param string|null $foreignKey
* @param string|null $relation
*
* @return EmbedsOne
*/
protected function embedsOne($related, $localKey = null, $foreignKey = null, $relation = null)
{
// If no relation name was given, we will use this debug backtrace to extract
// the calling method's name and use that as the relationship name as most
// of the time this will be what we desire to use for the relationships.
if ($relation === null) {
$relation = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'];
}
if ($localKey === null) {
$localKey = $relation;
}
if ($foreignKey === null) {
$foreignKey = Str::snake(class_basename($this));
}
$query = $this->newQuery();
$instance = new $related();
return new EmbedsOne($query, $this, $instance, $localKey, $foreignKey, $relation);
}
}
================================================
FILE: src/Eloquent/HasSchemaVersion.php
================================================
getAttribute($model::getSchemaVersionKey()) === null) {
$model->setAttribute($model::getSchemaVersionKey(), $model->getModelSchemaVersion());
}
});
static::retrieved(function (self $model) {
$version = $model->getSchemaVersion();
if ($version < $model->getModelSchemaVersion()) {
$model->migrateSchema($version);
$model->setAttribute($model::getSchemaVersionKey(), $model->getModelSchemaVersion());
}
});
}
/**
* Get Current document version, fallback to 0 if not set
*/
public function getSchemaVersion(): int
{
return $this->{static::getSchemaVersionKey()} ?? 0;
}
protected static function getSchemaVersionKey(): string
{
return 'schema_version';
}
protected function getModelSchemaVersion(): int
{
try {
return $this::SCHEMA_VERSION;
} catch (Error) {
throw new LogicException(sprintf('Constant %s::SCHEMA_VERSION is required when using HasSchemaVersion', $this::class));
}
}
}
================================================
FILE: src/Eloquent/HybridRelations.php
================================================
getForeignKey();
$instance = new $related();
$localKey = $localKey ?: $this->getKeyName();
return new HasOne($instance->newQuery(), $this, $foreignKey, $localKey);
}
/**
* Define a polymorphic one-to-one relationship.
*
* @see HasRelationships::morphOne()
*
* @param class-string $related
* @param string $name
* @param string|null $type
* @param string|null $id
* @param string|null $localKey
*
* @return MorphOne
*/
public function morphOne($related, $name, $type = null, $id = null, $localKey = null)
{
// Check if it is a relation with an original model.
if (! Model::isDocumentModel($related)) {
return parent::morphOne($related, $name, $type, $id, $localKey);
}
$instance = new $related();
[$type, $id] = $this->getMorphs($name, $type, $id);
$localKey = $localKey ?: $this->getKeyName();
return new MorphOne($instance->newQuery(), $this, $type, $id, $localKey);
}
/**
* Define a one-to-many relationship.
*
* @see HasRelationships::hasMany()
*
* @param class-string $related
* @param string|null $foreignKey
* @param string|null $localKey
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function hasMany($related, $foreignKey = null, $localKey = null)
{
// Check if it is a relation with an original model.
if (! Model::isDocumentModel($related)) {
return parent::hasMany($related, $foreignKey, $localKey);
}
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related();
$localKey = $localKey ?: $this->getKeyName();
return new HasMany($instance->newQuery(), $this, $foreignKey, $localKey);
}
/**
* Define a polymorphic one-to-many relationship.
*
* @see HasRelationships::morphMany()
*
* @param class-string $related
* @param string $name
* @param string|null $type
* @param string|null $id
* @param string|null $localKey
*
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function morphMany($related, $name, $type = null, $id = null, $localKey = null)
{
// Check if it is a relation with an original model.
if (! Model::isDocumentModel($related)) {
return parent::morphMany($related, $name, $type, $id, $localKey);
}
$instance = new $related();
// Here we will gather up the morph type and ID for the relationship so that we
// can properly query the intermediate table of a relation. Finally, we will
// get the table and create the relationship instances for the developers.
[$type, $id] = $this->getMorphs($name, $type, $id);
$table = $instance->getTable();
$localKey = $localKey ?: $this->getKeyName();
return new MorphMany($instance->newQuery(), $this, $type, $id, $localKey);
}
/**
* Define an inverse one-to-one or many relationship.
*
* @see HasRelationships::belongsTo()
*
* @param class-string $related
* @param string|null $foreignKey
* @param string|null $ownerKey
* @param string|null $relation
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null)
{
// If no relation name was given, we will use this debug backtrace to extract
// the calling method's name and use that as the relationship name as most
// of the time this will be what we desire to use for the relationships.
if ($relation === null) {
$relation = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'];
}
// Check if it is a relation with an original model.
if (! Model::isDocumentModel($related)) {
return parent::belongsTo($related, $foreignKey, $ownerKey, $relation);
}
// If no foreign key was supplied, we can use a backtrace to guess the proper
// foreign key name by using the name of the relationship function, which
// when combined with an "_id" should conventionally match the columns.
if ($foreignKey === null) {
$foreignKey = Str::snake($relation) . '_id';
}
$instance = new $related();
// Once we have the foreign key names, we'll just create a new Eloquent query
// for the related models and returns the relationship instance which will
// actually be responsible for retrieving and hydrating every relations.
$query = $instance->newQuery();
$ownerKey = $ownerKey ?: $instance->getKeyName();
return new BelongsTo($query, $this, $foreignKey, $ownerKey, $relation);
}
/**
* Define a polymorphic, inverse one-to-one or many relationship.
*
* @see HasRelationships::morphTo()
*
* @param string $name
* @param string|null $type
* @param string|null $id
* @param string|null $ownerKey
*
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function morphTo($name = null, $type = null, $id = null, $ownerKey = null)
{
// If no name is provided, we will use the backtrace to get the function name
// since that is most likely the name of the polymorphic interface. We can
// use that to get both the class and foreign key that will be utilized.
if ($name === null) {
$name = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'];
}
[$type, $id] = $this->getMorphs(Str::snake($name), $type, $id);
// If the type value is null it is probably safe to assume we're eager loading
// the relationship. When that is the case we will pass in a dummy query as
// there are multiple types in the morph and we can't use single queries.
$class = $this->$type;
if ($class === null) {
return new MorphTo(
$this->newQuery(),
$this,
$id,
$ownerKey,
$type,
$name,
);
}
// If we are not eager loading the relationship we will essentially treat this
// as a belongs-to style relationship since morph-to extends that class and
// we will pass in the appropriate values so that it behaves as expected.
$class = $this->getActualClassNameForMorph($class);
$instance = new $class();
$ownerKey ??= $instance->getKeyName();
// Check if it is a relation with an original model.
if (! Model::isDocumentModel($instance)) {
return parent::morphTo($name, $type, $id, $ownerKey);
}
return new MorphTo(
$instance->newQuery(),
$this,
$id,
$ownerKey,
$type,
$name,
);
}
/**
* Define a many-to-many relationship.
*
* @see HasRelationships::belongsToMany()
*
* @param class-string $related
* @param string|null $collection
* @param string|null $foreignPivotKey
* @param string|null $relatedPivotKey
* @param string|null $parentKey
* @param string|null $relatedKey
* @param string|null $relation
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function belongsToMany(
$related,
$collection = null,
$foreignPivotKey = null,
$relatedPivotKey = null,
$parentKey = null,
$relatedKey = null,
$relation = null,
) {
// If no relationship name was passed, we will pull backtraces to get the
// name of the calling function. We will use that function name as the
// title of this relation since that is a great convention to apply.
if ($relation === null) {
$relation = $this->guessBelongsToManyRelation();
}
// Check if it is a relation with an original model.
if (! Model::isDocumentModel($related)) {
return parent::belongsToMany(
$related,
$collection,
$foreignPivotKey,
$relatedPivotKey,
$parentKey,
$relatedKey,
$relation,
);
}
// First, we'll need to determine the foreign key and "other key" for the
// relationship. Once we have determined the keys we'll make the query
// instances as well as the relationship instances we need for this.
$foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey() . 's';
$instance = new $related();
$relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey() . 's';
// If no table name was provided, we can guess it by concatenating the two
// models using underscores in alphabetical order. The two model names
// are transformed to snake case from their default CamelCase also.
if ($collection === null) {
$collection = $instance->getTable();
}
// Now we're ready to create a new query builder for the related model and
// the relationship instances for the relation. The relations will set
// appropriate query constraint and entirely manages the hydrations.
$query = $instance->newQuery();
return new BelongsToMany(
$query,
$this,
$collection,
$foreignPivotKey,
$relatedPivotKey,
$parentKey ?: $this->getKeyName(),
$relatedKey ?: $instance->getKeyName(),
$relation,
);
}
/**
* Define a morph-to-many relationship.
*
* @param class-string $related
* @param string $name
* @param string|null $table
* @param string|null $foreignPivotKey
* @param string|null $relatedPivotKey
* @param string|null $parentKey
* @param string|null $relatedKey
* @param string|null $relation
* @param bool $inverse
*
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
public function morphToMany(
$related,
$name,
$table = null,
$foreignPivotKey = null,
$relatedPivotKey = null,
$parentKey = null,
$relatedKey = null,
$relation = null,
$inverse = false,
) {
// If no relationship name was passed, we will pull backtraces to get the
// name of the calling function. We will use that function name as the
// title of this relation since that is a great convention to apply.
if ($relation === null) {
$relation = $this->guessBelongsToManyRelation();
}
// Check if it is a relation with an original model.
if (! Model::isDocumentModel($related)) {
return parent::morphToMany(
$related,
$name,
$table,
$foreignPivotKey,
$relatedPivotKey,
$parentKey,
$relatedKey,
$relation,
$inverse,
);
}
$instance = new $related();
$foreignPivotKey = $foreignPivotKey ?: $name . '_id';
$relatedPivotKey = $relatedPivotKey ?: Str::plural($instance->getForeignKey());
// Now we're ready to create a new query builder for the related model and
// the relationship instances for this relation. This relation will set
// appropriate query constraints then entirely manage the hydration.
if (! $table) {
$words = preg_split('/(_)/u', $name, -1, PREG_SPLIT_DELIM_CAPTURE);
$lastWord = array_pop($words);
$table = implode('', $words) . Str::plural($lastWord);
}
return new MorphToMany(
$instance->newQuery(),
$this,
$name,
$table,
$foreignPivotKey,
$relatedPivotKey,
$parentKey ?: $this->getKeyName(),
$relatedKey ?: $instance->getKeyName(),
$relation,
$inverse,
);
}
/**
* Define a polymorphic, inverse many-to-many relationship.
*
* @param class-string $related
* @param string $name
* @param string|null $table
* @param string|null $foreignPivotKey
* @param string|null $relatedPivotKey
* @param string|null $parentKey
* @param string|null $relatedKey
* @param string|null $relation
*
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
public function morphedByMany(
$related,
$name,
$table = null,
$foreignPivotKey = null,
$relatedPivotKey = null,
$parentKey = null,
$relatedKey = null,
$relation = null,
) {
// If the related model is an instance of eloquent model class, leave pivot keys
// as default. It's necessary for supporting hybrid relationship
if (Model::isDocumentModel($related)) {
// For the inverse of the polymorphic many-to-many relations, we will change
// the way we determine the foreign and other keys, as it is the opposite
// of the morph-to-many method since we're figuring out these inverses.
$foreignPivotKey = $foreignPivotKey ?: Str::plural($this->getForeignKey());
$relatedPivotKey = $relatedPivotKey ?: $name . '_id';
}
return $this->morphToMany(
$related,
$name,
$table,
$foreignPivotKey,
$relatedPivotKey,
$parentKey,
$relatedKey,
$relatedKey,
true,
);
}
/** @inheritdoc */
public function newEloquentBuilder($query)
{
if (Model::isDocumentModel($this)) {
return new Builder($query);
}
return new EloquentBuilder($query);
}
}
================================================
FILE: src/Eloquent/MassPrunable.php
================================================
prunable();
$total = in_array(SoftDeletes::class, class_uses_recursive(static::class))
? $query->forceDelete()
: $query->delete();
event(new ModelsPruned(static::class, $total));
return $total;
}
}
================================================
FILE: src/Eloquent/Model.php
================================================
true];
/**
* Indicates if the given model class is a MongoDB document model.
* It must be a subclass of {@see BaseModel} and use the
* {@see DocumentModel} trait.
*
* @param class-string|object $class
*/
final public static function isDocumentModel(string|object $class): bool
{
if (is_object($class)) {
$class = $class::class;
}
if (array_key_exists($class, self::$documentModelClasses)) {
return self::$documentModelClasses[$class];
}
// We know all child classes of this class are document models.
if (is_subclass_of($class, self::class)) {
return self::$documentModelClasses[$class] = true;
}
// Document models must be subclasses of Laravel's base model class.
if (! is_subclass_of($class, BaseModel::class)) {
return self::$documentModelClasses[$class] = false;
}
// Document models must use the DocumentModel trait.
return self::$documentModelClasses[$class] = array_key_exists(DocumentModel::class, class_uses_recursive($class));
}
}
================================================
FILE: src/Eloquent/SoftDeletes.php
================================================
getDeletedAtColumn();
}
}
================================================
FILE: src/Helpers/EloquentBuilder.php
================================================
=', $count = 1, $boolean = 'and', ?Closure $callback = null)
{
if (is_string($relation)) {
if (str_contains($relation, '.')) {
return $this->hasNested($relation, $operator, $count, $boolean, $callback);
}
$relation = $this->getRelationWithoutConstraints($relation);
}
// If this is a hybrid relation then we can not use a normal whereExists() query that relies on a subquery
// We need to use a `whereIn` query
if (Model::isDocumentModel($this->getModel()) || $this->isAcrossConnections($relation)) {
return $this->addHybridHas($relation, $operator, $count, $boolean, $callback);
}
// If we only need to check for the existence of the relation, then we can optimize
// the subquery to only run a "where exists" clause instead of this full "count"
// clause. This will make these queries run much faster compared with a count.
$method = $this->canUseExistsForExistenceCheck($operator, $count)
? 'getRelationExistenceQuery'
: 'getRelationExistenceCountQuery';
$hasQuery = $relation->{$method}(
$relation->getRelated()->newQuery(),
$this
);
// Next we will call any given callback as an "anonymous" scope so they can get the
// proper logical grouping of the where clauses if needed by this Eloquent query
// builder. Then, we will be ready to finalize and return this query instance.
if ($callback) {
$hasQuery->callScope($callback);
}
return $this->addHasWhere(
$hasQuery,
$relation,
$operator,
$count,
$boolean,
);
}
/** @return bool */
protected function isAcrossConnections(Relation $relation)
{
return $relation->getParent()->getConnectionName() !== $relation->getRelated()->getConnectionName();
}
/**
* Compare across databases.
*
* @param string $operator
* @param int $count
* @param string $boolean
*
* @return mixed
*
* @throws Exception
*/
public function addHybridHas(Relation $relation, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null)
{
$this->assertHybridRelationSupported($relation);
$hasQuery = $relation->getQuery();
if ($callback) {
$hasQuery->callScope($callback);
}
// If the operator is <, <= or !=, we will use whereNotIn.
$not = in_array($operator, ['<', '<=', '!=']);
// If we are comparing to 0, we need an additional $not flip.
if ($count === 0) {
$not = ! $not;
}
$relations = match (true) {
$relation instanceof MorphToMany => $relation->getInverse() ?
$this->handleMorphedByMany($hasQuery, $relation) :
$this->handleMorphToMany($hasQuery, $relation),
default => $hasQuery->pluck($this->getHasCompareKey($relation))
};
$relatedIds = $this->getConstrainedRelatedIds($relations, $operator, $count);
return $this->whereIn($this->getRelatedConstraintKey($relation), $relatedIds, $boolean, $not);
}
/**
* @param Relation $relation
*
* @return void
*
* @throws Exception
*/
private function assertHybridRelationSupported(Relation $relation): void
{
if (
$relation instanceof HasOneOrMany
|| $relation instanceof BelongsTo
|| ($relation instanceof BelongsToMany && ! $this->isAcrossConnections($relation))
) {
return;
}
throw new LogicException(class_basename($relation) . ' is not supported for hybrid query constraints.');
}
/**
* @param Builder $hasQuery
* @param Relation $relation
*
* @return Collection
*/
private function handleMorphToMany($hasQuery, $relation)
{
// First we select the parent models that have a relation to our related model,
// Then extracts related model's ids from the pivot column
$hasQuery->where($relation->getTable() . '.' . $relation->getMorphType(), $relation->getParent()::class);
$relations = $hasQuery->pluck($relation->getTable());
$relations = $relation->extractIds($relations->flatten(1)->toArray(), $relation->getForeignPivotKeyName());
return collect($relations);
}
/**
* @param Builder $hasQuery
* @param Relation $relation
*
* @return Collection
*/
private function handleMorphedByMany($hasQuery, $relation)
{
$hasQuery->whereNotNull($relation->getForeignPivotKeyName());
return $hasQuery->pluck($relation->getForeignPivotKeyName())->flatten(1);
}
/** @return string */
protected function getHasCompareKey(Relation $relation)
{
if (method_exists($relation, 'getHasCompareKey')) {
return $relation->getHasCompareKey();
}
return $relation instanceof HasOneOrMany ? $relation->getForeignKeyName() : $relation->getOwnerKeyName();
}
/**
* @param Collection $relations
* @param string $operator
* @param int $count
*
* @return array
*/
protected function getConstrainedRelatedIds($relations, $operator, $count)
{
$relationCount = array_count_values(array_map(function ($id) {
return (string) $id; // Convert Back ObjectIds to Strings
}, is_array($relations) ? $relations : $relations->flatten()->toArray()));
// Remove unwanted related objects based on the operator and count.
$relationCount = array_filter($relationCount, function ($counted) use ($count, $operator) {
// If we are comparing to 0, we always need all results.
if ($count === 0) {
return true;
}
switch ($operator) {
case '>=':
case '<':
return $counted >= $count;
case '>':
case '<=':
return $counted > $count;
case '=':
case '!=':
return $counted === $count;
}
});
// All related ids.
return array_keys($relationCount);
}
/**
* Returns key we are constraining this parent model's query with.
*
* @return string
*
* @throws Exception
*/
protected function getRelatedConstraintKey(Relation $relation)
{
$this->assertHybridRelationSupported($relation);
if ($relation instanceof HasOneOrMany) {
return $relation->getLocalKeyName();
}
if ($relation instanceof BelongsTo) {
return $relation->getForeignKeyName();
}
if ($relation instanceof BelongsToMany) {
return $this->model->getKeyName();
}
throw new Exception(class_basename($relation) . ' is not supported for hybrid query constraints.');
}
}
================================================
FILE: src/MongoDBBusServiceProvider.php
================================================
app->singleton(MongoBatchRepository::class, function (Container $app) {
$connection = $app->make('db')->connection($app->config->get('queue.batching.database'));
if (! $connection instanceof Connection) {
throw new InvalidArgumentException(sprintf('The "mongodb" batch driver requires a MongoDB connection. The "%s" connection uses the "%s" driver.', $connection->getName(), $connection->getDriverName()));
}
return new MongoBatchRepository(
$app->make(BatchFactory::class),
$connection,
$app->config->get('queue.batching.collection', 'job_batches'),
);
});
/** The {@see BatchRepository} service is registered in {@see BusServiceProvider} */
$this->app->register(BusServiceProvider::class);
$this->app->extend(BatchRepository::class, function (BatchRepository $repository, Container $app) {
$driver = $app->config->get('queue.batching.driver');
return match ($driver) {
'mongodb' => $app->make(MongoBatchRepository::class),
default => $repository,
};
});
}
/** @inheritdoc */
#[Override]
public function provides()
{
return [
BatchRepository::class,
MongoBatchRepository::class,
];
}
}
================================================
FILE: src/MongoDBServiceProvider.php
================================================
app['db']);
Model::setEventDispatcher($this->app['events']);
}
/**
* Register the service provider.
*/
#[Override]
public function register()
{
// Add database driver.
$this->app->resolving('db', function ($db) {
$db->extend('mongodb', function ($config, $name) {
$config['name'] = $name;
return new Connection($config);
});
});
// Session handler for MongoDB
$this->app->resolving(SessionManager::class, function (SessionManager $sessionManager) {
$sessionManager->extend('mongodb', function (Application $app) {
$connectionName = $app->config->get('session.connection') ?: 'mongodb';
$connection = $app->make('db')->connection($connectionName);
assert($connection instanceof Connection, new InvalidArgumentException(sprintf('The database connection "%s" used for the session does not use the "mongodb" driver.', $connectionName)));
return new MongoDbSessionHandler(
$connection,
$app->config->get('session.table', 'sessions'),
$app->config->get('session.lifetime'),
$app,
);
});
});
// Add cache and lock drivers.
$this->app->resolving('cache', function (CacheManager $cache) {
$cache->extend('mongodb', function (Application $app, array $config): Repository {
// The closure is bound to the CacheManager
assert($this instanceof CacheManager);
$store = new MongoStore(
$app['db']->connection($config['connection'] ?? null),
$config['collection'] ?? 'cache',
$this->getPrefix($config),
$app['db']->connection($config['lock_connection'] ?? $config['connection'] ?? null),
$config['lock_collection'] ?? ($config['collection'] ?? 'cache') . '_locks',
$config['lock_lottery'] ?? [2, 100],
$config['lock_timeout'] ?? 86400,
);
return $this->repository($store, $config);
});
});
// Add connector for queue support.
$this->app->resolving('queue', function ($queue) {
$queue->addConnector('mongodb', function () {
return new MongoConnector($this->app['db']);
});
});
$this->registerFlysystemAdapter();
$this->registerScoutEngine();
}
private function registerFlysystemAdapter(): void
{
// GridFS adapter for filesystem
$this->app->resolving('filesystem', static function (FilesystemManager $filesystemManager) {
$filesystemManager->extend('gridfs', function (Application $app, array $config) {
if (! class_exists(GridFSAdapter::class)) {
throw new RuntimeException('GridFS adapter for Flysystem is missing. Try running "composer require league/flysystem-gridfs"');
}
$bucket = $config['bucket'] ?? null;
if ($bucket instanceof Closure) {
// Get the bucket from a factory function
$bucket = $bucket($app, $config);
} elseif (is_string($bucket) && $app->has($bucket)) {
// Get the bucket from a service
$bucket = $app->get($bucket);
} elseif (is_string($bucket) || $bucket === null) {
// Get the bucket from the database connection
$connection = $app['db']->connection($config['connection']);
if (! $connection instanceof Connection) {
throw new InvalidArgumentException(sprintf('The database connection "%s" does not use the "mongodb" driver.', $config['connection'] ?? $app['config']['database.default']));
}
$bucket = $connection->getClient()
->getDatabase($config['database'] ?? $connection->getDatabaseName())
->selectGridFSBucket(['bucketName' => $config['bucket'] ?? 'fs', 'disableMD5' => true]);
}
if (! $bucket instanceof Bucket) {
throw new InvalidArgumentException(sprintf('Unexpected value for GridFS "bucket" configuration. Expecting "%s". Got "%s"', Bucket::class, get_debug_type($bucket)));
}
$adapter = new GridFSAdapter($bucket, $config['prefix'] ?? '');
/** @see FilesystemManager::createFlysystem() */
if ($config['read-only'] ?? false) {
if (! class_exists(ReadOnlyFilesystemAdapter::class)) {
throw new RuntimeException('Read-only Adapter for Flysystem is missing. Try running "composer require league/flysystem-read-only"');
}
$adapter = new ReadOnlyFilesystemAdapter($adapter);
}
/** Prevent using backslash on Windows in {@see FilesystemAdapter::__construct()} */
$config['directory_separator'] = '/';
return new FilesystemAdapter(new Filesystem($adapter, $config), $adapter, $config);
});
});
}
private function registerScoutEngine(): void
{
$this->app->resolving(EngineManager::class, function (EngineManager $engineManager) {
$engineManager->extend('mongodb', function (Container $app) {
$connectionName = $app->get('config')->get('scout.mongodb.connection', 'mongodb');
$connection = $app->get('db')->connection($connectionName);
$softDelete = (bool) $app->get('config')->get('scout.soft_delete', false);
$indexDefinitions = $app->get('config')->get('scout.mongodb.index-definitions', []);
assert($connection instanceof Connection, new InvalidArgumentException(sprintf('The connection "%s" is not a MongoDB connection.', $connectionName)));
return new ScoutEngine($connection->getDatabase(), $softDelete, $indexDefinitions);
});
return $engineManager;
});
}
}
================================================
FILE: src/Query/AggregationBuilder.php
================================================
pipeline[] = [$operator => $value];
return $this;
}
/**
* Execute the aggregation pipeline and return the results.
*/
public function get(array $options = []): LaravelCollection|LazyCollection
{
$cursor = $this->execute($options);
return collect($cursor->toArray());
}
/**
* Execute the aggregation pipeline and return the results in a lazy collection.
*/
public function cursor($options = []): LazyCollection
{
$cursor = $this->execute($options);
return LazyCollection::make(function () use ($cursor) {
foreach ($cursor as $item) {
yield $item;
}
});
}
/**
* Execute the aggregation pipeline and return the first result.
*/
public function first(array $options = []): mixed
{
return (clone $this)
->limit(1)
->get($options)
->first();
}
/**
* Execute the aggregation pipeline and return MongoDB cursor.
*/
private function execute(array $options): CursorInterface&Iterator
{
$encoder = new BuilderEncoder();
$pipeline = $encoder->encode($this->getPipeline());
$options = array_replace(
['typeMap' => ['root' => 'array', 'document' => 'array']],
$this->options,
$options,
);
return $this->collection->aggregate($pipeline, $options);
}
}
================================================
FILE: src/Query/Builder.php
================================================
',
'<=',
'>=',
'<>',
'!=',
'like',
'not like',
'between',
'ilike',
'&',
'|',
'^',
'<<',
'>>',
'rlike',
'regexp',
'not regexp',
'exists',
'type',
'mod',
'where',
'all',
'size',
'regex',
'not regex',
'text',
'slice',
'elemmatch',
'geowithin',
'geointersects',
'near',
'nearsphere',
'geometry',
'maxdistance',
'center',
'centersphere',
'box',
'polygon',
'uniquedocs',
];
/**
* Operator conversion.
*
* @var array
*/
protected $conversion = [
'=' => 'eq',
'!=' => 'ne',
'<>' => 'ne',
'<' => 'lt',
'<=' => 'lte',
'>' => 'gt',
'>=' => 'gte',
'regexp' => 'regex',
'not regexp' => 'not regex',
'ilike' => 'like',
'elemmatch' => 'elemMatch',
'geointersects' => 'geoIntersects',
'geowithin' => 'geoWithin',
'nearsphere' => 'nearSphere',
'maxdistance' => 'maxDistance',
'centersphere' => 'centerSphere',
'uniquedocs' => 'uniqueDocs',
];
/**
* Set the projections.
*
* @param array $columns
*
* @return $this
*/
public function project($columns)
{
$this->projections = is_array($columns) ? $columns : func_get_args();
return $this;
}
/**
* Set the cursor hint.
*
* @param mixed $index
*
* @return $this
*/
public function hint($index)
{
$this->hint = $index;
return $this;
}
/** @inheritdoc */
#[Override]
public function find($id, $columns = [])
{
return $this->where('_id', '=', $this->convertKey($id))->first($columns);
}
/** @inheritdoc */
#[Override]
public function value($column)
{
$result = (array) $this->first([$column]);
return Arr::get($result, $column);
}
/** @inheritdoc */
#[Override]
public function get($columns = [])
{
return $this->getFresh($columns);
}
/** @inheritdoc */
#[Override]
public function cursor($columns = [])
{
$result = $this->getFresh($columns, true);
if ($result instanceof LazyCollection) {
return $result;
}
throw new RuntimeException('Query not compatible with cursor');
}
/**
* Die and dump the current MongoDB query
*
* @return never-return
*/
#[Override]
public function dd()
{
dd($this->toMql());
}
/**
* Dump the current MongoDB query
*
* @param mixed ...$args
*
* @return $this
*/
#[Override]
public function dump(mixed ...$args)
{
dump($this->toMql(), ...$args);
return $this;
}
/**
* Return the MongoDB query to be run in the form of an element array like ['method' => [arguments]].
*
* Example: ['find' => [['name' => 'John Doe'], ['projection' => ['birthday' => 1]]]]
*
* @return array
*/
public function toMql(): array
{
$columns = $this->columns ?? [];
// Drop all columns if * is present, MongoDB does not work this way.
if (in_array('*', $columns)) {
$columns = [];
}
$wheres = $this->compileWheres();
$wheres = $this->aliasIdForQuery($wheres);
// Use MongoDB's aggregation framework when using grouping or aggregation functions.
if ($this->groups || $this->aggregate) {
$group = [];
$unwinds = [];
$set = [];
// Add grouping columns to the $group part of the aggregation pipeline.
if ($this->groups) {
foreach ($this->groups as $column) {
$group['_id'][$column] = '$' . $column;
// When grouping, also add the $last operator to each grouped field,
// this mimics SQL's behaviour a bit.
$group[$column] = ['$last' => '$' . $column];
}
}
// Add the last value of each column when there is no aggregate function.
if ($this->groups && ! $this->aggregate) {
foreach ($columns as $column) {
$key = str_replace('.', '_', $column);
$group[$key] = ['$last' => '$' . $column];
}
}
// Add aggregation functions to the $group part of the aggregation pipeline,
// these may override previous aggregations.
if ($this->aggregate) {
$function = $this->aggregate['function'];
foreach ($this->aggregate['columns'] as $column) {
// Add unwind if a subdocument array should be aggregated
// column: subarray.price => {$unwind: '$subarray'}
$splitColumns = explode('.*.', $column);
if (count($splitColumns) === 2) {
$unwinds[] = $splitColumns[0];
$column = implode('.', $splitColumns);
}
$aggregations = blank($this->aggregate['columns']) ? [] : $this->aggregate['columns'];
if ($column === '*' && $function === 'count' && ! $this->groups) {
$options = $this->inheritConnectionOptions($this->options);
return ['countDocuments' => [$wheres, $options]];
}
// "aggregate" is the name of the field that will hold the aggregated value.
if ($function === 'count') {
if ($column === '*' || $aggregations === []) {
// Translate count into sum.
$group['aggregate'] = ['$sum' => 1];
} else {
// Count the number of distinct values.
$group['aggregate'] = ['$addToSet' => '$' . $column];
$set['aggregate'] = ['$size' => '$aggregate'];
}
} else {
$group['aggregate'] = ['$' . $function => '$' . $column];
}
}
}
// The _id field is mandatory when using grouping.
if ($group && empty($group['_id'])) {
$group['_id'] = null;
}
// Build the aggregation pipeline.
$pipeline = [];
if ($wheres) {
$pipeline[] = ['$match' => $wheres];
}
// apply unwinds for subdocument array aggregation
foreach ($unwinds as $unwind) {
$pipeline[] = ['$unwind' => '$' . $unwind];
}
if ($group) {
$pipeline[] = ['$group' => $group];
}
if ($set) {
$pipeline[] = ['$set' => $set];
}
// Apply order and limit
if ($this->orders) {
$pipeline[] = ['$sort' => $this->aliasIdForQuery($this->orders)];
}
if ($this->offset) {
$pipeline[] = ['$skip' => $this->offset];
}
if ($this->limit) {
$pipeline[] = ['$limit' => $this->limit];
}
if ($this->projections) {
$pipeline[] = ['$project' => $this->projections];
}
$options = [
'typeMap' => ['root' => 'object', 'document' => 'array'],
];
// Add custom query options
if (count($this->options)) {
$options = array_replace($options, $this->options);
}
$options = $this->inheritConnectionOptions($options);
return ['aggregate' => [$pipeline, $options]];
}
// Distinct query
if ($this->distinct) {
// Return distinct results directly
$column = $columns[0] ?? '_id';
$options = $this->inheritConnectionOptions();
return ['distinct' => [$column, $wheres, $options]];
}
// Normal query
// Convert select columns to simple projections.
$projection = $this->aliasIdForQuery(array_fill_keys($columns, true));
// Add custom projections.
if ($this->projections) {
$projection = array_replace($projection, $this->projections);
}
$options = [];
// Apply order, offset, limit and projection
if ($this->timeout) {
$options['maxTimeMS'] = (int) ($this->timeout * 1000);
}
if ($this->orders) {
$options['sort'] = $this->aliasIdForQuery($this->orders);
}
if ($this->offset) {
$options['skip'] = $this->offset;
}
if ($this->limit) {
$options['limit'] = $this->limit;
}
if ($this->hint) {
$options['hint'] = $this->hint;
}
if ($projection) {
$options['projection'] = $projection;
}
$options['typeMap'] = ['root' => 'object', 'document' => 'array'];
// Add custom query options
if (count($this->options)) {
$options = array_replace($options, $this->options);
}
$options = $this->inheritConnectionOptions($options);
return ['find' => [$wheres, $options]];
}
/**
* Execute the query as a fresh "select" statement.
*
* @param array $columns
* @param bool $returnLazy
*
* @return array|static[]|Collection|LazyCollection
*/
public function getFresh($columns = [], $returnLazy = false)
{
// If no columns have been specified for the select statement, we will set them
// here to either the passed columns, or the standard default of retrieving
// all of the columns on the table using the "wildcard" column character.
if ($this->columns === null) {
$this->columns = $columns;
}
// Drop all columns if * is present, MongoDB does not work this way.
if (in_array('*', $this->columns)) {
$this->columns = [];
}
$command = $this->toMql();
assert(count($command) >= 1, 'At least one method call is required to execute a query');
$result = $this->collection;
foreach ($command as $method => $arguments) {
$result = call_user_func_array([$result, $method], $arguments);
}
// countDocuments method returns int, wrap it to the format expected by the framework
if (is_int($result)) {
$result = [
[
'_id' => null,
'aggregate' => $result,
],
];
}
if ($returnLazy) {
return LazyCollection::make(function () use ($result) {
foreach ($result as $item) {
yield $this->aliasIdForResult($item);
}
});
}
if ($result instanceof Cursor) {
$result = $result->toArray();
}
foreach ($result as &$document) {
if (is_array($document) || is_object($document)) {
$document = $this->aliasIdForResult($document);
}
}
return new Collection($result);
}
/**
* Generate the unique cache key for the current query.
*
* @return string
*/
public function generateCacheKey()
{
$key = [
'connection' => $this->collection->getDatabaseName(),
'collection' => $this->collection->getCollectionName(),
'wheres' => $this->wheres,
'columns' => $this->columns,
'groups' => $this->groups,
'orders' => $this->orders,
'offset' => $this->offset,
'limit' => $this->limit,
'aggregate' => $this->aggregate,
];
return md5(serialize(array_values($key)));
}
/** @return ($function is null ? AggregationBuilder : mixed) */
#[Override]
public function aggregate($function = null, $columns = ['*'])
{
assert(is_array($columns), new TypeError(sprintf('Argument #2 ($columns) must be of type array, %s given', get_debug_type($columns))));
if ($function === null) {
if (! trait_exists(FluentFactoryTrait::class)) {
// This error will be unreachable when the mongodb/builder package will be merged into mongodb/mongodb
throw new BadMethodCallException('Aggregation builder requires package mongodb/builder 0.2+');
}
if ($columns !== ['*']) {
throw new InvalidArgumentException('Columns cannot be specified to create an aggregation builder. Add a $project stage instead.');
}
if ($this->wheres) {
throw new BadMethodCallException('Aggregation builder does not support previous query-builder instructions. Use a $match stage instead.');
}
return new AggregationBuilder($this->collection, $this->options);
}
$this->aggregate = [
'function' => $function,
'columns' => $columns,
];
$previousColumns = $this->columns;
// We will also back up the select bindings since the select clause will be
// removed when performing the aggregate function. Once the query is run
// we will add the bindings back onto this query so they can get used.
$previousSelectBindings = $this->bindings['select'];
$this->bindings['select'] = [];
$results = $this->get($columns);
// Once we have executed the query, we will reset the aggregate property so
// that more select queries can be executed against the database without
// the aggregate value getting in the way when the grammar builds it.
$this->aggregate = null;
$this->columns = $previousColumns;
$this->bindings['select'] = $previousSelectBindings;
// When the aggregation is per group, we return the results as is.
if ($this->groups) {
return $results->map(function (object $result) {
unset($result->id);
return $result;
});
}
if (isset($results[0])) {
$result = (array) $results[0];
return $result['aggregate'];
}
}
/**
* @param string $function
* @param array $columns
*
* @return mixed
*/
public function aggregateByGroup(string $function, array $columns = ['*'])
{
if (count($columns) > 1) {
throw new InvalidArgumentException('Aggregating by group requires zero or one columns.');
}
return $this->aggregate($function, $columns);
}
/** @inheritdoc */
#[Override]
public function exists()
{
return $this->first(['id']) !== null;
}
/** @inheritdoc */
public function distinct($column = false)
{
$this->distinct = true;
if ($column) {
$this->columns = [$column];
}
return $this;
}
/**
* @param int|string|array $direction
*
* @inheritdoc
*/
#[Override]
public function orderBy($column, $direction = 'asc')
{
if (is_string($direction)) {
$direction = match ($direction) {
'asc', 'ASC' => 1,
'desc', 'DESC' => -1,
default => throw new InvalidArgumentException('Order direction must be "asc" or "desc".'),
};
}
$column = (string) $column;
if ($column === 'natural') {
$this->orders['$natural'] = $direction;
} else {
$this->orders[$column] = $direction;
}
return $this;
}
/**
* Override Illuminate's removeExistingOrdersFor to support associative order storage used by MongoDB.
*
* @inheritdoc
*/
#[Override]
protected function removeExistingOrdersFor($column): array
{
$orders = $this->orders ?? [];
$toUnset = array_filter(
array_keys($orders),
function ($orderColumn) use ($column) {
return $orderColumn === $column
|| ($orderColumn === 'id' && $column === '_id')
|| ($orderColumn === '_id' && $column === 'id'
);
},
);
foreach ($toUnset as $column) {
unset($orders[$column]);
}
return $orders;
}
/** @inheritdoc */
#[Override]
public function whereBetween($column, iterable $values, $boolean = 'and', $not = false)
{
$type = 'between';
if ($values instanceof Collection) {
$values = $values->all();
}
if (is_array($values) && (! array_is_list($values) || count($values) !== 2)) {
throw new InvalidArgumentException('Between $values must be a list with exactly two elements: [min, max]');
}
$this->wheres[] = [
'column' => $column,
'type' => $type,
'boolean' => $boolean,
'values' => $values,
'not' => $not,
];
return $this;
}
/** @inheritdoc */
#[Override]
public function insert(array $values)
{
// Allow empty insert batch for consistency with Eloquent SQL
if ($values === []) {
return true;
}
// Since every insert gets treated like a batch insert, we will have to detect
// if the user is inserting a single document or an array of documents.
$batch = true;
foreach ($values as $value) {
// As soon as we find a value that is not an array we assume the user is
// inserting a single document.
if (! is_array($value)) {
$batch = false;
break;
}
}
if (! $batch) {
$values = [$values];
}
$values = array_map(
$this->aliasIdForQuery(...),
$values,
);
$options = $this->inheritConnectionOptions();
$result = $this->collection->insertMany($values, $options);
return $result->isAcknowledged();
}
/** @inheritdoc */
#[Override]
public function insertGetId(array $values, $sequence = null)
{
$options = $this->inheritConnectionOptions();
$values = $this->aliasIdForQuery($values);
$result = $this->collection->insertOne($values, $options);
if (! $result->isAcknowledged()) {
return null;
}
return match ($sequence) {
'_id', 'id', null => $result->getInsertedId(),
default => $values[$sequence],
};
}
/** @inheritdoc */
#[Override]
public function update(array $values, array $options = [])
{
// Use $set as default operator for field names that are not in an operator
foreach ($values as $key => $value) {
if (is_string($key) && str_starts_with($key, '$')) {
continue;
}
$values['$set'][$key] = $value;
unset($values[$key]);
}
return $this->performUpdate($values, $options);
}
/** @inheritdoc */
#[Override]
public function upsert(array $values, $uniqueBy, $update = null): int
{
if ($values === []) {
return 0;
}
// Single document provided
if (! array_is_list($values)) {
$values = [$values];
}
$this->applyBeforeQueryCallbacks();
$options = $this->inheritConnectionOptions();
$uniqueBy = array_fill_keys((array) $uniqueBy, 1);
// If no update fields are specified, all fields are updated
if ($update !== null) {
$update = array_fill_keys((array) $update, 1);
}
$bulk = [];
foreach ($values as $value) {
$filter = $operation = [];
foreach ($value as $key => $val) {
if (isset($uniqueBy[$key])) {
$filter[$key] = $val;
}
if ($update === null || array_key_exists($key, $update)) {
$operation['$set'][$key] = $val;
} else {
$operation['$setOnInsert'][$key] = $val;
}
}
$bulk[] = ['updateOne' => [$filter, $operation, ['upsert' => true]]];
}
$result = $this->collection->bulkWrite($bulk, $options);
return $result->getInsertedCount() + $result->getUpsertedCount() + $result->getModifiedCount();
}
/** @inheritdoc */
#[Override]
public function increment($column, $amount = 1, array $extra = [], array $options = [])
{
$query = ['$inc' => [(string) $column => $amount]];
if (! empty($extra)) {
$query['$set'] = $extra;
}
// Protect
$this->where(function ($query) use ($column) {
$query->where($column, 'exists', false);
$query->orWhereNotNull($column);
});
$options = $this->inheritConnectionOptions($options);
return $this->performUpdate($query, $options);
}
/**
* @param array $options
*
* @inheritdoc
*/
#[Override]
public function incrementEach(array $columns, array $extra = [], array $options = [])
{
$stage['$addFields'] = $extra;
// Not using $inc for each column, because it would fail if one column is null.
foreach ($columns as $column => $amount) {
$stage['$addFields'][$column] = [
'$add' => [$amount, ['$ifNull' => ['$' . $column, 0]]],
];
}
$options = $this->inheritConnectionOptions($options);
return $this->performUpdate([$stage], $options);
}
/** @inheritdoc */
#[Override]
public function decrement($column, $amount = 1, array $extra = [], array $options = [])
{
return $this->increment($column, -1 * $amount, $extra, $options);
}
/** @inheritdoc */
#[Override]
public function decrementEach(array $columns, array $extra = [], array $options = [])
{
$decrement = [];
foreach ($columns as $column => $amount) {
$decrement[$column] = -1 * $amount;
}
return $this->incrementEach($decrement, $extra, $options);
}
/**
* Multiply a column's value by a given amount.
*
* @param string $column
* @param float|int $amount
*
* @return int
*/
public function multiply($column, $amount, array $extra = [], array $options = [])
{
$query = ['$mul' => [(string) $column => $amount]];
if (! empty($extra)) {
$query['$set'] = $extra;
}
// Protect
$this->where(function ($query) use ($column) {
$query->where($column, 'exists', true);
$query->whereNotNull($column);
});
$options = $this->inheritConnectionOptions($options);
return $this->performUpdate($query, $options);
}
/**
* Divide a column's value by a given amount.
*
* @param string $column
* @param float|int $amount
*
* @return int
*/
public function divide($column, $amount, array $extra = [], array $options = [])
{
return $this->multiply($column, 1 / $amount, $extra, $options);
}
/** @inheritdoc */
#[Override]
public function pluck($column, $key = null)
{
$results = $this->get($key === null ? [$column] : [$column, $key]);
$p = Arr::pluck($results, $column, $key);
return new Collection($p);
}
/** @inheritdoc */
#[Override]
public function delete($id = null)
{
// If an ID is passed to the method, we will set the where clause to check
// the ID to allow developers to simply and quickly remove a single row
// from their database without manually specifying the where clauses.
if ($id !== null) {
$this->where('_id', '=', $id);
}
$wheres = $this->compileWheres();
$wheres = $this->aliasIdForQuery($wheres);
$options = $this->inheritConnectionOptions();
/**
* Ignore the limit if it is set to more than 1, as it is not supported by the deleteMany method.
* Required for {@see DatabaseFailedJobProvider::prune()}
*/
if ($this->limit === 1) {
$result = $this->collection->deleteOne($wheres, $options);
} else {
$result = $this->collection->deleteMany($wheres, $options);
}
if ($result->isAcknowledged()) {
return $result->getDeletedCount();
}
return 0;
}
/** @inheritdoc */
#[Override]
public function from($collection, $as = null)
{
if ($collection) {
$this->collection = $this->connection->getCollection($collection);
}
return parent::from($collection);
}
public function truncate(): bool
{
$options = $this->inheritConnectionOptions();
$result = $this->collection->deleteMany([], $options);
return $result->isAcknowledged();
}
/**
* Get an array with the values of a given column.
*
* @deprecated Use pluck instead.
*
* @param string $column
* @param string $key
*
* @return Collection
*/
public function lists($column, $key = null)
{
return $this->pluck($column, $key);
}
/**
* @param (Closure():T)|Expression|null $value
*
* @return ($value is Closure ? T : ($value is null ? Collection : Expression))
*
* @template T
*/
#[Override]
public function raw($value = null)
{
// Execute the closure on the mongodb collection
if ($value instanceof Closure) {
return call_user_func($value, $this->collection);
}
// Create an expression for the given value
if ($value !== null) {
return new Expression($value);
}
// Quick access to the mongodb collection
return $this->collection;
}
/**
* Append one or more values to an array.
*
* @param string|array $column
* @param mixed $value
* @param bool $unique
*
* @return int
*/
public function push($column, $value = null, $unique = false)
{
// Use the addToSet operator in case we only want unique items.
$operator = $unique ? '$addToSet' : '$push';
// Check if we are pushing multiple values.
$batch = is_array($value) && array_is_list($value);
if (is_array($column)) {
if ($value !== null) {
throw new InvalidArgumentException(sprintf('2nd argument of %s() must be "null" when 1st argument is an array. Got "%s" instead.', __METHOD__, get_debug_type($value)));
}
$query = [$operator => $column];
} elseif ($batch) {
$query = [$operator => [(string) $column => ['$each' => $value]]];
} else {
$query = [$operator => [(string) $column => $value]];
}
return $this->performUpdate($query);
}
/**
* Remove one or more values from an array.
*
* @param string|array $column
* @param mixed $value
*
* @return int
*/
public function pull($column, $value = null)
{
// Check if we passed an associative array.
$batch = is_array($value) && array_is_list($value);
// If we are pulling multiple values, we need to use $pullAll.
$operator = $batch ? '$pullAll' : '$pull';
if (is_array($column)) {
$query = [$operator => $column];
} else {
$query = [$operator => [$column => $value]];
}
return $this->performUpdate($query);
}
/**
* Remove one or more fields.
*
* @param string|string[] $columns
*
* @return int
*/
public function drop($columns)
{
if (! is_array($columns)) {
$columns = [$columns];
}
$fields = [];
foreach ($columns as $column) {
$fields[$column] = 1;
}
$query = ['$unset' => $fields];
return $this->performUpdate($query);
}
/**
* @return static
*
* @inheritdoc
*/
#[Override]
public function newQuery()
{
return new static($this->connection, $this->grammar, $this->processor);
}
#[Override]
public function runPaginationCountQuery($columns = ['*'])
{
if ($this->distinct) {
throw new BadMethodCallException('Distinct queries cannot be used for pagination. Use GroupBy instead');
}
if ($this->groups || $this->havings) {
$without = $this->unions ? ['orders', 'limit', 'offset'] : ['columns', 'orders', 'limit', 'offset'];
$mql = $this->cloneWithout($without)
->cloneWithoutBindings($this->unions ? ['order'] : ['select', 'order'])
->toMql();
// Adds the $count stage to the pipeline
$mql['aggregate'][0][] = ['$count' => 'aggregate'];
return $this->collection->aggregate($mql['aggregate'][0], $mql['aggregate'][1])->toArray();
}
return parent::runPaginationCountQuery($columns);
}
/**
* Perform an update query.
*
* @return int
*/
protected function performUpdate(array $update, array $options = [])
{
// Update multiple items by default.
if (! array_key_exists('multiple', $options)) {
$options['multiple'] = true;
}
$update = $this->aliasIdForQuery($update);
$options = $this->inheritConnectionOptions($options);
$wheres = $this->compileWheres();
$wheres = $this->aliasIdForQuery($wheres);
$result = $this->collection->updateMany($wheres, $update, $options);
if ($result->isAcknowledged()) {
return $result->getModifiedCount() ?: $result->getUpsertedCount();
}
return 0;
}
/**
* Convert a key to ObjectID if needed.
*
* @param mixed $id
*
* @return mixed
*/
public function convertKey($id)
{
if (is_string($id) && strlen($id) === 24 && ctype_xdigit($id)) {
return new ObjectID($id);
}
if (is_string($id) && strlen($id) === 16 && preg_match('~[^\x20-\x7E\t\r\n]~', $id) > 0) {
return new Binary($id, Binary::TYPE_UUID);
}
return $id;
}
/**
* Add a basic where clause to the query.
*
* If 1 argument, the signature is: where(array|Closure $where)
* If 2 arguments, the signature is: where(string $column, mixed $value)
* If 3 arguments, the signature is: where(string $colum, string $operator, mixed $value)
*
* @param Closure|string|array $column
* @param mixed $operator
* @param mixed $value
* @param string $boolean
*
* @return $this
*/
#[Override]
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
$params = func_get_args();
// Remove the leading $ from operators.
if (func_num_args() >= 3) {
$operator = &$params[1];
if (is_string($operator) && str_starts_with($operator, '$')) {
$operator = substr($operator, 1);
}
}
if (func_num_args() === 1 && ! is_array($column) && ! is_callable($column)) {
throw new ArgumentCountError(sprintf('Too few arguments to function %s(%s), 1 passed and at least 2 expected when the 1st is not an array or a callable', __METHOD__, var_export($column, true)));
}
if (is_float($column) || is_bool($column) || $column === null) {
throw new InvalidArgumentException(sprintf('First argument of %s must be a field path as "string". Got "%s"', __METHOD__, get_debug_type($column)));
}
return parent::where(...$params);
}
/**
* Compile the where array.
*
* @return array
*/
protected function compileWheres(): array
{
// The wheres to compile.
$wheres = $this->wheres ?: [];
// We will add all compiled wheres to this array.
$compiled = [];
foreach ($wheres as $i => &$where) {
// Make sure the operator is in lowercase.
if (isset($where['operator'])) {
$where['operator'] = strtolower($where['operator']);
// Convert aliased operators
if (isset($this->conversion[$where['operator']])) {
$where['operator'] = $this->conversion[$where['operator']];
}
}
// Convert column name to string to use as array key
if (isset($where['column'])) {
$where['column'] = (string) $where['column'];
// Compatibility with Eloquent queries that uses "id" instead of MongoDB's _id
if ($where['column'] === 'id') {
$where['column'] = '_id';
}
// Convert id's.
if ($where['column'] === '_id' || str_ends_with($where['column'], '._id')) {
if (isset($where['values'])) {
// Multiple values.
$where['values'] = array_map($this->convertKey(...), $where['values']);
} elseif (isset($where['value'])) {
// Single value.
$where['value'] = $this->convertKey($where['value']);
}
}
}
// Convert CarbonPeriod to DateTime interval.
if (isset($where['values']) && $where['values'] instanceof CarbonPeriod) {
$where['values'] = [
$where['values']->getStartDate(),
$where['values']->getEndDate(),
];
}
// In a sequence of "where" clauses, the logical operator of the
// first "where" is determined by the 2nd "where".
// $where['boolean'] = "and", "or", "and not" or "or not"
if (
$i === 0 && count($wheres) > 1
&& str_starts_with($where['boolean'], 'and')
&& str_starts_with($wheres[$i + 1]['boolean'], 'or')
) {
$where['boolean'] = 'or' . (str_ends_with($where['boolean'], 'not') ? ' not' : '');
}
// We use different methods to compile different wheres.
$method = 'compileWhere' . $where['type'];
$result = $this->{$method}($where);
// Negate the expression
if (str_ends_with($where['boolean'], 'not')) {
$result = ['$nor' => [$result]];
}
// Wrap the where with an $or operator.
if (str_starts_with($where['boolean'], 'or')) {
$result = ['$or' => [$result]];
// phpcs:ignore Squiz.ControlStructures.ControlSignature.SpaceAfterCloseBrace
}
// If there are multiple wheres, we will wrap it with $and. This is needed
// to make nested wheres work.
elseif (count($wheres) > 1) {
$result = ['$and' => [$result]];
}
// Merge the compiled where with the others.
// array_merge_recursive can't be used here because it converts int keys to sequential int.
foreach ($result as $key => $value) {
if (in_array($key, ['$and', '$or', '$nor'])) {
$compiled[$key] = array_merge($compiled[$key] ?? [], $value);
} else {
$compiled[$key] = $value;
}
}
}
return $compiled;
}
protected function compileWhereBasic(array $where): array
{
$column = $where['column'];
$operator = $where['operator'];
$value = $where['value'];
// Replace like or not like with a Regex instance.
if (in_array($operator, ['like', 'not like'])) {
$regex = preg_replace(
[
// Unescaped % are converted to .*
// Group consecutive %
'#(^|[^\\\])%+#',
// Unescaped _ are converted to .
// Use positive lookahead to replace consecutive _
'#(?<=^|[^\\\\])_#',
// Escaped \% or \_ are unescaped
'#\\\\\\\(%|_)#',
],
['$1.*', '$1.', '$1'],
// Escape any regex reserved characters, so they are matched
// All backslashes are converted to \\, which are needed in matching regexes.
preg_quote($value),
);
$flags = $where['caseSensitive'] ?? false ? '' : 'i';
$value = new Regex('^' . $regex . '$', $flags);
// For inverse like operations, we can just use the $not operator with the Regex
$operator = $operator === 'like' ? '=' : 'not';
// phpcs:ignore Squiz.ControlStructures.ControlSignature.SpaceAfterCloseBrace
}
// Manipulate regex operations.
elseif (in_array($operator, ['regex', 'not regex'])) {
// Automatically convert regular expression strings to Regex objects.
if (is_string($value)) {
// Detect the delimiter and validate the preg pattern
$delimiter = substr($value, 0, 1);
if (! in_array($delimiter, self::REGEX_DELIMITERS)) {
throw new LogicException(sprintf('Missing expected starting delimiter in regular expression "%s", supported delimiters are: %s', $value, implode(' ', self::REGEX_DELIMITERS)));
}
$e = explode($delimiter, $value);
// We don't try to detect if the last delimiter is escaped. This would be an invalid regex.
if (count($e) < 3) {
throw new LogicException(sprintf('Missing expected ending delimiter "%s" in regular expression "%s"', $delimiter, $value));
}
// Flags are after the last delimiter
$flags = end($e);
// Extract the regex string between the delimiters
$regstr = substr($value, 1, -1 - strlen($flags));
$value = new Regex($regstr, $flags);
}
// For inverse regex operations, we can just use the $not operator with the Regex
$operator = $operator === 'regex' ? '=' : 'not';
}
if (! isset($operator) || $operator === '=' || $operator === 'eq') {
$query = [$column => $value];
} else {
$query = [$column => ['$' . $operator => $value]];
}
return $query;
}
protected function compileWhereNested(array $where): mixed
{
return $where['query']->compileWheres();
}
protected function compileWhereIn(array $where): array
{
return [$where['column'] => ['$in' => array_values($where['values'])]];
}
protected function compileWhereNotIn(array $where): array
{
return [$where['column'] => ['$nin' => array_values($where['values'])]];
}
protected function compileWhereLike(array $where): array
{
$where['operator'] = $where['not'] ? 'not like' : 'like';
return $this->compileWhereBasic($where);
}
protected function compileWhereNull(array $where): array
{
$where['operator'] = '=';
$where['value'] = null;
return $this->compileWhereBasic($where);
}
protected function compileWhereNotNull(array $where): array
{
$where['operator'] = 'ne';
$where['value'] = null;
return $this->compileWhereBasic($where);
}
protected function compileWhereBetween(array $where): array
{
$column = $where['column'];
$not = $where['not'];
$values = $where['values'];
if ($not) {
return [
'$or' => [
[
$column => [
'$lte' => $values[0],
],
],
[
$column => [
'$gte' => $values[1],
],
],
],
];
}
return [
$column => [
'$gte' => $values[0],
'$lte' => $values[1],
],
];
}
protected function compileWhereDate(array $where): array
{
$startOfDay = new UTCDateTime(Carbon::parse($where['value'])->startOfDay());
$endOfDay = new UTCDateTime(Carbon::parse($where['value'])->endOfDay());
return match ($where['operator']) {
'eq', '=' => [
$where['column'] => [
'$gte' => $startOfDay,
'$lte' => $endOfDay,
],
],
'ne' => [
$where['column'] => [
'$not' => [
'$gte' => $startOfDay,
'$lte' => $endOfDay,
],
],
],
'lt', 'gte' => [
$where['column'] => ['$' . $where['operator'] => $startOfDay],
],
'gt', 'lte' => [
$where['column'] => ['$' . $where['operator'] => $endOfDay],
],
};
}
protected function compileWhereMonth(array $where): array
{
return [
'$expr' => [
'$' . $where['operator'] => [
[
'$month' => '$' . $where['column'],
],
(int) $where['value'],
],
],
];
}
protected function compileWhereDay(array $where): array
{
return [
'$expr' => [
'$' . $where['operator'] => [
[
'$dayOfMonth' => '$' . $where['column'],
],
(int) $where['value'],
],
],
];
}
protected function compileWhereYear(array $where): array
{
return [
'$expr' => [
'$' . $where['operator'] => [
[
'$year' => '$' . $where['column'],
],
(int) $where['value'],
],
],
];
}
protected function compileWhereTime(array $where): array
{
if (! is_string($where['value']) || ! preg_match('/^[0-2][0-9](:[0-6][0-9](:[0-6][0-9])?)?$/', $where['value'], $matches)) {
throw new InvalidArgumentException(sprintf('Invalid time format, expected HH:MM:SS, HH:MM or HH, got "%s"', is_string($where['value']) ? $where['value'] : get_debug_type($where['value'])));
}
$format = match (count($matches)) {
1 => '%H',
2 => '%H:%M',
3 => '%H:%M:%S',
};
return [
'$expr' => [
'$' . $where['operator'] => [
[
'$dateToString' => ['date' => '$' . $where['column'], 'format' => $format],
],
$where['value'],
],
],
];
}
protected function compileWhereRaw(array $where): mixed
{
return $where['sql'];
}
protected function compileWhereSub(array $where): mixed
{
$where['value'] = $where['query']->compileWheres();
return $this->compileWhereBasic($where);
}
/**
* Set custom options for the query.
*
* @return $this
*/
public function options(array $options)
{
$this->options = $options;
return $this;
}
/**
* Set the read preference for the query
*
* @see https://www.php.net/manual/en/class.mongodb-driver-readpreference.php
*
* @param string $mode
* @param array $tagSets
* @param array $options
*
* @return $this
*/
public function readPreference(string $mode, ?array $tagSets = null, ?array $options = null): static
{
$this->readPreference = new ReadPreference($mode, $tagSets, $options);
return $this;
}
/**
* Performs a full-text search of the field or fields in an Atlas collection.
* NOTE: $search is only available for MongoDB Atlas clusters, and is not available for self-managed deployments.
*
* @see https://www.mongodb.com/docs/atlas/atlas-search/aggregation-stages/search/
*
* @return Collection
*/
public function search(
SearchOperatorInterface|array $operator,
?string $index = null,
?array $highlight = null,
?bool $concurrent = null,
?string $count = null,
?string $searchAfter = null,
?string $searchBefore = null,
?bool $scoreDetails = null,
?array $sort = null,
?bool $returnStoredSource = null,
?array $tracking = null,
): Collection {
// Forward named arguments to the search stage, skip null values
$args = array_filter([
'operator' => $operator,
'index' => $index,
'highlight' => $highlight,
'concurrent' => $concurrent,
'count' => $count,
'searchAfter' => $searchAfter,
'searchBefore' => $searchBefore,
'scoreDetails' => $scoreDetails,
'sort' => $sort,
'returnStoredSource' => $returnStoredSource,
'tracking' => $tracking,
], fn ($arg) => $arg !== null);
return $this->aggregate()->search(...$args)->get();
}
/**
* Performs a semantic search on data in your Atlas Vector Search index.
* NOTE: $vectorSearch is only available for MongoDB Atlas clusters, and is not available for self-managed deployments.
*
* @see https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/
*
* @return Collection
*/
public function vectorSearch(
string $index,
string $path,
array $queryVector,
int $limit,
bool $exact = false,
QueryInterface|array|null $filter = null,
int|null $numCandidates = null,
): Collection {
// Forward named arguments to the vectorSearch stage, skip null values
$args = array_filter([
'index' => $index,
'limit' => $limit,
'path' => $path,
'queryVector' => $queryVector,
'exact' => $exact,
'filter' => $filter,
'numCandidates' => $numCandidates,
], fn ($arg) => $arg !== null);
return $this->aggregate()
->vectorSearch(...$args)
->addFields(vectorSearchScore: ['$meta' => 'vectorSearchScore'])
->get();
}
/**
* Performs an autocomplete search of the field using an Atlas Search index.
* NOTE: $search is only available for MongoDB Atlas clusters, and is not available for self-managed deployments.
* You must create an Atlas Search index with an autocomplete configuration before you can use this stage.
*
* @see https://www.mongodb.com/docs/atlas/atlas-search/autocomplete/
*
* @return Collection
*/
public function autocomplete(string $path, string $query, bool|array $fuzzy = false, string $tokenOrder = 'any'): Collection
{
$args = ['path' => $path, 'query' => $query, 'tokenOrder' => $tokenOrder];
if ($fuzzy === true) {
$args['fuzzy'] = ['maxEdits' => 2];
} elseif ($fuzzy !== false) {
$args['fuzzy'] = $fuzzy;
}
return $this->aggregate()->search(
Search::autocomplete(...$args),
)->get()->pluck($path);
}
/**
* Apply the connection's session to options if it's not already specified.
*/
private function inheritConnectionOptions(array $options = []): array
{
if (! isset($options['session'])) {
$session = $this->connection->getSession();
if ($session) {
$options['session'] = $session;
}
}
if (! isset($options['readPreference']) && isset($this->readPreference)) {
$options['readPreference'] = $this->readPreference;
}
return $options;
}
/** @inheritdoc */
#[Override]
public function __call($method, $parameters)
{
if ($method === 'unset') {
return $this->drop(...$parameters);
}
return parent::__call($method, $parameters);
}
/** @internal This method is not supported by MongoDB. */
#[Override]
public function toSql()
{
throw new BadMethodCallException('This method is not supported by MongoDB. Try "toMql()" instead.');
}
/** @internal This method is not supported by MongoDB. */
#[Override]
public function toRawSql()
{
throw new BadMethodCallException('This method is not supported by MongoDB. Try "toMql()" instead.');
}
/** @internal This method is not supported by MongoDB. */
#[Override]
public function whereColumn($first, $operator = null, $second = null, $boolean = 'and')
{
throw new BadMethodCallException('This method is not supported by MongoDB');
}
/** @internal This method is not supported by MongoDB. */
#[Override]
public function whereFullText($columns, $value, array $options = [], $boolean = 'and')
{
throw new BadMethodCallException('This method is not supported by MongoDB');
}
/** @internal This method is not supported by MongoDB. */
#[Override]
public function groupByRaw($sql, array $bindings = [])
{
throw new BadMethodCallException('This method is not supported by MongoDB');
}
/** @internal This method is not supported by MongoDB. */
#[Override]
public function orderByRaw($sql, $bindings = [])
{
throw new BadMethodCallException('This method is not supported by MongoDB');
}
/** @internal This method is not supported by MongoDB. */
#[Override]
public function unionAll($query)
{
throw new BadMethodCallException('This method is not supported by MongoDB');
}
/** @internal This method is not supported by MongoDB. */
#[Override]
public function union($query, $all = false)
{
throw new BadMethodCallException('This method is not supported by MongoDB');
}
/** @internal This method is not supported by MongoDB. */
#[Override]
public function having($column, $operator = null, $value = null, $boolean = 'and')
{
throw new BadMethodCallException('This method is not supported by MongoDB');
}
/** @internal This method is not supported by MongoDB. */
#[Override]
public function havingRaw($sql, array $bindings = [], $boolean = 'and')
{
throw new BadMethodCallException('This method is not supported by MongoDB');
}
/** @internal This method is not supported by MongoDB. */
#[Override]
public function havingBetween($column, iterable $values, $boolean = 'and', $not = false)
{
throw new BadMethodCallException('This method is not supported by MongoDB');
}
/** @internal This method is not supported by MongoDB. */
#[Override]
public function whereIntegerInRaw($column, $values, $boolean = 'and', $not = false)
{
throw new BadMethodCallException('This method is not supported by MongoDB');
}
/** @internal This method is not supported by MongoDB. */
#[Override]
public function orWhereIntegerInRaw($column, $values)
{
throw new BadMethodCallException('This method is not supported by MongoDB');
}
/** @internal This method is not supported by MongoDB. */
#[Override]
public function whereIntegerNotInRaw($column, $values, $boolean = 'and')
{
throw new BadMethodCallException('This method is not supported by MongoDB');
}
/** @internal This method is not supported by MongoDB. */
#[Override]
public function orWhereIntegerNotInRaw($column, $values, $boolean = 'and')
{
throw new BadMethodCallException('This method is not supported by MongoDB');
}
private function aliasIdForQuery(array $values, bool $root = true): array
{
if (array_key_exists('id', $values) && ($root || $this->connection->getRenameEmbeddedIdField())) {
if (array_key_exists('_id', $values) && $values['id'] !== $values['_id']) {
throw new InvalidArgumentException('Cannot have both "id" and "_id" fields.');
}
$values['_id'] = $values['id'];
unset($values['id']);
}
foreach ($values as $key => $value) {
if (! is_string($key)) {
continue;
}
// "->" arrow notation for subfields is an alias for "." dot notation
if (str_contains($key, '->')) {
$newkey = str_replace('->', '.', $key);
if (array_key_exists($newkey, $values) && $value !== $values[$newkey]) {
throw new InvalidArgumentException(sprintf('Cannot have both "%s" and "%s" fields.', $key, $newkey));
}
$values[$newkey] = $value;
unset($values[$key]);
$key = $newkey;
}
// ".id" subfield are alias for "._id"
if (str_ends_with($key, '.id') && $this->connection->getRenameEmbeddedIdField()) {
$newkey = substr($key, 0, -3) . '._id';
if (array_key_exists($newkey, $values) && $value !== $values[$newkey]) {
throw new InvalidArgumentException(sprintf('Cannot have both "%s" and "%s" fields.', $key, $newkey));
}
$values[$newkey] = $value;
unset($values[$key]);
}
}
foreach ($values as &$value) {
if (is_array($value)) {
$value = $this->aliasIdForQuery($value, false);
} elseif ($value instanceof DateTimeInterface) {
$value = new UTCDateTime($value);
}
}
return $values;
}
/**
* @internal
*
* @psalm-param T $values
*
* @psalm-return T
*
* @template T of array|object
*/
public function aliasIdForResult(array|object $values, bool $root = true): array|object
{
if (is_array($values)) {
if (
array_key_exists('_id', $values) && ! array_key_exists('id', $values)
&& ($root || $this->connection->getRenameEmbeddedIdField())
) {
$values['id'] = $values['_id'];
unset($values['_id']);
}
foreach ($values as $key => $value) {
if ($value instanceof UTCDateTime) {
$values[$key] = Date::instance($value->toDateTime())
->setTimezone(new DateTimeZone(date_default_timezone_get()));
} elseif (is_array($value) || is_object($value)) {
$values[$key] = $this->aliasIdForResult($value, false);
}
}
}
if ($values instanceof stdClass) {
if (
property_exists($values, '_id') && ! property_exists($values, 'id')
&& ($root || $this->connection->getRenameEmbeddedIdField())
) {
$values->id = $values->_id;
unset($values->_id);
}
foreach (get_object_vars($values) as $key => $value) {
if ($value instanceof UTCDateTime) {
$values->{$key} = Date::instance($value->toDateTime())
->setTimezone(new DateTimeZone(date_default_timezone_get()));
} elseif (is_array($value) || is_object($value)) {
$values->{$key} = $this->aliasIdForResult($value, false);
}
}
}
return $values;
}
}
================================================
FILE: src/Query/BuilderTimeout.php
================================================
timeout = $seconds;
return $this;
}
}
}
================================================
FILE: src/Query/Grammar.php
================================================
connections = $connections;
}
/**
* Establish a queue connection.
*
* @return Queue
*/
public function connect(array $config)
{
if (! isset($config['collection']) && isset($config['table'])) {
trigger_error('Since mongodb/laravel-mongodb 4.4: Using "table" option in queue configuration is deprecated. Use "collection" instead.', E_USER_DEPRECATED);
$config['collection'] = $config['table'];
}
if (! isset($config['retry_after']) && isset($config['expire'])) {
trigger_error('Since mongodb/laravel-mongodb 4.4: Using "expire" option in queue configuration is deprecated. Use "retry_after" instead.', E_USER_DEPRECATED);
$config['retry_after'] = $config['expire'];
}
return new MongoQueue(
$this->connections->connection($config['connection'] ?? null),
$config['collection'] ?? 'jobs',
$config['queue'] ?? 'default',
$config['retry_after'] ?? 60,
);
}
}
================================================
FILE: src/Queue/MongoJob.php
================================================
job->reserved;
}
/** @return DateTime */
public function reservedAt()
{
return $this->job->reserved_at;
}
}
================================================
FILE: src/Queue/MongoQueue.php
================================================
retryAfter = $retryAfter;
}
/**
* @return MongoJob|null
*
* @inheritdoc
*/
#[Override]
public function pop($queue = null)
{
$queue = $this->getQueue($queue);
if ($this->retryAfter !== null) {
$this->releaseJobsThatHaveBeenReservedTooLong($queue);
}
$job = $this->getNextAvailableJobAndReserve($queue);
if (! $job) {
return null;
}
return new MongoJob(
$this->container,
$this,
$job,
$this->connectionName,
$queue,
);
}
/**
* Get the next available job for the queue and mark it as reserved.
* When using multiple daemon queue listeners to process jobs there
* is a possibility that multiple processes can end up reading the
* same record before one has flagged it as reserved.
* This race condition can result in random jobs being run more than
* once. To solve this we use findOneAndUpdate to lock the next jobs
* record while flagging it as reserved at the same time.
*
* @param string|null $queue
*
* @return stdClass|null
*/
protected function getNextAvailableJobAndReserve($queue)
{
$job = $this->database->getCollection($this->table)->findOneAndUpdate(
[
'queue' => $this->getQueue($queue),
'reserved' => ['$ne' => 1],
'available_at' => ['$lte' => Carbon::now()->getTimestamp()],
],
[
'$set' => [
'reserved' => 1,
'reserved_at' => Carbon::now()->getTimestamp(),
],
'$inc' => ['attempts' => 1],
],
[
'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER,
'sort' => ['available_at' => 1],
],
);
if ($job) {
$job->id = $job->_id;
}
return $job;
}
/**
* Release the jobs that have been reserved for too long.
*
* @param string $queue
*
* @return void
*/
protected function releaseJobsThatHaveBeenReservedTooLong($queue)
{
$expiration = Carbon::now()->subSeconds($this->retryAfter)->getTimestamp();
$reserved = $this->database->table($this->table)
->where('queue', $this->getQueue($queue))
->whereNotNull('reserved_at')
->where('reserved_at', '<=', $expiration)
->get();
foreach ($reserved as $job) {
$this->releaseJob($job->id, $job->attempts);
}
}
/**
* Release the given job ID from reservation.
*
* @param string $id
* @param int $attempts
*
* @return void
*/
protected function releaseJob($id, $attempts)
{
$this->database->table($this->table)->where('_id', $id)->update([
'reserved' => 0,
'reserved_at' => null,
'attempts' => $attempts,
]);
}
/** @inheritdoc */
#[Override]
public function deleteReserved($queue, $id)
{
$this->database->table($this->table)->where('_id', $id)->delete();
}
/** @inheritdoc */
#[Override]
public function deleteAndRelease($queue, $job, $delay)
{
$this->deleteReserved($queue, $job->getJobId());
$this->release($queue, $job->getJobRecord(), $delay);
}
}
================================================
FILE: src/Relations/BelongsTo.php
================================================
*/
class BelongsTo extends EloquentBelongsTo
{
/**
* Get the key for comparing against the parent key in "has" query.
*
* @return string
*/
public function getHasCompareKey()
{
return $this->ownerKey;
}
/** @inheritdoc */
#[Override]
public function addConstraints()
{
if (static::$constraints) {
// For belongs to relationships, which are essentially the inverse of has one
// or has many relationships, we need to actually query on the primary key
// of the related models matching on the foreign key that's on a parent.
$this->query->where($this->ownerKey, '=', $this->parent->{$this->foreignKey});
}
}
/** @inheritdoc */
#[Override]
public function addEagerConstraints(array $models)
{
// We'll grab the primary key name of the related models since it could be set to
// a non-standard name and not "id". We will then construct the constraint for
// our eagerly loading query so it returns the proper models from execution.
$this->query->whereIn($this->ownerKey, $this->getEagerModelKeys($models));
}
/** @inheritdoc */
#[Override]
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
return $query;
}
/**
* Get the name of the "where in" method for eager loading.
*
* @param string $key
*
* @return string
*/
#[Override]
protected function whereInMethod(Model $model, $key)
{
return 'whereIn';
}
#[Override]
public function getQualifiedForeignKeyName(): string
{
return $this->foreignKey;
}
}
================================================
FILE: src/Relations/BelongsToMany.php
================================================
*/
class BelongsToMany extends EloquentBelongsToMany
{
/**
* Get the key for comparing against the parent key in "has" query.
*
* @return string
*/
public function getHasCompareKey()
{
return $this->getForeignKey();
}
/** @inheritdoc */
#[Override]
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
return $query;
}
/** @inheritdoc */
#[Override]
protected function hydratePivotRelation(array $models)
{
// Do nothing.
}
/**
* Set the select clause for the relation query.
*
* @return array
*/
protected function getSelectColumns(array $columns = ['*'])
{
return $columns;
}
/** @inheritdoc */
#[Override]
protected function shouldSelect(array $columns = ['*'])
{
return $columns;
}
/** @inheritdoc */
#[Override]
public function addConstraints()
{
if (static::$constraints) {
$this->setWhere();
}
}
/**
* Set the where clause for the relation query.
*
* @return $this
*/
protected function setWhere()
{
$foreign = $this->getForeignKey();
$this->query->where($foreign, '=', $this->parent->{$this->parentKey});
return $this;
}
/** @inheritdoc */
#[Override]
public function save(Model $model, array $pivotAttributes = [], $touch = true)
{
$model->save(['touch' => false]);
$this->attach($model, $pivotAttributes, $touch);
return $model;
}
/** @inheritdoc */
#[Override]
public function create(array $attributes = [], array $joining = [], $touch = true)
{
$instance = $this->related->newInstance($attributes);
// Once we save the related model, we need to attach it to the base model via
// through intermediate table so we'll use the existing "attach" method to
// accomplish this which will insert the record and any more attributes.
$instance->save(['touch' => false]);
$this->attach($instance, $joining, $touch);
return $instance;
}
/** @inheritdoc */
#[Override]
public function sync($ids, $detaching = true)
{
$changes = [
'attached' => [],
'detached' => [],
'updated' => [],
];
if ($ids instanceof Collection) {
$ids = $this->parseIds($ids);
} elseif ($ids instanceof Model) {
$ids = $this->parseIds($ids);
}
// First we need to attach any of the associated models that are not currently
// in this joining table. We'll spin through the given IDs, checking to see
// if they exist in the array of current ones, and if not we will insert.
$current = match (\MongoDB\Laravel\Eloquent\Model::isDocumentModel($this->parent)) {
true => $this->parent->{$this->relatedPivotKey} ?: [],
false => $this->parent->{$this->relationName} ?: [],
};
if ($current instanceof Collection) {
$current = $this->parseIds($current);
}
$records = $this->formatRecordsList($ids);
$current = Arr::wrap($current);
$detach = array_diff($current, array_keys($records));
// We need to make sure we pass a clean array, so that it is not interpreted
// as an associative array.
$detach = array_values($detach);
// Next, we will take the differences of the currents and given IDs and detach
// all of the entities that exist in the "current" array but are not in the
// the array of the IDs given to the method which will complete the sync.
if ($detaching && count($detach) > 0) {
$this->detach($detach);
$changes['detached'] = (array) array_map(function ($v) {
return is_numeric($v) ? (int) $v : (string) $v;
}, $detach);
}
// Now we are finally ready to attach the new records. Note that we'll disable
// touching until after the entire operation is complete so we don't fire a
// ton of touch operations until we are totally done syncing the records.
$changes = array_replace(
$changes,
$this->attachNew($records, $current, false),
);
if (count($changes['attached']) || count($changes['updated'])) {
$this->touchIfTouching();
}
return $changes;
}
/** @inheritdoc */
#[Override]
public function updateExistingPivot($id, array $attributes, $touch = true)
{
// Do nothing, we have no pivot table.
return $this;
}
/** @inheritdoc */
#[Override]
public function attach($id, array $attributes = [], $touch = true)
{
if ($id instanceof Model) {
$model = $id;
$id = $this->parseId($model);
// Attach the new parent id to the related model.
$model->push($this->foreignPivotKey, $this->parent->{$this->parentKey}, true);
} else {
if ($id instanceof Collection) {
$id = $this->parseIds($id);
}
$query = $this->newRelatedQuery();
$query->whereIn($this->relatedKey, (array) $id);
// Attach the new parent id to the related model.
$query->push($this->foreignPivotKey, $this->parent->{$this->parentKey}, true);
}
// Attach the new ids to the parent model.
if (\MongoDB\Laravel\Eloquent\Model::isDocumentModel($this->parent)) {
$this->parent->push($this->relatedPivotKey, (array) $id, true);
} else {
$instance = new $this->related();
$instance->forceFill([$this->relatedKey => $id]);
$relationData = $this->parent->{$this->relationName}->push($instance)->unique($this->relatedKey);
$this->parent->setRelation($this->relationName, $relationData);
}
if (! $touch) {
return;
}
$this->touchIfTouching();
}
/** @inheritdoc */
#[Override]
public function detach($ids = [], $touch = true)
{
if ($ids instanceof Model) {
$ids = $this->parseIds($ids);
}
$query = $this->newRelatedQuery();
// If associated IDs were passed to the method we will only delete those
// associations, otherwise all of the association ties will be broken.
// We'll return the numbers of affected rows when we do the deletes.
$ids = (array) $ids;
// Detach all ids from the parent model.
if (DocumentModel::isDocumentModel($this->parent)) {
$this->parent->pull($this->relatedPivotKey, $ids);
} else {
$value = $this->parent->{$this->relationName}
->filter(fn ($rel) => ! in_array($rel->{$this->relatedKey}, $ids));
$this->parent->setRelation($this->relationName, $value);
}
// Prepare the query to select all related objects.
if (count($ids) > 0) {
$query->whereIn($this->relatedKey, $ids);
}
// Remove the relation to the parent.
assert($this->parent instanceof Model);
assert($query instanceof \MongoDB\Laravel\Eloquent\Builder);
$query->pull($this->foreignPivotKey, $this->parent->{$this->parentKey});
if ($touch) {
$this->touchIfTouching();
}
return count($ids);
}
/** @inheritdoc */
#[Override]
protected function buildDictionary(Collection $results)
{
$foreign = $this->foreignPivotKey;
// First we will build a dictionary of child models keyed by the foreign key
// of the relation so that we will easily and quickly match them to their
// parents without having a possibly slow inner loops for every models.
$dictionary = [];
foreach ($results as $result) {
foreach ($result->$foreign as $item) {
$dictionary[$item][] = $result;
}
}
return $dictionary;
}
/** @inheritdoc */
#[Override]
public function newPivotQuery()
{
return $this->newRelatedQuery();
}
/**
* Create a new query builder for the related model.
*
* @return Builder|Model
*/
public function newRelatedQuery()
{
return $this->related->newQuery();
}
/**
* Get the fully qualified foreign key for the relation.
*
* @return string
*/
public function getForeignKey()
{
return $this->foreignPivotKey;
}
/** @inheritdoc */
#[Override]
public function getQualifiedForeignPivotKeyName()
{
return $this->foreignPivotKey;
}
/** @inheritdoc */
#[Override]
public function getQualifiedRelatedPivotKeyName()
{
return $this->relatedPivotKey;
}
/**
* Get the name of the "where in" method for eager loading.
*
* @inheritdoc
*/
#[Override]
protected function whereInMethod(Model $model, $key)
{
return 'whereIn';
}
}
================================================
FILE: src/Relations/EmbedsMany.php
================================================
*/
class EmbedsMany extends EmbedsOneOrMany
{
/** @inheritdoc */
public function initRelation(array $models, $relation)
{
foreach ($models as $model) {
$model->setRelation($relation, $this->related->newCollection());
}
return $models;
}
/** @inheritdoc */
public function getResults()
{
return $this->toCollection($this->getEmbedded());
}
/**
* Save a new model and attach it to the parent model.
*
* @return Model|bool
*/
public function performInsert(Model $model)
{
// Create a new key if needed.
if (($model->getKeyName() === '_id' || $model->getKeyName() === 'id') && ! $model->getKey()) {
$model->setAttribute($model->getKeyName(), new ObjectID());
}
// For deeply nested documents, let the parent handle the changes.
if ($this->isNested()) {
$this->associate($model);
return $this->parent->save() ? $model : false;
}
// Push the new model to the database.
$result = $this->toBase()->push($this->localKey, $model->getAttributes(), true);
// Attach the model to its parent.
if ($result) {
$this->associate($model);
}
return $result ? $model : false;
}
/**
* Save an existing model and attach it to the parent model.
*
* @return Model|bool
*/
public function performUpdate(Model $model)
{
// For deeply nested documents, let the parent handle the changes.
if ($this->isNested()) {
$this->associate($model);
return $this->parent->save();
}
// Get the correct foreign key value.
$foreignKey = $this->getForeignKeyValue($model);
$values = self::getUpdateValues($model->getDirty(), $this->localKey . '.$.');
// Update document in database.
$result = $this->toBase()->where($this->localKey . '.' . $model->getKeyName(), $foreignKey)
->update($values);
// Attach the model to its parent.
if ($result) {
$this->associate($model);
}
return $result ? $model : false;
}
/**
* Delete an existing model and detach it from the parent model.
*
* @return int
*/
public function performDelete(Model $model)
{
// For deeply nested documents, let the parent handle the changes.
if ($this->isNested()) {
$this->dissociate($model);
return $this->parent->save();
}
// Get the correct foreign key value.
$foreignKey = $this->getForeignKeyValue($model);
$result = $this->toBase()->pull($this->localKey, [$model->getKeyName() => $foreignKey]);
if ($result) {
$this->dissociate($model);
}
return $result;
}
/**
* Associate the model instance to the given parent, without saving it to the database.
*
* @return Model
*/
public function associate(Model $model)
{
if (! $this->contains($model)) {
return $this->associateNew($model);
}
return $this->associateExisting($model);
}
/**
* Dissociate the model instance from the given parent, without saving it to the database.
*
* @param mixed $ids
*
* @return int
*/
public function dissociate($ids = [])
{
$ids = $this->getIdsArrayFrom($ids);
$records = $this->getEmbedded();
$primaryKey = $this->related->getKeyName();
// Remove the document from the parent model.
foreach ($records as $i => $record) {
if (array_key_exists($primaryKey, $record) && in_array($record[$primaryKey], $ids)) {
unset($records[$i]);
}
}
$this->setEmbedded($records);
// We return the total number of deletes for the operation. The developers
// can then check this number as a boolean type value or get this total count
// of records deleted for logging, etc.
return count($ids);
}
/**
* Destroy the embedded models for the given IDs.
*
* @param mixed $ids
*
* @return int
*/
public function destroy($ids = [])
{
$count = 0;
$ids = $this->getIdsArrayFrom($ids);
// Get all models matching the given ids.
$models = $this->getResults()->only($ids);
// Pull the documents from the database.
foreach ($models as $model) {
if ($model->delete()) {
$count++;
}
}
return $count;
}
/**
* Delete all embedded models.
*
* @param null $id
*
* @note The $id is not used to delete embedded models.
*/
public function delete($id = null): int
{
throw_if($id !== null, new LogicException('The id parameter should not be used.'));
// Overwrite the local key with an empty array.
$result = $this->query->update([$this->localKey => []]);
if ($result) {
$this->setEmbedded([]);
}
return $result;
}
/**
* Destroy alias.
*
* @param mixed $ids
*
* @return int
*/
public function detach($ids = [])
{
return $this->destroy($ids);
}
/**
* Save alias.
*
* @return Model
*/
public function attach(Model $model)
{
return $this->save($model);
}
/**
* Associate a new model instance to the given parent, without saving it to the database.
*
* @param Model $model
*
* @return Model
*/
protected function associateNew($model)
{
// Create a new key if needed.
if (($model->getKeyName() === '_id' || $model->getKeyName() === 'id') && ! $model->getKey()) {
$model->setAttribute($model->getKeyName(), new ObjectID());
}
$records = $this->getEmbedded();
// Add the new model to the embedded documents.
$records[] = $model->getAttributes();
return $this->setEmbedded($records);
}
/**
* Associate an existing model instance to the given parent, without saving it to the database.
*
* @param Model $model
*
* @return Model
*/
protected function associateExisting($model)
{
// Get existing embedded documents.
$records = $this->getEmbedded();
$primaryKey = $this->related->getKeyName();
$key = $model->getKey();
// Replace the document in the parent model.
foreach ($records as &$record) {
// @phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators
if ($record[$primaryKey] == $key) {
$record = $model->getAttributes();
break;
}
}
return $this->setEmbedded($records);
}
/**
* @param int|Closure $perPage
* @param array|string $columns
* @param string $pageName
* @param int|null $page
* @param Closure|int|null $total
*
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null, $total = null)
{
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$results = $this->getEmbedded();
$results = $this->toCollection($results);
$total = value($total) ?? $results->count();
$perPage = $perPage ?: $this->related->getPerPage();
$perPage = $perPage instanceof Closure ? $perPage($total) : $perPage;
$start = ($page - 1) * $perPage;
$sliced = $results->slice(
$start,
$perPage,
);
return new LengthAwarePaginator(
$sliced,
$total,
$perPage,
$page,
[
'path' => Paginator::resolveCurrentPath(),
],
);
}
/** @inheritdoc */
protected function getEmbedded()
{
return parent::getEmbedded() ?: [];
}
/** @inheritdoc */
protected function setEmbedded($records)
{
if (! is_array($records)) {
$records = [$records];
}
return parent::setEmbedded(array_values($records));
}
/** @inheritdoc */
public function __call($method, $parameters)
{
if (method_exists(Collection::class, $method)) {
return $this->getResults()->$method(...$parameters);
}
return parent::__call($method, $parameters);
}
/**
* Get the name of the "where in" method for eager loading.
*
* @param string $key
*
* @return string
*/
protected function whereInMethod(Model $model, $key)
{
return 'whereIn';
}
}
================================================
FILE: src/Relations/EmbedsOne.php
================================================
*/
class EmbedsOne extends EmbedsOneOrMany
{
public function initRelation(array $models, $relation)
{
foreach ($models as $model) {
$model->setRelation($relation, null);
}
return $models;
}
public function getResults()
{
return $this->toModel($this->getEmbedded());
}
public function getEager()
{
$eager = $this->get();
// EmbedsOne only brings one result, Eager needs a collection!
return $this->toCollection([$eager]);
}
/**
* Save a new model and attach it to the parent model.
*
* @return Model|bool
*/
public function performInsert(Model $model)
{
// Create a new key if needed.
if (($model->getKeyName() === '_id' || $model->getKeyName() === 'id') && ! $model->getKey()) {
$model->setAttribute($model->getKeyName(), new ObjectID());
}
// For deeply nested documents, let the parent handle the changes.
if ($this->isNested()) {
$this->associate($model);
return $this->parent->save() ? $model : false;
}
$result = $this->toBase()->update([$this->localKey => $model->getAttributes()]);
// Attach the model to its parent.
if ($result) {
$this->associate($model);
}
return $result ? $model : false;
}
/**
* Save an existing model and attach it to the parent model.
*
* @return Model|bool
*/
public function performUpdate(Model $model)
{
if ($this->isNested()) {
$this->associate($model);
return $this->parent->save();
}
$values = self::getUpdateValues($model->getDirty(), $this->localKey . '.');
$result = $this->toBase()->update($values);
// Attach the model to its parent.
if ($result) {
$this->associate($model);
}
return $result ? $model : false;
}
/**
* Delete an existing model and detach it from the parent model.
*
* @return int
*/
public function performDelete()
{
// For deeply nested documents, let the parent handle the changes.
if ($this->isNested()) {
$this->dissociate();
return $this->parent->save();
}
// Overwrite the local key with an empty array.
$result = $this->toBase()->update([$this->localKey => null]);
// Detach the model from its parent.
if ($result) {
$this->dissociate();
}
return $result;
}
/**
* Attach the model to its parent.
*
* @return Model
*/
public function associate(Model $model)
{
return $this->setEmbedded($model->getAttributes());
}
/**
* Detach the model from its parent.
*
* @return Model
*/
public function dissociate()
{
return $this->setEmbedded(null);
}
/**
* Delete all embedded models.
*
* @param ?string $id
*
* @throws LogicException|Throwable
*
* @note The $id is not used to delete embedded models.
*/
public function delete($id = null): int
{
throw_if($id !== null, new LogicException('The id parameter should not be used.'));
return $this->performDelete();
}
/**
* Get the name of the "where in" method for eager loading.
*
* @param string $key
*
* @return string
*/
protected function whereInMethod(Model $model, $key)
{
return 'whereIn';
}
}
================================================
FILE: src/Relations/EmbedsOneOrMany.php
================================================
*/
abstract class EmbedsOneOrMany extends Relation
{
/**
* The local key of the parent model.
*
* @var string
*/
protected $localKey;
/**
* The foreign key of the parent model.
*
* @var string
*/
protected $foreignKey;
/**
* The "name" of the relationship.
*
* @var string
*/
protected $relation;
/**
* Create a new embeds many relationship instance.
*/
public function __construct(Builder $query, Model $parent, Model $related, string $localKey, string $foreignKey, string $relation)
{
if (! DocumentModel::isDocumentModel($parent)) {
throw new LogicException('Parent model must be a document model.');
}
if (! DocumentModel::isDocumentModel($related)) {
throw new LogicException('Related model must be a document model.');
}
parent::__construct($query, $parent);
$this->related = $related;
$this->localKey = $localKey;
$this->foreignKey = $foreignKey;
$this->relation = $relation;
// If this is a nested relation, we need to get the parent query instead.
$parentRelation = $this->getParentRelation();
if ($parentRelation) {
$this->query = $parentRelation->getQuery();
}
}
/** @inheritdoc */
#[Override]
public function addConstraints()
{
if (static::$constraints) {
$this->query->where($this->getQualifiedParentKeyName(), '=', $this->getParentKey());
}
}
/** @inheritdoc */
#[Override]
public function addEagerConstraints(array $models)
{
// There are no eager loading constraints.
}
/** @inheritdoc */
#[Override]
public function match(array $models, Collection $results, $relation)
{
foreach ($models as $model) {
$results = $model->$relation()->getResults();
$model->setParentRelation($this);
$model->setRelation($relation, $results);
}
return $models;
}
#[Override]
public function get($columns = ['*'])
{
return $this->getResults();
}
/**
* Get the number of embedded models.
*
* @param Expression|string $columns
*
* @throws LogicException|Throwable
*
* @note The $column parameter is not used to count embedded models.
*/
public function count($columns = '*'): int
{
throw_if($columns !== '*', new LogicException('The columns parameter should not be used.'));
return count($this->getEmbedded());
}
/**
* Attach a model instance to the parent model.
*
* @return Model|bool
*/
public function save(Model $model)
{
$model->setParentRelation($this);
return $model->save() ? $model : false;
}
/**
* Attach a collection of models to the parent instance.
*
* @param Collection|array $models
*
* @return Collection|array
*/
public function saveMany($models)
{
foreach ($models as $model) {
$this->save($model);
}
return $models;
}
/**
* Create a new instance of the related model.
*
* @return Model
*/
public function create(array $attributes = [])
{
// Here we will set the raw attributes to avoid hitting the "fill" method so
// that we do not have to worry about a mass accessor rules blocking sets
// on the models. Otherwise, some of these attributes will not get set.
$instance = $this->related->newInstance($attributes);
$instance->setParentRelation($this);
$instance->save();
return $instance;
}
/**
* Create an array of new instances of the related model.
*
* @return array
*/
public function createMany(array $records)
{
$instances = [];
foreach ($records as $record) {
$instances[] = $this->create($record);
}
return $instances;
}
/**
* Transform single ID, single Model or array of Models into an array of IDs.
*
* @param mixed $ids
*
* @return array
*/
protected function getIdsArrayFrom($ids)
{
if ($ids instanceof \Illuminate\Support\Collection) {
$ids = $ids->all();
}
if (! is_array($ids)) {
$ids = [$ids];
}
foreach ($ids as &$id) {
if ($id instanceof Model) {
$id = $id->getKey();
}
}
return $ids;
}
/** @inheritdoc */
protected function getEmbedded()
{
// Get raw attributes to skip relations and accessors.
$attributes = $this->parent->getAttributes();
// Get embedded models form parent attributes.
return isset($attributes[$this->localKey]) ? (array) $attributes[$this->localKey] : null;
}
/** @inheritdoc */
protected function setEmbedded($records)
{
// Assign models to parent attributes array.
$attributes = $this->parent->getAttributes();
$attributes[$this->localKey] = $records;
// Set raw attributes to skip mutators.
$this->parent->setRawAttributes($attributes);
// Set the relation on the parent.
return $this->parent->setRelation($this->relation, $records === null ? null : $this->getResults());
}
/**
* Get the foreign key value for the relation.
*
* @param mixed $id
*
* @return mixed
*/
protected function getForeignKeyValue($id)
{
if ($id instanceof Model) {
$id = $id->getKey();
}
// Convert the id to MongoId if necessary.
return $this->toBase()->convertKey($id);
}
/**
* Convert an array of records to a Collection.
*
* @return Collection
*/
protected function toCollection(array $records = [])
{
$models = [];
foreach ($records as $attributes) {
$models[] = $this->toModel($attributes);
}
if (count($models) > 0) {
$models = $this->eagerLoadRelations($models);
}
return $this->related->newCollection($models);
}
/**
* Create a related model instanced.
*
* @param mixed $attributes
*
* @return Model | null
*/
protected function toModel(mixed $attributes = []): Model|null
{
if ($attributes === null) {
return null;
}
$connection = $this->related->getConnection();
$model = $this->related->newFromBuilder(
(array) $attributes,
$connection?->getName(),
);
$model->setParentRelation($this);
$model->setRelation($this->foreignKey, $this->parent);
// If you remove this, you will get segmentation faults!
$model->setHidden(array_merge($model->getHidden(), [$this->foreignKey]));
return $model;
}
/**
* Get the relation instance of the parent.
*
* @return Relation
*/
protected function getParentRelation()
{
return $this->parent->getParentRelation();
}
/** @inheritdoc */
#[Override]
public function getQuery()
{
// Because we are sharing this relation instance to models, we need
// to make sure we use separate query instances.
return clone $this->query;
}
/** @inheritdoc */
#[Override]
public function toBase()
{
// Because we are sharing this relation instance to models, we need
// to make sure we use separate query instances.
return clone $this->query->getQuery();
}
/**
* Check if this relation is nested in another relation.
*
* @return bool
*/
protected function isNested()
{
return $this->getParentRelation() !== null;
}
/**
* Get the fully qualified local key name.
*
* @param string $glue
*
* @return string
*/
protected function getPathHierarchy($glue = '.')
{
$parentRelation = $this->getParentRelation();
if ($parentRelation) {
return $parentRelation->getPathHierarchy($glue) . $glue . $this->localKey;
}
return $this->localKey;
}
/** @inheritdoc */
#[Override]
public function getQualifiedParentKeyName()
{
$parentRelation = $this->getParentRelation();
if ($parentRelation) {
return $parentRelation->getPathHierarchy() . '.' . $this->parent->getKeyName();
}
return $this->parent->getKeyName();
}
/**
* Get the primary key value of the parent.
*
* @return string
*/
protected function getParentKey()
{
return $this->parent->getKey();
}
/**
* Return update values.
*
* @param array $array
* @param string $prepend
*
* @return array
*/
public static function getUpdateValues($array, $prepend = '')
{
$results = [];
foreach ($array as $key => $value) {
if (str_starts_with($key, '$')) {
assert(is_array($value), 'Update operator value must be an array.');
$results[$key] = static::getUpdateValues($value, $prepend);
} else {
$results[$prepend . $key] = $value;
}
}
return $results;
}
/**
* Get the foreign key for the relationship.
*
* @return string
*/
public function getQualifiedForeignKeyName()
{
return $this->foreignKey;
}
/**
* Get the name of the "where in" method for eager loading.
*
* @param EloquentModel $model
*
* @inheritdoc
*/
#[Override]
protected function whereInMethod(EloquentModel $model, $key)
{
return 'whereIn';
}
}
================================================
FILE: src/Relations/HasMany.php
================================================
*/
class HasMany extends EloquentHasMany
{
/**
* Get the plain foreign key.
*
* @return string
*/
#[Override]
public function getForeignKeyName()
{
return $this->foreignKey;
}
/**
* Get the key for comparing against the parent key in "has" query.
*
* @return string
*/
public function getHasCompareKey()
{
return $this->getForeignKeyName();
}
/** @inheritdoc */
#[Override]
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
$foreignKey = $this->getHasCompareKey();
return $query->select($foreignKey)->where($foreignKey, 'exists', true);
}
/**
* Get the name of the "where in" method for eager loading.
*
* @inheritdoc
*/
#[Override]
protected function whereInMethod(Model $model, $key)
{
return 'whereIn';
}
}
================================================
FILE: src/Relations/HasOne.php
================================================
*/
class HasOne extends EloquentHasOne
{
/**
* Get the key for comparing against the parent key in "has" query.
*
* @return string
*/
#[Override]
public function getForeignKeyName()
{
return $this->foreignKey;
}
/**
* Get the key for comparing against the parent key in "has" query.
*
* @return string
*/
public function getHasCompareKey()
{
return $this->getForeignKeyName();
}
/** @inheritdoc */
#[Override]
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
$foreignKey = $this->getForeignKeyName();
return $query->select($foreignKey)->where($foreignKey, 'exists', true);
}
/** Get the name of the "where in" method for eager loading. */
#[Override]
protected function whereInMethod(Model $model, $key)
{
return 'whereIn';
}
}
================================================
FILE: src/Relations/MorphMany.php
================================================
*/
class MorphMany extends EloquentMorphMany
{
#[Override]
protected function whereInMethod(Model $model, $key)
{
return 'whereIn';
}
}
================================================
FILE: src/Relations/MorphTo.php
================================================
*/
class MorphTo extends EloquentMorphTo
{
/** @inheritdoc */
#[Override]
public function addConstraints()
{
if (static::$constraints) {
// For belongs to relationships, which are essentially the inverse of has one
// or has many relationships, we need to actually query on the primary key
// of the related models matching on the foreign key that's on a parent.
$this->query->where(
$this->ownerKey ?? $this->getForeignKeyName(),
'=',
$this->getForeignKeyFrom($this->parent),
);
}
}
/** @inheritdoc */
#[Override]
protected function getResultsByType($type)
{
$instance = $this->createModelByType($type);
$key = $this->ownerKey ?? $instance->getKeyName();
$query = $instance->newQuery();
return $query->whereIn($key, $this->gatherKeysByType($type, $instance->getKeyType()))->get();
}
/** Get the name of the "where in" method for eager loading. */
#[Override]
protected function whereInMethod(Model $model, $key)
{
return 'whereIn';
}
}
================================================
FILE: src/Relations/MorphToMany.php
================================================
*/
class MorphToMany extends EloquentMorphToMany
{
#[Override]
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
return $query;
}
#[Override]
protected function hydratePivotRelation(array $models)
{
// Do nothing.
}
#[Override]
protected function shouldSelect(array $columns = ['*'])
{
return $columns;
}
#[Override]
public function addConstraints()
{
if (static::$constraints) {
$this->setWhere();
}
}
#[Override]
public function addEagerConstraints(array $models)
{
// To load relation's data, we act normally on MorphToMany relation,
// But on MorphedByMany relation, we collect related ids from pivot column
// and add to a whereIn condition
if ($this->getInverse()) {
$ids = $this->getKeys($models, $this->table);
$ids = $this->extractIds($ids[0] ?? []);
$this->query->whereIn($this->relatedKey, $ids);
} else {
parent::addEagerConstraints($models);
$this->query->where($this->qualifyPivotColumn($this->morphType), $this->morphClass);
}
}
/**
* Set the where clause for the relation query.
*
* @return $this
*/
protected function setWhere()
{
if ($this->getInverse()) {
if (\MongoDB\Laravel\Eloquent\Model::isDocumentModel($this->parent)) {
$ids = $this->extractIds((array) $this->parent->{$this->table});
$this->query->whereIn($this->relatedKey, $ids);
} else {
$this->query
->whereIn($this->foreignPivotKey, (array) $this->parent->{$this->parentKey});
}
} else {
match (\MongoDB\Laravel\Eloquent\Model::isDocumentModel($this->parent)) {
true => $this->query->whereIn($this->relatedKey, (array) $this->parent->{$this->relatedPivotKey}),
false => $this->query
->whereIn($this->getQualifiedForeignPivotKeyName(), (array) $this->parent->{$this->parentKey}),
};
}
return $this;
}
/** @inheritdoc */
#[Override]
public function save(Model $model, array $pivotAttributes = [], $touch = true)
{
$model->save(['touch' => false]);
$this->attach($model, $pivotAttributes, $touch);
return $model;
}
/** @inheritdoc */
#[Override]
public function create(array $attributes = [], array $joining = [], $touch = true)
{
$instance = $this->related->newInstance($attributes);
// Once we save the related model, we need to attach it to the base model via
// through intermediate table so we'll use the existing "attach" method to
// accomplish this which will insert the record and any more attributes.
$instance->save(['touch' => false]);
$this->attach($instance, $joining, $touch);
return $instance;
}
/** @inheritdoc */
#[Override]
public function sync($ids, $detaching = true)
{
$changes = [
'attached' => [],
'detached' => [],
'updated' => [],
];
if ($ids instanceof Collection) {
$ids = $this->parseIds($ids);
} elseif ($ids instanceof Model) {
$ids = $this->parseIds($ids);
}
// First we need to attach any of the associated models that are not currently
// in this joining table. We'll spin through the given IDs, checking to see
// if they exist in the array of current ones, and if not we will insert.
if ($this->getInverse()) {
$current = match (\MongoDB\Laravel\Eloquent\Model::isDocumentModel($this->parent)) {
true => $this->parent->{$this->table} ?: [],
false => $this->parent->{$this->relationName} ?: [],
};
if ($current instanceof Collection) {
$current = collect($this->parseIds($current))->flatten()->toArray();
} else {
$current = $this->extractIds($current);
}
} else {
$current = match (\MongoDB\Laravel\Eloquent\Model::isDocumentModel($this->parent)) {
true => $this->parent->{$this->relatedPivotKey} ?: [],
false => $this->parent->{$this->relationName} ?: [],
};
if ($current instanceof Collection) {
$current = $this->parseIds($current);
}
}
$records = $this->formatRecordsList($ids);
$current = Arr::wrap($current);
$detach = array_diff($current, array_keys($records));
// We need to make sure we pass a clean array, so that it is not interpreted
// as an associative array.
$detach = array_values($detach);
// Next, we will take the differences of the currents and given IDs and detach
// all of the entities that exist in the "current" array but are not in the
// the array of the IDs given to the method which will complete the sync.
if ($detaching && count($detach) > 0) {
$this->detach($detach);
$changes['detached'] = array_map(function ($v) {
return is_numeric($v) ? (int) $v : (string) $v;
}, $detach);
}
// Now we are finally ready to attach the new records. Note that we'll disable
// touching until after the entire operation is complete so we don't fire a
// ton of touch operations until we are totally done syncing the records.
$changes = array_replace(
$changes,
$this->attachNew($records, $current, false),
);
if (count($changes['attached']) || count($changes['updated'])) {
$this->touchIfTouching();
}
return $changes;
}
/** @inheritdoc */
#[Override]
public function updateExistingPivot($id, array $attributes, $touch = true): void
{
// Do nothing, we have no pivot table.
}
/** @inheritdoc */
#[Override]
public function attach($id, array $attributes = [], $touch = true)
{
if ($id instanceof Model) {
$model = $id;
$id = $this->parseId($model);
if ($this->getInverse()) {
// Attach the new ids to the parent model.
if (\MongoDB\Laravel\Eloquent\Model::isDocumentModel($this->parent)) {
$this->parent->push($this->table, [
[
$this->relatedPivotKey => $model->{$this->relatedKey},
$this->morphType => $model->getMorphClass(),
],
], true);
} else {
$this->addIdToParentRelationData($id);
}
// Attach the new parent id to the related model.
$model->push($this->foreignPivotKey, (array) $this->parent->{$this->parentKey}, true);
} else {
// Attach the new parent id to the related model.
$model->push($this->table, [
[
$this->foreignPivotKey => $this->parent->{$this->parentKey},
$this->morphType => $this->parent instanceof Model ? $this->parent->getMorphClass() : null,
],
], true);
// Attach the new ids to the parent model.
if (\MongoDB\Laravel\Eloquent\Model::isDocumentModel($this->parent)) {
$this->parent->push($this->relatedPivotKey, (array) $id, true);
} else {
$this->addIdToParentRelationData($id);
}
}
} else {
if ($id instanceof Collection) {
$id = $this->parseIds($id);
}
$id = (array) $id;
$query = $this->newRelatedQuery();
$query->whereIn($this->relatedKey, $id);
if ($this->getInverse()) {
// Attach the new parent id to the related model.
$query->push($this->foreignPivotKey, $this->parent->{$this->parentKey});
// Attach the new ids to the parent model.
if (\MongoDB\Laravel\Eloquent\Model::isDocumentModel($this->parent)) {
foreach ($id as $item) {
$this->parent->push($this->table, [
[
$this->relatedPivotKey => $item,
$this->morphType => $this->related instanceof Model ? $this->related->getMorphClass() : null,
],
], true);
}
} else {
foreach ($id as $item) {
$this->addIdToParentRelationData($item);
}
}
} else {
// Attach the new parent id to the related model.
$query->push($this->table, [
[
$this->foreignPivotKey => $this->parent->{$this->parentKey},
$this->morphType => $this->parent instanceof Model ? $this->parent->getMorphClass() : null,
],
], true);
// Attach the new ids to the parent model.
if (\MongoDB\Laravel\Eloquent\Model::isDocumentModel($this->parent)) {
$this->parent->push($this->relatedPivotKey, $id, true);
} else {
foreach ($id as $item) {
$this->addIdToParentRelationData($item);
}
}
}
}
if ($touch) {
$this->touchIfTouching();
}
}
/** @inheritdoc */
#[Override]
public function detach($ids = [], $touch = true)
{
if ($ids instanceof Model) {
$ids = $this->parseIds($ids);
}
$query = $this->newRelatedQuery();
// If associated IDs were passed to the method we will only delete those
// associations, otherwise all the association ties will be broken.
// We'll return the numbers of affected rows when we do the deletes.
$ids = (array) $ids;
// Detach all ids from the parent model.
if ($this->getInverse()) {
// Remove the relation from the parent.
$data = [];
foreach ($ids as $item) {
$data = [
...$data,
[
$this->relatedPivotKey => $item,
$this->morphType => $this->related->getMorphClass(),
],
];
}
if (\MongoDB\Laravel\Eloquent\Model::isDocumentModel($this->parent)) {
$this->parent->pull($this->table, $data);
} else {
$value = $this->parent->{$this->relationName}
->filter(fn ($rel) => ! in_array($rel->{$this->relatedKey}, $this->extractIds($data)));
$this->parent->setRelation($this->relationName, $value);
}
// Prepare the query to select all related objects.
if (count($ids) > 0) {
$query->whereIn($this->relatedKey, $ids);
}
// Remove the relation from the related.
$query->pull($this->foreignPivotKey, $this->parent->{$this->parentKey});
} else {
// Remove the relation from the parent.
if (\MongoDB\Laravel\Eloquent\Model::isDocumentModel($this->parent)) {
$this->parent->pull($this->relatedPivotKey, $ids);
} else {
$value = $this->parent->{$this->relationName}
->filter(fn ($rel) => ! in_array($rel->{$this->relatedKey}, $ids));
$this->parent->setRelation($this->relationName, $value);
}
// Prepare the query to select all related objects.
if (count($ids) > 0) {
$query->whereIn($this->relatedKey, $ids);
}
// Remove the relation to the related.
$query->pull($this->table, [
[
$this->foreignPivotKey => $this->parent->{$this->parentKey},
$this->morphType => $this->parent->getMorphClass(),
],
]);
}
if ($touch) {
$this->touchIfTouching();
}
return count($ids);
}
/** @inheritdoc */
#[Override]
protected function buildDictionary(Collection $results)
{
$foreign = $this->foreignPivotKey;
// First we will build a dictionary of child models keyed by the foreign key
// of the relation so that we will easily and quickly match them to their
// parents without having a possibly slow inner loops for every models.
$dictionary = [];
foreach ($results as $result) {
if ($this->getInverse()) {
foreach ($result->$foreign as $item) {
$dictionary[$item][] = $result;
}
} else {
// Collect $foreign value from pivot column of result model
$items = $this->extractIds($result->{$this->table} ?? [], $foreign);
foreach ($items as $item) {
$dictionary[$item][] = $result;
}
}
}
return $dictionary;
}
/** @inheritdoc */
#[Override]
public function newPivotQuery()
{
return $this->newRelatedQuery();
}
/**
* Create a new query builder for the related model.
*
* @return \Illuminate\Database\Query\Builder
*/
public function newRelatedQuery()
{
return $this->related->newQuery();
}
#[Override]
public function getQualifiedRelatedPivotKeyName()
{
return $this->relatedPivotKey;
}
#[Override]
protected function whereInMethod(Model $model, $key)
{
return 'whereIn';
}
/**
* Extract ids from given pivot table data
*
* @param array $data
* @param string|null $relatedPivotKey
*
* @return mixed
*/
public function extractIds(array $data, ?string $relatedPivotKey = null)
{
$relatedPivotKey = $relatedPivotKey ?: $this->relatedPivotKey;
return array_reduce($data, function ($carry, $item) use ($relatedPivotKey) {
if (is_array($item) && array_key_exists($relatedPivotKey, $item)) {
$carry[] = $item[$relatedPivotKey];
}
return $carry;
}, []);
}
/**
* Add the given id to the relation's data of the current parent instance.
* It helps to keep up-to-date the sql model instances in hybrid relationships.
*
* @param ObjectId|string|int $id
*
* @return void
*/
private function addIdToParentRelationData($id)
{
$instance = new $this->related();
$instance->forceFill([$this->relatedKey => $id]);
$relationData = $this->parent->{$this->relationName}->push($instance)->unique($this->relatedKey);
$this->parent->setRelation($this->relationName, $relationData);
}
}
================================================
FILE: src/Schema/Blueprint.php
================================================
fluent($columns);
// Columns are passed as a default array.
if (is_array($columns) && is_int(key($columns))) {
// Transform the columns to the required array format.
$transform = [];
foreach ($columns as $column) {
$transform[$column] = 1;
}
$columns = $transform;
}
if ($name !== null) {
$options['name'] = $name;
}
$this->collection->createIndex($columns, $options);
return $this;
}
/** @inheritdoc */
#[Override]
public function primary($columns = null, $name = null, $algorithm = null, $options = [])
{
return $this->unique($columns, $name, $algorithm, $options);
}
/** @inheritdoc */
#[Override]
public function dropIndex($index = null)
{
$index = $this->transformColumns($index);
$this->collection->dropIndex($index);
return $this;
}
/**
* Indicate that the given index should be dropped, but do not fail if it didn't exist.
*
* @param string|array $indexOrColumns
*
* @return Blueprint
*/
public function dropIndexIfExists($indexOrColumns = null)
{
if ($this->hasIndex($indexOrColumns)) {
$this->dropIndex($indexOrColumns);
}
return $this;
}
/**
* Check whether the given index exists.
*
* @param string|array $indexOrColumns
*
* @return bool
*/
public function hasIndex($indexOrColumns = null)
{
$indexOrColumns = $this->transformColumns($indexOrColumns);
foreach ($this->collection->listIndexes() as $index) {
if (is_array($indexOrColumns) && in_array($index->getName(), $indexOrColumns)) {
return true;
}
if (is_string($indexOrColumns) && $index->getName() === $indexOrColumns) {
return true;
}
}
return false;
}
public function jsonSchema(
array $schema = [],
?string $validationLevel = null,
?string $validationAction = null,
): void {
$options = array_merge(
[
'validator' => ['$jsonSchema' => $schema],
],
$validationLevel ? ['validationLevel' => $validationLevel] : [],
$validationAction ? ['validationAction' => $validationAction] : [],
);
$this->connection->getDatabase()->modifyCollection($this->collection->getCollectionName(), $options);
}
/**
* @param string|array $indexOrColumns
*
* @return string
*/
protected function transformColumns($indexOrColumns)
{
if (is_array($indexOrColumns)) {
$indexOrColumns = $this->fluent($indexOrColumns);
// Transform the columns to the index name.
$transform = [];
foreach ($indexOrColumns as $key => $value) {
if (is_int($key)) {
// There is no sorting order, use the default.
$column = $value;
$sorting = '1';
} else {
// This is a column with sorting order e.g 'my_column' => -1.
$column = $key;
$sorting = $value;
}
$transform[$column] = $column . '_' . $sorting;
}
$indexOrColumns = implode('_', $transform);
}
return $indexOrColumns;
}
/** @inheritdoc */
#[Override]
public function unique($columns = null, $name = null, $algorithm = null, $options = [])
{
$columns = $this->fluent($columns);
$options['unique'] = true;
$this->index($columns, $name, $algorithm, $options);
return $this;
}
/**
* Specify a sparse index for the collection.
*
* @param string|array $columns
* @param array $options
*
* @return Blueprint
*/
public function sparse($columns = null, $options = [])
{
$columns = $this->fluent($columns);
$options['sparse'] = true;
$this->index($columns, null, null, $options);
return $this;
}
/**
* Specify a geospatial index for the collection.
*
* @param string|array $columns
* @param string $index
* @param array $options
*
* @return Blueprint
*/
public function geospatial($columns = null, $index = '2d', $options = [])
{
if ($index === '2d' || $index === '2dsphere') {
$columns = $this->fluent($columns);
$columns = array_flip($columns);
foreach ($columns as $column => $value) {
$columns[$column] = $index;
}
$this->index($columns, null, null, $options);
}
return $this;
}
/**
* Specify the number of seconds after which a document should be considered expired based,
* on the given single-field index containing a date.
*
* @param string|array $columns
* @param int $seconds
*
* @return Blueprint
*/
public function expire($columns, $seconds)
{
$columns = $this->fluent($columns);
$this->index($columns, null, null, ['expireAfterSeconds' => $seconds]);
return $this;
}
/**
* Indicate that the collection needs to be created.
*
* @param array $options
*
* @return void
*/
#[Override]
public function create($options = [])
{
$collection = $this->collection->getCollectionName();
$db = $this->connection->getDatabase();
// Ensure the collection is created.
$db->createCollection($collection, $options);
}
/** @inheritdoc */
#[Override]
public function drop()
{
$this->collection->drop();
return $this;
}
/** @inheritdoc */
#[Override]
public function renameColumn($from, $to)
{
$this->collection->updateMany([$from => ['$exists' => true]], ['$rename' => [$from => $to]]);
return $this;
}
/** @inheritdoc */
#[Override]
public function addColumn($type, $name, array $parameters = [])
{
$this->fluent($name);
return $this;
}
/**
* Specify a sparse and unique index for the collection.
*
* @param string|array $columns
* @param array $options
*
* @return Blueprint
*
* phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
*/
public function sparse_and_unique($columns = null, $options = [])
{
$columns = $this->fluent($columns);
$options['sparse'] = true;
$options['unique'] = true;
$this->index($columns, null, null, $options);
return $this;
}
/**
* Create an Atlas Search Index.
*
* @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#std-label-search-index-definition-create
*
* @phpstan-param array{
* analyzer?: string,
* analyzers?: list,
* searchAnalyzer?: string,
* mappings: array{dynamic: true} | array{dynamic?: bool, fields: array},
* storedSource?: bool|array,
* synonyms?: list,
* ...
* } $definition
*/
public function searchIndex(array $definition, string $name = 'default'): static
{
$this->collection->createSearchIndex($definition, ['name' => $name, 'type' => 'search']);
return $this;
}
/**
* Create an Atlas Vector Search Index.
*
* @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#std-label-vector-search-index-definition-create
*
* @phpstan-param array{fields: array} $definition
*/
public function vectorSearchIndex(array $definition, string $name = 'default'): static
{
$this->collection->createSearchIndex($definition, ['name' => $name, 'type' => 'vectorSearch']);
return $this;
}
/**
* Drop an Atlas Search or Vector Search index
*/
public function dropSearchIndex(string $name): static
{
$this->collection->dropSearchIndex($name);
return $this;
}
/**
* Allow fluent columns.
*
* @param string|array $columns
*
* @return string|array
*/
protected function fluent($columns = null)
{
if ($columns === null) {
return $this->columns;
}
if (is_string($columns)) {
return $this->columns = [$columns];
}
return $this->columns = $columns;
}
/**
* Allows the use of unsupported schema methods.
*
* @param string $method
* @param array $parameters
*
* @return Blueprint
*/
public function __call($method, $parameters)
{
// Dummy.
return $this;
}
}
================================================
FILE: src/Schema/BlueprintLaravelCompatibility.php
================================================
connection = $connection;
$this->collection = $connection->getCollection($collection);
}
}
} else {
/** @internal For compatibility with Laravel 12+ */
trait BlueprintLaravelCompatibility
{
public function __construct(Connection $connection, string $collection, ?Closure $callback = null)
{
parent::__construct($connection, $collection, $callback);
$this->collection = $connection->getCollection($collection);
}
}
}
================================================
FILE: src/Schema/Builder.php
================================================
hasColumns($table, [$column]);
}
/**
* Check if columns exist in the collection schema.
*
* @param string $table
* @param string[] $columns
*/
public function hasColumns($table, array $columns): bool
{
// The field "id" (alias of "_id") always exists in MongoDB documents
$columns = array_filter($columns, fn (string $column): bool => ! in_array($column, ['_id', 'id'], true));
// Any subfield named "*.id" is an alias of "*._id"
$columns = array_map(fn (string $column): string => str_ends_with($column, '.id') ? substr($column, 0, -3) . '._id' : $column, $columns);
if ($columns === []) {
return true;
}
$collection = $this->connection->table($table);
return $collection
->where(array_fill_keys($columns, ['$exists' => true]))
->project(['_id' => 1])
->exists();
}
/**
* Determine if the given collection exists.
*
* @param string $name
*
* @return bool
*/
public function hasCollection($name)
{
$db = $this->connection->getDatabase();
$collections = iterator_to_array($db->listCollections([
'filter' => ['name' => $name],
]), false);
return count($collections) !== 0;
}
/** @inheritdoc */
#[Override]
public function hasTable($table)
{
return $this->hasCollection($table);
}
/** @inheritdoc */
#[Override]
public function table($table, Closure $callback)
{
$blueprint = $this->createBlueprint($table);
if ($callback) {
$callback($blueprint);
}
}
/** @inheritdoc */
#[Override]
public function create($table, ?Closure $callback = null, array $options = [])
{
$blueprint = $this->createBlueprint($table);
$blueprint->create($options);
if ($callback) {
$callback($blueprint);
}
}
/** @inheritdoc */
#[Override]
public function dropIfExists($table)
{
if ($this->hasCollection($table)) {
$this->drop($table);
}
}
/** @inheritdoc */
#[Override]
public function drop($table)
{
$blueprint = $this->createBlueprint($table);
$blueprint->drop();
}
/**
* @inheritdoc
*
* Drops the entire database instead of deleting each collection individually.
*
* In MongoDB, dropping the whole database is much faster than dropping collections
* one by one. The database will be automatically recreated when a new connection
* writes to it.
*/
#[Override]
public function dropAllTables()
{
$this->connection->getDatabase()->drop();
}
/**
* @param string|null $schema Database name
*
* @inheritdoc
*/
#[Override]
public function getTables($schema = null)
{
return $this->getCollectionRows('collection', $schema);
}
/**
* @param string|null $schema Database name
*
* @inheritdoc
*/
#[Override]
public function getViews($schema = null)
{
return $this->getCollectionRows('view', $schema);
}
/**
* @param string|null $schema
* @param bool $schemaQualified If a schema is provided, prefix the collection names with the schema name
*
* @return array
*/
#[Override]
public function getTableListing($schema = null, $schemaQualified = false)
{
$collections = [];
if ($schema === null || is_string($schema)) {
$collections[$schema ?? 0] = iterator_to_array($this->connection->getDatabase($schema)->listCollectionNames());
} elseif (is_array($schema)) {
foreach ($schema as $db) {
$collections[$db] = iterator_to_array($this->connection->getDatabase($db)->listCollectionNames());
}
}
if ($schema && $schemaQualified) {
$collections = array_map(fn ($db, $collections) => array_map(static fn ($collection) => $db . '.' . $collection, $collections), array_keys($collections), $collections);
}
$collections = array_merge(...array_values($collections));
sort($collections);
return $collections;
}
#[Override]
public function getColumns($table)
{
$db = null;
if (str_contains($table, '.')) {
[$db, $table] = explode('.', $table, 2);
}
$stats = $this->connection->getDatabase($db)->getCollection($table)->aggregate([
// Sample 1,000 documents to get a representative sample of the collection
['$sample' => ['size' => 1_000]],
// Convert each document to an array of fields
['$project' => ['fields' => ['$objectToArray' => '$$ROOT']]],
// Unwind to get one document per field
['$unwind' => '$fields'],
// Group by field name, count the number of occurrences and get the types
[
'$group' => [
'_id' => '$fields.k',
'total' => ['$sum' => 1],
'types' => ['$addToSet' => ['$type' => '$fields.v']],
],
],
// Get the most seen field names
['$sort' => ['total' => -1]],
// Limit to 1,000 fields
['$limit' => 1_000],
// Sort by field name
['$sort' => ['_id' => 1]],
], [
'typeMap' => ['array' => 'array'],
'allowDiskUse' => true,
])->toArray();
$columns = [];
foreach ($stats as $stat) {
sort($stat->types);
$type = implode(', ', $stat->types);
$name = $stat->_id;
if ($name === '_id') {
$name = 'id';
}
$columns[] = [
'name' => $name,
'type_name' => $type,
'type' => $type,
'collation' => null,
'nullable' => $name !== 'id',
'default' => null,
'auto_increment' => false,
'comment' => sprintf('%d occurrences', $stat->total),
'generation' => $name === 'id' ? ['type' => 'objectId', 'expression' => null] : null,
];
}
return $columns;
}
#[Override]
public function getIndexes($table)
{
$collection = $this->connection->getDatabase()->selectCollection($table);
assert($collection instanceof Collection);
$indexList = [];
$indexes = $collection->listIndexes();
foreach ($indexes as $index) {
assert($index instanceof IndexInfo);
$indexList[] = [
'name' => $index->getName(),
'columns' => array_keys($index->getKey()),
'primary' => $index->getKey() === ['_id' => 1],
'type' => match (true) {
$index->isText() => 'text',
$index->is2dSphere() => '2dsphere',
$index->isTtl() => 'ttl',
default => null,
},
'unique' => $index->isUnique(),
];
}
try {
$indexes = $collection->listSearchIndexes(['typeMap' => ['root' => 'array', 'array' => 'array', 'document' => 'array']]);
foreach ($indexes as $index) {
// Status 'DOES_NOT_EXIST' means the index has been dropped but is still in the process of being removed
if ($index['status'] === 'DOES_NOT_EXIST') {
continue;
}
$indexList[] = [
'name' => $index['name'],
'columns' => match ($index['type']) {
'search' => array_merge(
$index['latestDefinition']['mappings']['dynamic'] ? ['dynamic'] : [],
array_keys($index['latestDefinition']['mappings']['fields'] ?? []),
),
'vectorSearch' => array_column($index['latestDefinition']['fields'], 'path'),
},
'type' => $index['type'],
'primary' => false,
'unique' => false,
];
}
} catch (ServerException $exception) {
if (! self::isAtlasSearchNotSupportedException($exception)) {
throw $exception;
}
}
return $indexList;
}
#[Override]
public function getForeignKeys($table)
{
return [];
}
/**
* @return Blueprint
*
* @inheritdoc
*/
#[Override]
protected function createBlueprint($table, ?Closure $callback = null)
{
return new Blueprint($this->connection, $table);
}
/**
* Get collection.
*
* @param string $name
*
* @return bool|CollectionInfo
*/
public function getCollection($name)
{
$db = $this->connection->getDatabase();
$collections = iterator_to_array($db->listCollections([
'filter' => ['name' => $name],
]), false);
return count($collections) ? current($collections) : false;
}
/**
* Get all the collections names for the database.
*
* @deprecated
*
* @return array
*/
protected function getAllCollections()
{
trigger_error(sprintf('Since mongodb/laravel-mongodb:5.4, Method "%s()" is deprecated without replacement.', __METHOD__), E_USER_DEPRECATED);
$collections = [];
foreach ($this->connection->getDatabase()->listCollections() as $collection) {
$collections[] = $collection->getName();
}
return $collections;
}
/** @internal */
public static function isAtlasSearchNotSupportedException(ServerException $e): bool
{
return in_array($e->getCode(), [
59, // MongoDB 4 to 6, 7-community: no such command: 'createSearchIndexes'
40324, // MongoDB 4 to 6: Unrecognized pipeline stage name: '$listSearchIndexes'
115, // MongoDB 7-ent: Search index commands are only supported with Atlas.
6047401, // MongoDB 7: $listSearchIndexes stage is only allowed on MongoDB Atlas
31082, // MongoDB 8: Using Atlas Search Database Commands and the $listSearchIndexes aggregation stage requires additional configuration.
], true);
}
/** @param string|null $schema Database name */
private function getCollectionRows(string $collectionType, $schema = null)
{
$db = $this->connection->getDatabase($schema);
$collections = [];
foreach ($db->listCollections() as $collectionInfo) {
$collectionName = $collectionInfo->getName();
if ($collectionInfo->getType() !== $collectionType) {
continue;
}
$options = $collectionInfo->getOptions();
$collation = $options['collation'] ?? [];
// Aggregation is not supported on views
$stats = $collectionType !== 'view' ? $db->selectCollection($collectionName)->aggregate([
['$collStats' => ['storageStats' => ['scale' => 1]]],
['$project' => ['storageStats.totalSize' => 1]],
])->toArray() : null;
$collections[] = [
'name' => $collectionName,
'schema' => $db->getDatabaseName(),
'schema_qualified_name' => $db->getDatabaseName() . '.' . $collectionName,
'size' => $stats[0]?->storageStats?->totalSize ?? null,
'comment' => null,
'collation' => $this->collationToString($collation),
'engine' => null,
];
}
usort($collections, fn ($a, $b) => $a['name'] <=> $b['name']);
return $collections;
}
private function collationToString(array $collation): string
{
$map = [
'locale' => 'l',
'strength' => 's',
'caseLevel' => 'cl',
'caseFirst' => 'cf',
'numericOrdering' => 'no',
'alternate' => 'a',
'maxVariable' => 'mv',
'normalization' => 'n',
'backwards' => 'b',
];
$parts = [];
foreach ($collation as $key => $value) {
if (array_key_exists($key, $map)) {
$shortKey = $map[$key];
$shortValue = is_bool($value) ? ($value ? '1' : '0') : $value;
$parts[] = $shortKey . '=' . $shortValue;
}
}
return implode(';', $parts);
}
}
================================================
FILE: src/Schema/Grammar.php
================================================
['dynamic' => true],
];
private const TYPEMAP = ['root' => 'object', 'document' => 'bson', 'array' => 'bson'];
/** @param array $indexDefinitions */
public function __construct(
private Database $database,
private bool $softDelete,
private array $indexDefinitions = [],
) {
}
/**
* Update the given model in the index.
*
* @see Engine::update()
*
* @param EloquentCollection $models
*
* @throws MongoDBRuntimeException
*/
#[Override]
public function update($models)
{
assert($models instanceof EloquentCollection, new TypeError(sprintf('Argument #1 ($models) must be of type %s, %s given', EloquentCollection::class, get_debug_type($models))));
if ($models->isEmpty()) {
return;
}
if ($this->softDelete && $this->usesSoftDelete($models)) {
$models->each->pushSoftDeleteMetadata();
}
$bulk = [];
foreach ($models as $model) {
assert($model instanceof Model && method_exists($model, 'toSearchableArray'), new LogicException(sprintf('Model "%s" must use "%s" trait', $model::class, Searchable::class)));
$searchableData = $model->toSearchableArray();
$searchableData = self::serialize($searchableData);
// Skip/remove the model if it doesn't provide any searchable data
if (! $searchableData) {
$bulk[] = [
'deleteOne' => [
['_id' => $model->getScoutKey()],
],
];
continue;
}
unset($searchableData['_id']);
$searchableData = array_replace($searchableData, $model->scoutMetadata());
/** Convert the __soft_deleted set by {@see Searchable::pushSoftDeleteMetadata()}
* into a boolean for efficient storage and indexing. */
if (isset($searchableData['__soft_deleted'])) {
$searchableData['__soft_deleted'] = (bool) $searchableData['__soft_deleted'];
}
$bulk[] = [
'updateOne' => [
['_id' => $model->getScoutKey()],
// The _id field is added automatically when the document is inserted
// Update all other fields
['$set' => $searchableData],
['upsert' => true],
],
];
}
$this->getIndexableCollection($models)->bulkWrite($bulk);
}
/**
* Remove the given model from the index.
*
* @see Engine::delete()
*
* @param EloquentCollection $models
*/
#[Override]
public function delete($models): void
{
assert($models instanceof EloquentCollection, new TypeError(sprintf('Argument #1 ($models) must be of type %s, %s given', EloquentCollection::class, get_debug_type($models))));
if ($models->isEmpty()) {
return;
}
$collection = $this->getIndexableCollection($models);
$ids = $models->map(fn (Model $model) => $model->getScoutKey())->all();
$collection->deleteMany(['_id' => ['$in' => $ids]]);
}
/**
* Perform the given search on the engine.
*
* @see Engine::search()
*
* @return array
*/
#[Override]
public function search(Builder $builder)
{
return $this->performSearch($builder);
}
/**
* Perform the given search on the engine with pagination.
*
* @see Engine::paginate()
*
* @param int $perPage
* @param int $page
*
* @return array
*/
#[Override]
public function paginate(Builder $builder, $perPage, $page)
{
assert(is_int($perPage), new TypeError(sprintf('Argument #2 ($perPage) must be of type int, %s given', get_debug_type($perPage))));
assert(is_int($page), new TypeError(sprintf('Argument #3 ($page) must be of type int, %s given', get_debug_type($page))));
$builder = clone $builder;
$builder->take($perPage);
return $this->performSearch($builder, $perPage * ($page - 1));
}
/**
* Perform the given search on the engine.
*/
private function performSearch(Builder $builder, ?int $offset = null): array
{
$collection = $this->getSearchableCollection($builder->model);
if ($builder->callback) {
$cursor = call_user_func(
$builder->callback,
$collection,
$builder->query,
$offset,
);
assert($cursor instanceof CursorInterface, new LogicException(sprintf('The search builder closure must return a MongoDB cursor, %s returned', get_debug_type($cursor))));
$cursor->setTypeMap(self::TYPEMAP);
return $cursor->toArray();
}
// Using compound to combine search operators
// https://www.mongodb.com/docs/atlas/atlas-search/compound/#options
// "should" specifies conditions that contribute to the relevance score
// at least one of them must match,
// - "text" search for the text including fuzzy matching
// - "wildcard" allows special characters like * and ?, similar to LIKE in SQL
// These are the only search operators to accept wildcard path.
$compound = [
'should' => [
[
'text' => [
'query' => $builder->query,
'path' => ['wildcard' => '*'],
'fuzzy' => ['maxEdits' => 2],
'score' => ['boost' => ['value' => 5]],
],
],
[
'wildcard' => [
'query' => $builder->query . '*',
'path' => ['wildcard' => '*'],
'allowAnalyzedField' => true,
],
],
],
'minimumShouldMatch' => 1,
];
// "filter" specifies conditions on exact values to match
// "mustNot" specifies conditions on exact values that must not match
// They don't contribute to the relevance score
foreach ($builder->wheres as $field => $value) {
if ($field === '__soft_deleted') {
$value = (bool) $value;
}
$compound['filter'][] = ['equals' => ['path' => $field, 'value' => $value]];
}
foreach ($builder->whereIns as $field => $value) {
$compound['filter'][] = ['in' => ['path' => $field, 'value' => $value]];
}
foreach ($builder->whereNotIns as $field => $value) {
$compound['mustNot'][] = ['in' => ['path' => $field, 'value' => $value]];
}
// Sort by field value only if specified
$sort = [];
foreach ($builder->orders as $order) {
$sort[$order['column']] = $order['direction'] === 'asc' ? 1 : -1;
}
$pipeline = [
[
'$search' => [
'index' => self::INDEX_NAME,
'compound' => $compound,
'count' => ['type' => 'lowerBound'],
...($sort ? ['sort' => $sort] : []),
],
],
[
// Metadata field with the total count of documents
'$addFields' => ['__count' => '$$SEARCH_META.count.lowerBound'],
],
];
if ($offset) {
$pipeline[] = ['$skip' => $offset];
}
if ($builder->limit) {
$pipeline[] = ['$limit' => $builder->limit];
}
$cursor = $collection->aggregate($pipeline);
$cursor->setTypeMap(self::TYPEMAP);
return $cursor->toArray();
}
/**
* Pluck and return the primary keys of the given results.
*
* @see Engine::mapIds()
*
* @param list $results
*/
#[Override]
public function mapIds($results): Collection
{
assert(is_array($results), new TypeError(sprintf('Argument #1 ($results) must be of type array, %s given', get_debug_type($results))));
return new Collection(array_column($results, '_id'));
}
/**
* Map the given results to instances of the given model.
*
* @see Engine::map()
*
* @param Builder $builder
* @param array $results
* @param Model $model
*
* @return Collection
*/
#[Override]
public function map(Builder $builder, $results, $model): Collection
{
return $this->performMap($builder, $results, $model, false);
}
/**
* Map the given results to instances of the given model via a lazy collection.
*
* @see Engine::lazyMap()
*
* @param Builder $builder
* @param array $results
* @param Model $model
*
* @return LazyCollection
*/
#[Override]
public function lazyMap(Builder $builder, $results, $model): LazyCollection
{
return $this->performMap($builder, $results, $model, true);
}
/** @return ($lazy is true ? LazyCollection : Collection) */
private function performMap(Builder $builder, array $results, Model $model, bool $lazy): Collection|LazyCollection
{
if (! $results) {
$collection = $model->newCollection();
return $lazy ? LazyCollection::make($collection) : $collection;
}
$objectIds = array_column($results, '_id');
$objectIdPositions = array_flip($objectIds);
return $model->queryScoutModelsByIds($builder, $objectIds)
->{$lazy ? 'cursor' : 'get'}()
->filter(function ($model) use ($objectIds) {
return in_array($model->getScoutKey(), $objectIds);
})
->map(function ($model) use ($results, $objectIdPositions) {
$result = $results[$objectIdPositions[$model->getScoutKey()]] ?? [];
foreach ($result as $key => $value) {
if ($key[0] === '_' && $key !== '_id') {
$model->withScoutMetadata($key, $value);
}
}
return $model;
})
->sortBy(function ($model) use ($objectIdPositions) {
return $objectIdPositions[$model->getScoutKey()];
})
->values();
}
/**
* Get the total count from a raw result returned by the engine.
* This is an estimate if the count is larger than 1000.
*
* @see Engine::getTotalCount()
* @see https://www.mongodb.com/docs/atlas/atlas-search/counting/
*
* @param stdClass[] $results
*/
#[Override]
public function getTotalCount($results): int
{
if (! $results) {
return 0;
}
// __count field is added by the aggregation pipeline in performSearch()
// using the count.lowerBound in the $search stage
return $results[0]->__count;
}
/**
* Flush all records from the engine.
*
* @see Engine::flush()
*
* @param Model $model
*/
#[Override]
public function flush($model): void
{
assert($model instanceof Model, new TypeError(sprintf('Argument #1 ($model) must be of type %s, %s given', Model::class, get_debug_type($model))));
$collection = $this->getIndexableCollection($model);
$collection->deleteMany([]);
}
/**
* Create the MongoDB Atlas Search index.
*
* Accepted options:
* - wait: bool, default true. Wait for the index to be created.
*
* @see Engine::createIndex()
*
* @param string $name Collection name
* @param array{wait?:bool} $options
*/
#[Override]
public function createIndex($name, array $options = []): void
{
assert(is_string($name), new TypeError(sprintf('Argument #1 ($name) must be of type string, %s given', get_debug_type($name))));
$definition = $this->indexDefinitions[$name] ?? self::DEFAULT_DEFINITION;
if (! isset($definition['mappings'])) {
throw new InvalidArgumentException(sprintf('Invalid search index definition for collection "%s", the "mappings" key is required. Find documentation at https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#search-index-definition-syntax', $name));
}
// Ensure the collection exists before creating the search index
$this->database->createCollection($name);
$collection = $this->database->selectCollection($name);
$collection->createSearchIndex($definition, ['name' => self::INDEX_NAME]);
if ($options['wait'] ?? true) {
$this->wait(function () use ($collection) {
$indexes = $collection->listSearchIndexes([
'name' => self::INDEX_NAME,
'typeMap' => ['root' => 'bson'],
]);
return $indexes->current() && $indexes->current()->status === 'READY';
});
}
}
/**
* Delete a "search index", i.e. a MongoDB collection.
*
* @see Engine::deleteIndex()
*/
#[Override]
public function deleteIndex($name): void
{
assert(is_string($name), new TypeError(sprintf('Argument #1 ($name) must be of type string, %s given', get_debug_type($name))));
$this->database->dropCollection($name);
}
/** Get the MongoDB collection used to search for the provided model */
private function getSearchableCollection(Model|EloquentCollection $model): MongoDBCollection
{
if ($model instanceof EloquentCollection) {
$model = $model->first();
}
assert(method_exists($model, 'searchableAs'), sprintf('Model "%s" must use "%s" trait', $model::class, Searchable::class));
return $this->database->selectCollection($model->searchableAs());
}
/** Get the MongoDB collection used to index the provided model */
private function getIndexableCollection(Model|EloquentCollection $model): MongoDBCollection
{
if ($model instanceof EloquentCollection) {
$model = $model->first();
}
assert($model instanceof Model);
assert(method_exists($model, 'indexableAs'), sprintf('Model "%s" must use "%s" trait', $model::class, Searchable::class));
if (
$model->getConnection() instanceof Connection
&& $model->getConnection()->getDatabaseName() === $this->database->getDatabaseName()
&& $model->getTable() === $model->indexableAs()
) {
throw new LogicException(sprintf('The MongoDB Scout collection "%s.%s" must use a different collection from the collection name of the model "%s". Set the "scout.prefix" configuration or use a distinct MongoDB database', $this->database->getDatabaseName(), $model->indexableAs(), $model::class));
}
return $this->database->selectCollection($model->indexableAs());
}
private static function serialize(mixed $value): mixed
{
if ($value instanceof DateTimeInterface) {
return new UTCDateTime($value);
}
if ($value instanceof Serializable || ! is_iterable($value)) {
return $value;
}
// Convert Laravel Collections and other Iterators to arrays
if ($value instanceof Traversable) {
$value = iterator_to_array($value);
}
// Recursively serialize arrays
return array_map(self::serialize(...), $value);
}
private function usesSoftDelete(Model|EloquentCollection $model): bool
{
if ($model instanceof EloquentCollection) {
$model = $model->first();
}
return in_array(SoftDeletes::class, class_uses_recursive($model));
}
/**
* Wait for the callback to return true, use it for asynchronous
* Atlas Search index management operations.
*/
private function wait(Closure $callback): void
{
// Fallback to time() if hrtime() is not supported
$timeout = (hrtime()[0] ?? time()) + self::WAIT_TIMEOUT_SEC;
while ((hrtime()[0] ?? time()) < $timeout) {
if ($callback()) {
return;
}
sleep(1);
}
throw new MongoDBRuntimeException(sprintf('Atlas search index operation time out after %s seconds', self::WAIT_TIMEOUT_SEC));
}
}
================================================
FILE: src/Session/MongoDbSessionHandler.php
================================================
getCollection()->deleteMany(['last_activity' => ['$lt' => $this->getUTCDateTime(-$lifetime)]]);
return $result->getDeletedCount() ?? 0;
}
#[Override]
public function destroy($sessionId): bool
{
$this->getCollection()->deleteOne(['_id' => (string) $sessionId]);
return true;
}
#[Override]
public function read($sessionId): string|false
{
$result = $this->getCollection()->findOne(
['_id' => (string) $sessionId, 'expires_at' => ['$gte' => $this->getUTCDateTime()]],
[
'projection' => ['_id' => false, 'payload' => true],
'typeMap' => ['root' => 'bson'],
],
);
if ($result instanceof Document) {
return (string) $result->payload;
}
return false;
}
#[Override]
public function write($sessionId, $data): bool
{
$payload = $this->getDefaultPayload($data);
$this->getCollection()->replaceOne(
['_id' => (string) $sessionId],
$payload,
['upsert' => true],
);
return true;
}
/** Creates a TTL index that automatically deletes expired objects. */
public function createTTLIndex(): void
{
$this->collection->createIndex(
// UTCDateTime field that holds the expiration date
['expires_at' => 1],
// Delay to remove items after expiration
['expireAfterSeconds' => 0],
);
}
#[Override]
protected function getDefaultPayload($data): array
{
$payload = [
'payload' => new Binary($data),
'last_activity' => $this->getUTCDateTime(),
'expires_at' => $this->getUTCDateTime($this->minutes * 60),
];
if (! $this->container) {
return $payload;
}
return tap($payload, function (&$payload) {
$this->addUserInformation($payload)
->addRequestInformation($payload);
});
}
private function getCollection(): Collection
{
return $this->collection ??= $this->connection->getCollection($this->table);
}
private function getUTCDateTime(int $additionalSeconds = 0): UTCDateTime
{
return new UTCDateTime((time() + $additionalSeconds) * 1000);
}
}
================================================
FILE: src/Validation/DatabasePresenceVerifier.php
================================================
table($collection)->where($column, new Regex('^' . preg_quote($value) . '$', '/i'));
if ($excludeId !== null && $excludeId !== 'NULL') {
$query->where($idColumn ?: 'id', '<>', $excludeId);
}
foreach ($extra as $key => $extraValue) {
$this->addWhere($query, $key, $extraValue);
}
return $query->count();
}
/** Count the number of objects in a collection with the given values. */
#[Override]
public function getMultiCount($collection, $column, array $values, array $extra = [])
{
// Nothing can match an empty array. Return early to avoid matching an empty string.
if ($values === []) {
return 0;
}
// Generates a regex like '/^(a|b|c)$/i' which can query multiple values
$regex = new Regex('^(' . implode('|', array_map(preg_quote(...), $values)) . ')$', 'i');
$query = $this->table($collection)->where($column, 'regex', $regex);
foreach ($extra as $key => $extraValue) {
$this->addWhere($query, $key, $extraValue);
}
return $query->count();
}
}
================================================
FILE: src/Validation/ValidationServiceProvider.php
================================================
app->singleton('validation.presence', function ($app) {
return new DatabasePresenceVerifier($app['db']);
});
}
}
================================================
FILE: tests/AtlasSearchIndexManagement.php
================================================
listSearchIndexes()->count()) {
if (hrtime()[0] > $timeout) {
throw new RuntimeException('Timed out waiting for search indexes to be dropped');
}
usleep(1000);
}
}
/**
* Waits for all search indexes to be ready
*/
public function waitForSearchIndexesReady(Collection $collection)
{
$timeout = hrtime()[0] + 30;
do {
if (hrtime()[0] > $timeout) {
throw new RuntimeException('Timed out waiting for search indexes to be ready');
}
usleep(1000);
$ready = true;
foreach ($collection->listSearchIndexes() as $index) {
$ready = $ready && $index['queryable'];
}
} while (! $ready);
}
}
================================================
FILE: tests/AtlasSearchTest.php
================================================
getConnection('mongodb')->getCollection('books');
assert($collection instanceof MongoDBCollection);
$collection->drop();
Book::insert($this->addVector([
['title' => 'Introduction to Algorithms'],
['title' => 'Clean Code: A Handbook of Agile Software Craftsmanship'],
['title' => 'Design Patterns: Elements of Reusable Object-Oriented Software'],
['title' => 'The Pragmatic Programmer: Your Journey to Mastery'],
['title' => 'Artificial Intelligence: A Modern Approach'],
['title' => 'Structure and Interpretation of Computer Programs'],
['title' => 'Code Complete: A Practical Handbook of Software Construction'],
['title' => 'The Art of Computer Programming'],
['title' => 'Computer Networks'],
['title' => 'Operating System Concepts'],
['title' => 'Database System Concepts'],
['title' => 'Compilers: Principles, Techniques, and Tools'],
['title' => 'Introduction to the Theory of Computation'],
['title' => 'Modern Operating Systems'],
['title' => 'Computer Organization and Design'],
['title' => 'The Mythical Man-Month: Essays on Software Engineering'],
['title' => 'Algorithms'],
['title' => 'Understanding Machine Learning: From Theory to Algorithms'],
['title' => 'Deep Learning'],
['title' => 'Pattern Recognition and Machine Learning'],
]));
try {
$this->waitForSearchIndexesDropped($collection);
$collection->createSearchIndex([
'mappings' => [
'fields' => [
'title' => [
['type' => 'string', 'analyzer' => 'lucene.english'],
['type' => 'autocomplete', 'analyzer' => 'lucene.english'],
['type' => 'token'],
],
],
],
]);
$collection->createSearchIndex([
'mappings' => ['dynamic' => true],
], ['name' => 'dynamic_search']);
$collection->createSearchIndex([
'fields' => [
['type' => 'vector', 'numDimensions' => 4, 'path' => 'vector4', 'similarity' => 'cosine'],
['type' => 'vector', 'numDimensions' => 32, 'path' => 'vector32', 'similarity' => 'euclidean'],
['type' => 'filter', 'path' => 'title'],
],
], ['name' => 'vector', 'type' => 'vectorSearch']);
} catch (ServerException $e) {
if (Builder::isAtlasSearchNotSupportedException($e)) {
self::markTestSkipped('Atlas Search not supported. ' . $e->getMessage());
}
throw $e;
}
$this->waitForSearchIndexesReady($collection);
}
public function tearDown(): void
{
$this->getConnection('mongodb')->getCollection('books')->drop();
parent::tearDown();
}
public function testGetIndexes()
{
$indexes = Schema::getIndexes('books');
self::assertIsArray($indexes);
self::assertCount(4, $indexes);
// Order of indexes is not guaranteed
usort($indexes, fn ($a, $b) => $a['name'] <=> $b['name']);
$expected = [
[
'name' => '_id_',
'columns' => ['_id'],
'primary' => true,
'type' => null,
'unique' => false,
],
[
'name' => 'default',
'columns' => ['title'],
'type' => 'search',
'primary' => false,
'unique' => false,
],
[
'name' => 'dynamic_search',
'columns' => ['dynamic'],
'type' => 'search',
'primary' => false,
'unique' => false,
],
[
'name' => 'vector',
'columns' => ['vector4', 'vector32', 'title'],
'type' => 'vectorSearch',
'primary' => false,
'unique' => false,
],
];
self::assertSame($expected, $indexes);
}
public function testEloquentBuilderSearch()
{
$results = Book::search(
sort: ['title' => 1],
operator: Search::text('title', 'systems'),
);
self::assertInstanceOf(EloquentCollection::class, $results);
self::assertCount(3, $results);
self::assertInstanceOf(Book::class, $results->first());
self::assertSame([
'Database System Concepts',
'Modern Operating Systems',
'Operating System Concepts',
], $results->pluck('title')->all());
}
public function testDatabaseBuilderSearch()
{
$results = $this->getConnection('mongodb')->table('books')
->search(Search::text('title', 'systems'), sort: ['title' => 1]);
self::assertInstanceOf(LaravelCollection::class, $results);
self::assertCount(3, $results);
self::assertIsArray($results->first());
self::assertSame([
'Database System Concepts',
'Modern Operating Systems',
'Operating System Concepts',
], $results->pluck('title')->all());
}
public function testEloquentBuilderAutocomplete()
{
$results = Book::autocomplete('title', 'system');
self::assertInstanceOf(LaravelCollection::class, $results);
self::assertCount(3, $results);
self::assertSame([
'Database System Concepts',
'Modern Operating Systems',
'Operating System Concepts',
], $results->sort()->values()->all());
}
public function testDatabaseBuilderAutocomplete()
{
$results = $this->getConnection('mongodb')->table('books')
->autocomplete('title', 'system');
self::assertInstanceOf(LaravelCollection::class, $results);
self::assertCount(3, $results);
self::assertSame([
'Database System Concepts',
'Modern Operating Systems',
'Operating System Concepts',
], $results->sort()->values()->all());
}
public function testDatabaseBuilderVectorSearch()
{
$results = $this->getConnection('mongodb')->table('books')
->vectorSearch(
index: 'vector',
path: 'vector4',
queryVector: $this->vectors[7], // This is an exact match of the vector
limit: 4,
exact: true,
);
self::assertInstanceOf(LaravelCollection::class, $results);
self::assertCount(4, $results);
self::assertSame('The Art of Computer Programming', $results->first()['title']);
self::assertSame(1.0, $results->first()['vectorSearchScore']);
}
public function testEloquentBuilderVectorSearch()
{
$results = Book::vectorSearch(
index: 'vector',
path: 'vector4',
queryVector: $this->vectors[7],
limit: 5,
numCandidates: 15,
// excludes the exact match
filter: Query::query(
title: Query::ne('The Art of Computer Programming'),
),
);
self::assertInstanceOf(EloquentCollection::class, $results);
self::assertCount(5, $results);
self::assertInstanceOf(Book::class, $results->first());
self::assertNotSame('The Art of Computer Programming', $results->first()->title);
self::assertSame('The Mythical Man-Month: Essays on Software Engineering', $results->first()->title);
self::assertThat(
$results->first()->vectorSearchScore,
self::logicalAnd(self::isType('float'), self::greaterThan(0.9), self::lessThan(1.0)),
);
}
/** Generate random vectors using fixed seed to make tests deterministic */
private function addVector(array $items): array
{
srand(1);
foreach ($items as &$item) {
$this->vectors[] = $item['vector4'] = array_map(fn () => rand() / mt_getrandmax(), range(0, 3));
}
return $items;
}
}
================================================
FILE: tests/AuthTest.php
================================================
truncate();
parent::tearDown();
}
public function testAuthAttempt()
{
User::create([
'name' => 'John Doe',
'email' => 'john.doe@example.com',
'password' => Hash::make('foobar'),
]);
$this->assertTrue(Auth::attempt(['email' => 'john.doe@example.com', 'password' => 'foobar'], true));
$this->assertTrue(Auth::check());
}
public function testRemindOld()
{
$broker = $this->app->make('auth.password.broker');
$user = User::create([
'name' => 'John Doe',
'email' => 'john.doe@example.com',
'password' => Hash::make('foobar'),
]);
$token = null;
$this->assertSame(
PasswordBroker::RESET_LINK_SENT,
$broker->sendResetLink(
['email' => 'john.doe@example.com'],
function ($actualUser, $actualToken) use ($user, &$token) {
$this->assertEquals($user->id, $actualUser->id);
// Store token for later use
$token = $actualToken;
},
),
);
$this->assertEquals(1, DB::table('password_reset_tokens')->count());
$reminder = DB::table('password_reset_tokens')->first();
$this->assertEquals('john.doe@example.com', $reminder->email);
$this->assertNotNull($reminder->token);
$this->assertInstanceOf(Carbon::class, $reminder->created_at);
$credentials = [
'email' => 'john.doe@example.com',
'password' => 'foobar',
'password_confirmation' => 'foobar',
'token' => $token,
];
$response = $broker->reset($credentials, function ($user, $password) {
$user->password = bcrypt($password);
$user->save();
});
$this->assertEquals('passwords.reset', $response);
$this->assertEquals(0, DB::table('password_resets')->count());
}
}
================================================
FILE: tests/Bus/Fixtures/ChainHeadJob.php
================================================
getCollection('job_batches')->drop();
unset(
$_SERVER['__catch.batch'],
$_SERVER['__catch.count'],
$_SERVER['__catch.exception'],
$_SERVER['__finally.batch'],
$_SERVER['__finally.count'],
$_SERVER['__progress.batch'],
$_SERVER['__progress.count'],
$_SERVER['__then.batch'],
$_SERVER['__then.count'],
);
parent::tearDown();
}
/** @see BusBatchTest::test_jobs_can_be_added_to_the_batch */
public function testJobsCanBeAddedToTheBatch(): void
{
$queue = m::mock(Factory::class);
$batch = $this->createTestBatch($queue);
$job = new class
{
use Batchable;
};
$secondJob = new class
{
use Batchable;
};
$thirdJob = function () {
};
$queue->shouldReceive('connection')->once()
->with('test-connection')
->andReturn($connection = m::mock(stdClass::class));
$connection->shouldReceive('bulk')->once()->with(m::on(function ($args) use ($job, $secondJob) {
return $args[0] === $job &&
$args[1] === $secondJob &&
$args[2] instanceof CallQueuedClosure
&& is_string($args[2]->batchId);
}), '', 'test-queue');
$batch = $batch->add([$job, $secondJob, $thirdJob]);
$this->assertEquals(3, $batch->totalJobs);
$this->assertEquals(3, $batch->pendingJobs);
$this->assertIsString($job->batchId);
$this->assertInstanceOf(CarbonImmutable::class, $batch->createdAt);
}
/** @see BusBatchTest::test_successful_jobs_can_be_recorded */
public function testSuccessfulJobsCanBeRecorded()
{
$queue = m::mock(Factory::class);
$batch = $this->createTestBatch($queue);
$job = new class
{
use Batchable;
};
$secondJob = new class
{
use Batchable;
};
$queue->shouldReceive('connection')->once()
->with('test-connection')
->andReturn($connection = m::mock(stdClass::class));
$connection->shouldReceive('bulk')->once();
$batch = $batch->add([$job, $secondJob]);
$this->assertEquals(2, $batch->pendingJobs);
$batch->recordSuccessfulJob('test-id');
$batch->recordSuccessfulJob('test-id');
$this->assertInstanceOf(Batch::class, $_SERVER['__finally.batch']);
$this->assertInstanceOf(Batch::class, $_SERVER['__progress.batch']);
$this->assertInstanceOf(Batch::class, $_SERVER['__then.batch']);
$batch = $batch->fresh();
$this->assertEquals(0, $batch->pendingJobs);
$this->assertTrue($batch->finished());
$this->assertEquals(1, $_SERVER['__finally.count']);
$this->assertEquals(2, $_SERVER['__progress.count']);
$this->assertEquals(1, $_SERVER['__then.count']);
}
/** @see BusBatchTest::test_failed_jobs_can_be_recorded_while_not_allowing_failures */
public function testFailedJobsCanBeRecordedWhileNotAllowingFailures()
{
$queue = m::mock(Factory::class);
$batch = $this->createTestBatch($queue, $allowFailures = false);
$job = new class
{
use Batchable;
};
$secondJob = new class
{
use Batchable;
};
$queue->shouldReceive('connection')->once()
->with('test-connection')
->andReturn($connection = m::mock(stdClass::class));
$connection->shouldReceive('bulk')->once();
$batch = $batch->add([$job, $secondJob]);
$this->assertEquals(2, $batch->pendingJobs);
$batch->recordFailedJob('test-id', new RuntimeException('Something went wrong.'));
$batch->recordFailedJob('test-id', new RuntimeException('Something else went wrong.'));
$this->assertInstanceOf(Batch::class, $_SERVER['__finally.batch']);
$this->assertFalse(isset($_SERVER['__then.batch']));
$batch = $batch->fresh();
$this->assertEquals(2, $batch->pendingJobs);
$this->assertEquals(2, $batch->failedJobs);
$this->assertTrue($batch->finished());
$this->assertTrue($batch->cancelled());
$this->assertEquals(1, $_SERVER['__finally.count']);
$this->assertEquals(0, $_SERVER['__progress.count']);
$this->assertEquals(1, $_SERVER['__catch.count']);
$this->assertSame('Something went wrong.', $_SERVER['__catch.exception']->getMessage());
}
/** @see BusBatchTest::test_failed_jobs_can_be_recorded_while_allowing_failures */
public function testFailedJobsCanBeRecordedWhileAllowingFailures()
{
$queue = m::mock(Factory::class);
$batch = $this->createTestBatch($queue, $allowFailures = true);
$job = new class
{
use Batchable;
};
$secondJob = new class
{
use Batchable;
};
$queue->shouldReceive('connection')->once()
->with('test-connection')
->andReturn($connection = m::mock(stdClass::class));
$connection->shouldReceive('bulk')->once();
$batch = $batch->add([$job, $secondJob]);
$this->assertEquals(2, $batch->pendingJobs);
$batch->recordFailedJob('test-id', new RuntimeException('Something went wrong.'));
$batch->recordFailedJob('test-id', new RuntimeException('Something else went wrong.'));
// While allowing failures this batch never actually completes...
$this->assertFalse(isset($_SERVER['__then.batch']));
$batch = $batch->fresh();
$this->assertEquals(2, $batch->pendingJobs);
$this->assertEquals(2, $batch->failedJobs);
$this->assertFalse($batch->finished());
$this->assertFalse($batch->cancelled());
$this->assertEquals(1, $_SERVER['__catch.count']);
$this->assertEquals(2, $_SERVER['__progress.count']);
$this->assertSame('Something went wrong.', $_SERVER['__catch.exception']->getMessage());
}
/** @see BusBatchTest::test_batch_can_be_cancelled */
public function testBatchCanBeCancelled()
{
$queue = m::mock(Factory::class);
$batch = $this->createTestBatch($queue);
$batch->cancel();
$batch = $batch->fresh();
$this->assertTrue($batch->cancelled());
}
/** @see BusBatchTest::test_batch_can_be_deleted */
public function testBatchCanBeDeleted()
{
$queue = m::mock(Factory::class);
$batch = $this->createTestBatch($queue);
$batch->delete();
$batch = $batch->fresh();
$this->assertNull($batch);
}
/** @see BusBatchTest::test_batch_state_can_be_inspected */
public function testBatchStateCanBeInspected()
{
$queue = m::mock(Factory::class);
$batch = $this->createTestBatch($queue);
$this->assertFalse($batch->finished());
$batch->finishedAt = now();
$this->assertTrue($batch->finished());
$batch->options['progress'] = [];
$this->assertFalse($batch->hasProgressCallbacks());
$batch->options['progress'] = [1];
$this->assertTrue($batch->hasProgressCallbacks());
$batch->options['then'] = [];
$this->assertFalse($batch->hasThenCallbacks());
$batch->options['then'] = [1];
$this->assertTrue($batch->hasThenCallbacks());
$this->assertFalse($batch->allowsFailures());
$batch->options['allowFailures'] = true;
$this->assertTrue($batch->allowsFailures());
$this->assertFalse($batch->hasFailures());
$batch->failedJobs = 1;
$this->assertTrue($batch->hasFailures());
$batch->options['catch'] = [];
$this->assertFalse($batch->hasCatchCallbacks());
$batch->options['catch'] = [1];
$this->assertTrue($batch->hasCatchCallbacks());
$this->assertFalse($batch->cancelled());
$batch->cancelledAt = now();
$this->assertTrue($batch->cancelled());
$this->assertIsString(json_encode($batch));
}
/** @see BusBatchTest:test_chain_can_be_added_to_batch: */
public function testChainCanBeAddedToBatch()
{
$queue = m::mock(Factory::class);
$batch = $this->createTestBatch($queue);
$chainHeadJob = new ChainHeadJob();
$secondJob = new SecondTestJob();
$thirdJob = new ThirdTestJob();
$queue->shouldReceive('connection')->once()
->with('test-connection')
->andReturn($connection = m::mock(stdClass::class));
$connection->shouldReceive('bulk')->once()->with(m::on(function ($args) use ($chainHeadJob, $secondJob, $thirdJob) {
return $args[0] === $chainHeadJob
&& serialize($secondJob) === $args[0]->chained[0]
&& serialize($thirdJob) === $args[0]->chained[1];
}), '', 'test-queue');
$batch = $batch->add([
[$chainHeadJob, $secondJob, $thirdJob],
]);
$this->assertEquals(3, $batch->totalJobs);
$this->assertEquals(3, $batch->pendingJobs);
$this->assertSame('test-queue', $chainHeadJob->chainQueue);
$this->assertIsString($chainHeadJob->batchId);
$this->assertIsString($secondJob->batchId);
$this->assertIsString($thirdJob->batchId);
$this->assertInstanceOf(CarbonImmutable::class, $batch->createdAt);
}
/** @see BusBatchTest::createTestBatch() */
private function createTestBatch(Factory $queue, $allowFailures = false)
{
$connection = DB::connection('mongodb');
$this->assertInstanceOf(Connection::class, $connection);
$repository = new MongoBatchRepository(new BatchFactory($queue), $connection, 'job_batches');
$pendingBatch = (new PendingBatch(new Container(), collect()))
->progress(function (Batch $batch) {
$_SERVER['__progress.batch'] = $batch;
$_SERVER['__progress.count']++;
})
->then(function (Batch $batch) {
$_SERVER['__then.batch'] = $batch;
$_SERVER['__then.count']++;
})
->catch(function (Batch $batch, $e) {
$_SERVER['__catch.batch'] = $batch;
$_SERVER['__catch.exception'] = $e;
$_SERVER['__catch.count']++;
})
->finally(function (Batch $batch) {
$_SERVER['__finally.batch'] = $batch;
$_SERVER['__finally.count']++;
})
->allowFailures($allowFailures)
->onConnection('test-connection')
->onQueue('test-queue');
return $repository->store($pendingBatch);
}
}
================================================
FILE: tests/Cache/MongoCacheStoreTest.php
================================================
getCollection($this->getCacheCollectionName())
->drop();
parent::tearDown();
}
public function testGetNullWhenItemDoesNotExist()
{
$store = $this->getStore();
$this->assertNull($store->get('foo'));
}
public function testValueCanStoreNewCache()
{
$store = $this->getStore();
$store->put('foo', 'bar', 60);
$this->assertSame('bar', $store->get('foo'));
}
public function testPutOperationShouldNotStoreExpired()
{
$store = $this->getStore();
$store->put('foo', 'bar', 0);
$this->assertDatabaseMissing($this->getCacheCollectionName(), ['_id' => $this->withCachePrefix('foo')]);
}
public function testValueCanUpdateExistCache()
{
$store = $this->getStore();
$store->put('foo', 'bar', 60);
$store->put('foo', 'new-bar', 60);
$this->assertSame('new-bar', $store->get('foo'));
}
public function testValueCanUpdateExistCacheInTransaction()
{
$store = $this->getStore();
$store->put('foo', 'bar', 60);
// Transactions are not used in MongoStore
DB::beginTransaction();
$store->put('foo', 'new-bar', 60);
DB::commit();
$this->assertSame('new-bar', $store->get('foo'));
}
public function testAddOperationShouldNotStoreExpired()
{
$store = $this->getStore();
$result = $store->add('foo', 'bar', 0);
$this->assertFalse($result);
$this->assertDatabaseMissing($this->getCacheCollectionName(), ['_id' => $this->withCachePrefix('foo')]);
}
public function testAddOperationCanStoreNewCache()
{
$store = $this->getStore();
$result = $store->add('foo', 'bar', 60);
$this->assertTrue($result);
$this->assertSame('bar', $store->get('foo'));
}
public function testAddOperationShouldNotUpdateExistCache()
{
$store = $this->getStore();
$store->add('foo', 'bar', 60);
$result = $store->add('foo', 'new-bar', 60);
$this->assertFalse($result);
$this->assertSame('bar', $store->get('foo'));
}
public function testAddOperationShouldNotUpdateExistCacheInTransaction()
{
$store = $this->getStore();
$store->add('foo', 'bar', 60);
DB::beginTransaction();
$result = $store->add('foo', 'new-bar', 60);
DB::commit();
$this->assertFalse($result);
$this->assertSame('bar', $store->get('foo'));
}
public function testAddOperationCanUpdateIfCacheExpired()
{
$store = $this->getStore();
$this->insertToCacheTable('foo', 'bar', 0);
$result = $store->add('foo', 'new-bar', 60);
$this->assertTrue($result);
$this->assertSame('new-bar', $store->get('foo'));
}
public function testAddOperationCanUpdateIfCacheExpiredInTransaction()
{
$store = $this->getStore();
$this->insertToCacheTable('foo', 'bar', 0);
DB::beginTransaction();
$result = $store->add('foo', 'new-bar', 60);
DB::commit();
$this->assertTrue($result);
$this->assertSame('new-bar', $store->get('foo'));
}
public function testGetOperationReturnNullIfExpired()
{
$store = $this->getStore();
$this->insertToCacheTable('foo', 'bar', 0);
$result = $store->get('foo');
$this->assertNull($result);
}
public function testGetOperationCanDeleteExpired()
{
$store = $this->getStore();
$this->insertToCacheTable('foo', 'bar', 0);
$store->get('foo');
$this->assertDatabaseMissing($this->getCacheCollectionName(), ['_id' => $this->withCachePrefix('foo')]);
}
public function testForgetIfExpiredOperationCanDeleteExpired()
{
$store = $this->getStore();
$this->insertToCacheTable('foo', 'bar', 0);
$store->forgetIfExpired('foo');
$this->assertDatabaseMissing($this->getCacheCollectionName(), ['_id' => $this->withCachePrefix('foo')]);
}
public function testForgetIfExpiredOperationShouldNotDeleteUnExpired()
{
$store = $this->getStore();
$store->put('foo', 'bar', 60);
$store->forgetIfExpired('foo');
$this->assertDatabaseHas($this->getCacheCollectionName(), ['_id' => $this->withCachePrefix('foo')]);
}
public function testIncrementDecrement()
{
$store = $this->getStore();
$this->assertFalse($store->increment('foo', 10));
$this->assertFalse($store->decrement('foo', 10));
$store->put('foo', 3.5, 60);
$this->assertSame(13.5, $store->increment('foo', 10));
$this->assertSame(12.0, $store->decrement('foo', 1.5));
$store->forget('foo');
$this->insertToCacheTable('foo', 10, -5);
$this->assertFalse($store->increment('foo', 5));
}
public function testTouchReturnsFalseWhenKeyDoesNotExist()
{
$store = $this->getStore();
$this->assertFalse($store->touch('foo', 60));
}
public function testTouchExtendsExpirationAndPreservesValue()
{
$store = $this->getStore();
$this->insertToCacheTable('foo', 'bar', 60);
$result = $store->touch('foo', 3600);
$this->assertTrue($result);
$this->assertSame('bar', $store->get('foo'));
$document = DB::connection('mongodb')
->getCollection($this->getCacheCollectionName())
->findOne(['_id' => $this->withCachePrefix('foo')]);
$this->assertGreaterThan(
new UTCDateTime(Carbon::now()->addSeconds(60)),
$document['expires_at'],
);
}
public function testTouchReturnsFalseOnExpiredItem()
{
$store = $this->getStore();
$this->insertToCacheTable('foo', 'bar', -5);
$this->assertFalse($store->touch('foo', 60));
$this->assertNull($store->get('foo'));
}
public function testTTLIndex()
{
$store = $this->getStore();
$store->createTTLIndex();
// TTL index remove expired items asynchronously, this test would be very slow
$indexes = DB::connection('mongodb')->getCollection($this->getCacheCollectionName())->listIndexes();
$this->assertCount(2, $indexes);
}
private function getStore(): Repository
{
$repository = Cache::store('mongodb');
assert($repository instanceof Repository);
return $repository;
}
private function getCacheCollectionName(): string
{
return config('cache.stores.mongodb.collection');
}
private function withCachePrefix(string $key): string
{
return config('cache.prefix') . $key;
}
private function insertToCacheTable(string $key, $value, $ttl = 60)
{
DB::connection('mongodb')
->getCollection($this->getCacheCollectionName())
->insertOne([
'_id' => $this->withCachePrefix($key),
'value' => $value,
'expires_at' => new UTCDateTime(Carbon::now()->addSeconds($ttl)),
]);
}
}
================================================
FILE: tests/Cache/MongoLockTest.php
================================================
getCollection('foo_cache_locks')->drop();
parent::tearDown();
}
#[TestWith([[5, 2]])]
#[TestWith([['foo', 10]])]
#[TestWith([[10, 'foo']])]
#[TestWith([[10]])]
public function testInvalidLottery(array $lottery)
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Lock lottery must be a couple of integers');
new MongoLock(
$this->createMock(Collection::class),
'cache_lock',
10,
lottery: $lottery,
);
}
public function testLockCanBeAcquired()
{
$lock = $this->getCache()->lock('foo');
$this->assertTrue($lock->get());
$this->assertTrue($lock->get());
$otherLock = $this->getCache()->lock('foo');
$this->assertFalse($otherLock->get());
$lock->release();
$otherLock = $this->getCache()->lock('foo');
$this->assertTrue($otherLock->get());
$this->assertTrue($otherLock->get());
$otherLock->release();
}
public function testLockCanBeForceReleased()
{
$lock = $this->getCache()->lock('foo');
$this->assertTrue($lock->get());
$otherLock = $this->getCache()->lock('foo');
$otherLock->forceRelease();
$this->assertTrue($otherLock->get());
$otherLock->release();
}
public function testExpiredLockCanBeRetrieved()
{
$lock = $this->getCache()->lock('foo');
$this->assertTrue($lock->get());
DB::table('foo_cache_locks')->update(['expires_at' => new UTCDateTime(Carbon::now('UTC')->subDays(1))]);
$otherLock = $this->getCache()->lock('foo');
$this->assertTrue($otherLock->get());
$otherLock->release();
}
public function testOwnedByCurrentProcess()
{
$lock = $this->getCache()->lock('foo');
$this->assertFalse($lock->isOwnedByCurrentProcess());
$lock->acquire();
$this->assertTrue($lock->isOwnedByCurrentProcess());
$otherLock = $this->getCache()->lock('foo');
$this->assertFalse($otherLock->isOwnedByCurrentProcess());
}
public function testRestoreLock()
{
$lock = $this->getCache()->lock('foo');
$lock->acquire();
$this->assertInstanceOf(MongoLock::class, $lock);
$owner = $lock->owner();
$resoredLock = $this->getCache()->restoreLock('foo', $owner);
$this->assertTrue($resoredLock->isOwnedByCurrentProcess());
$resoredLock->release();
$this->assertFalse($resoredLock->isOwnedByCurrentProcess());
}
public function testTTLIndex()
{
$store = $this->getCache()->lock('')->createTTLIndex();
// TTL index remove expired items asynchronously, this test would be very slow
$indexes = DB::connection('mongodb')->getCollection('foo_cache_locks')->listIndexes();
$this->assertCount(2, $indexes);
}
private function getCache(): Repository
{
$repository = Cache::driver('mongodb');
$this->assertInstanceOf(Repository::class, $repository);
return $repository;
}
}
================================================
FILE: tests/Casts/BinaryUuidTest.php
================================================
$saveUuid]);
$model = Casting::firstWhere('uuid', $queryUuid);
$this->assertNotNull($model);
$this->assertSame($expectedUuid, $model->uuid);
}
public static function provideBinaryUuidCast(): Generator
{
$uuid = '0c103357-3806-48c9-a84b-867dcb625cfb';
$binaryUuid = new Binary(hex2bin('0c103357380648c9a84b867dcb625cfb'), Binary::TYPE_UUID);
yield 'Save Binary, Query Binary' => [$uuid, $binaryUuid, $binaryUuid];
yield 'Save string, Query Binary' => [$uuid, $uuid, $binaryUuid];
}
public function testQueryByStringDoesNotCast(): void
{
$uuid = '0c103357-3806-48c9-a84b-867dcb625cfb';
Casting::create(['uuid' => $uuid]);
$model = Casting::firstWhere('uuid', $uuid);
$this->assertNull($model);
}
}
================================================
FILE: tests/Casts/BooleanTest.php
================================================
create(['booleanValue' => true]);
self::assertIsBool($model->booleanValue);
self::assertSame(true, $model->booleanValue);
$model->update(['booleanValue' => false]);
self::assertIsBool($model->booleanValue);
self::assertSame(false, $model->booleanValue);
$model->update(['booleanValue' => 1]);
self::assertIsBool($model->booleanValue);
self::assertSame(true, $model->booleanValue);
$model->update(['booleanValue' => 0]);
self::assertIsBool($model->booleanValue);
self::assertSame(false, $model->booleanValue);
}
public function testBoolAsString(): void
{
$model = Casting::query()->create(['booleanValue' => '1.79']);
self::assertIsBool($model->booleanValue);
self::assertSame(true, $model->booleanValue);
$model->update(['booleanValue' => '0']);
self::assertIsBool($model->booleanValue);
self::assertSame(false, $model->booleanValue);
$model->update(['booleanValue' => 'false']);
self::assertIsBool($model->booleanValue);
self::assertSame(true, $model->booleanValue);
$model->update(['booleanValue' => '0.0']);
self::assertIsBool($model->booleanValue);
self::assertSame(true, $model->booleanValue);
$model->update(['booleanValue' => 'true']);
self::assertIsBool($model->booleanValue);
self::assertSame(true, $model->booleanValue);
}
public function testBoolAsNumber(): void
{
$model = Casting::query()->create(['booleanValue' => 1]);
self::assertIsBool($model->booleanValue);
self::assertSame(true, $model->booleanValue);
$model->update(['booleanValue' => 0]);
self::assertIsBool($model->booleanValue);
self::assertSame(false, $model->booleanValue);
$model->update(['booleanValue' => 1.79]);
self::assertIsBool($model->booleanValue);
self::assertSame(true, $model->booleanValue);
$model->update(['booleanValue' => 0.0]);
self::assertIsBool($model->booleanValue);
self::assertSame(false, $model->booleanValue);
}
}
================================================
FILE: tests/Casts/CollectionTest.php
================================================
create(['collectionValue' => ['g' => 'G-Eazy']]);
self::assertInstanceOf(Collection::class, $model->collectionValue);
self::assertIsString($model->getRawOriginal('collectionValue'));
self::assertEquals(collect(['g' => 'G-Eazy']), $model->collectionValue);
$model->update(['collectionValue' => ['Dont let me go' => 'Even the longest of nights turn days']]);
self::assertInstanceOf(Collection::class, $model->collectionValue);
self::assertIsString($model->getRawOriginal('collectionValue'));
self::assertEquals(collect(['Dont let me go' => 'Even the longest of nights turn days']), $model->collectionValue);
$model->update(['collectionValue' => [['Dont let me go' => 'Even the longest of nights turn days']]]);
self::assertInstanceOf(Collection::class, $model->collectionValue);
self::assertIsString($model->getRawOriginal('collectionValue'));
self::assertEquals(collect([['Dont let me go' => 'Even the longest of nights turn days']]), $model->collectionValue);
}
}
================================================
FILE: tests/Casts/DateTest.php
================================================
create(['dateField' => now()]);
self::assertInstanceOf(Carbon::class, $model->dateField);
self::assertEquals(now()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField);
$model->update(['dateField' => now()->subDay()]);
self::assertInstanceOf(Carbon::class, $model->dateField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('dateField'));
self::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField);
$model->update(['dateField' => new DateTime()]);
self::assertInstanceOf(Carbon::class, $model->dateField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('dateField'));
self::assertEquals(now()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField);
$model->update(['dateField' => (new DateTime())->modify('-1 day')]);
self::assertInstanceOf(Carbon::class, $model->dateField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('dateField'));
self::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField);
$refetchedModel = Casting::query()->find($model->getKey());
self::assertInstanceOf(Carbon::class, $refetchedModel->dateField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('dateField'));
self::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $refetchedModel->dateField);
$model = Casting::query()->create();
$this->assertNull($model->dateField);
$model->update(['dateField' => null]);
$this->assertNull($model->dateField);
}
public function testDateAsString(): void
{
$model = Casting::query()->create(['dateField' => '2023-10-29']);
self::assertInstanceOf(Carbon::class, $model->dateField);
self::assertEquals(
Carbon::createFromTimestamp(1698577443)->startOfDay()->format('Y-m-d H:i:s'),
(string) $model->dateField,
);
$model->update(['dateField' => '2023-10-28']);
self::assertInstanceOf(Carbon::class, $model->dateField);
self::assertEquals(
Carbon::createFromTimestamp(1698577443)->subDay()->startOfDay()->format('Y-m-d H:i:s'),
(string) $model->dateField,
);
}
public function testDateWithCustomFormat(): void
{
$model = Casting::query()->create(['dateWithFormatField' => new DateTime()]);
self::assertInstanceOf(Carbon::class, $model->dateWithFormatField);
self::assertEquals(now()->startOfDay()->format('j.n.Y H:i'), (string) $model->dateWithFormatField);
$model->update(['dateWithFormatField' => now()->subDay()]);
self::assertInstanceOf(Carbon::class, $model->dateWithFormatField);
self::assertEquals(now()->startOfDay()->subDay()->format('j.n.Y H:i'), (string) $model->dateWithFormatField);
$model = Casting::query()->create();
$this->assertNull($model->dateWithFormatField);
$model->update(['dateWithFormatField' => null]);
$this->assertNull($model->dateWithFormatField);
}
public function testImmutableDate(): void
{
$model = Casting::query()->create(['immutableDateField' => new DateTime()]);
self::assertInstanceOf(CarbonImmutable::class, $model->immutableDateField);
self::assertEquals(now()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->immutableDateField);
$model->update(['immutableDateField' => now()->subDay()]);
self::assertInstanceOf(CarbonImmutable::class, $model->immutableDateField);
self::assertEquals(now()->startOfDay()->subDay()->format('Y-m-d H:i:s'), (string) $model->immutableDateField);
$model->update(['immutableDateField' => '2023-10-28']);
self::assertInstanceOf(CarbonImmutable::class, $model->immutableDateField);
self::assertEquals(
Carbon::createFromTimestamp(1698577443)->subDay()->startOfDay()->format('Y-m-d H:i:s'),
(string) $model->immutableDateField,
);
$model = Casting::query()->create();
$this->assertNull($model->immutableDateField);
$model->update(['immutableDateField' => null]);
$this->assertNull($model->immutableDateField);
}
public function testImmutableDateWithCustomFormat(): void
{
$model = Casting::query()->create(['immutableDateWithFormatField' => new DateTime()]);
self::assertInstanceOf(CarbonImmutable::class, $model->immutableDateWithFormatField);
self::assertEquals(now()->startOfDay()->format('j.n.Y H:i'), (string) $model->immutableDateWithFormatField);
$model->update(['immutableDateWithFormatField' => now()->startOfDay()->subDay()]);
self::assertInstanceOf(CarbonImmutable::class, $model->immutableDateWithFormatField);
self::assertEquals(now()->startOfDay()->subDay()->format('j.n.Y H:i'), (string) $model->immutableDateWithFormatField);
$model->update(['immutableDateWithFormatField' => '2023-10-28']);
self::assertInstanceOf(CarbonImmutable::class, $model->immutableDateWithFormatField);
self::assertEquals(
Carbon::createFromTimestamp(1698577443)->subDay()->startOfDay()->format('j.n.Y H:i'),
(string) $model->immutableDateWithFormatField,
);
$model = Casting::query()->create();
$this->assertNull($model->immutableDateWithFormatField);
$model->update(['immutableDateWithFormatField' => null]);
$this->assertNull($model->immutableDateWithFormatField);
}
}
================================================
FILE: tests/Casts/DatetimeTest.php
================================================
create(['datetimeField' => now()]);
self::assertInstanceOf(Carbon::class, $model->datetimeField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('datetimeField'));
self::assertEquals(now()->format('Y-m-d H:i:s'), (string) $model->datetimeField);
$model->update(['datetimeField' => now()->subDay()]);
self::assertInstanceOf(Carbon::class, $model->datetimeField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('datetimeField'));
self::assertEquals(now()->subDay()->format('Y-m-d H:i:s'), (string) $model->datetimeField);
$model = Casting::query()->create();
$this->assertNull($model->datetimeField);
$model->update(['datetimeField' => null]);
$this->assertNull($model->datetimeField);
}
public function testDatetimeAsString(): void
{
$model = Casting::query()->create(['datetimeField' => '2023-10-29']);
self::assertInstanceOf(Carbon::class, $model->datetimeField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('datetimeField'));
self::assertEquals(
Carbon::createFromTimestamp(1698577443)->startOfDay()->format('Y-m-d H:i:s'),
(string) $model->datetimeField,
);
$model->update(['datetimeField' => '2023-10-28 11:04:03']);
self::assertInstanceOf(Carbon::class, $model->datetimeField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('datetimeField'));
self::assertEquals(
Carbon::createFromTimestamp(1698577443)->subDay()->format('Y-m-d H:i:s'),
(string) $model->datetimeField,
);
}
public function testDatetimeWithCustomFormat(): void
{
$model = Casting::query()->create(['datetimeWithFormatField' => DateTime::createFromInterface(now())]);
self::assertInstanceOf(Carbon::class, $model->datetimeWithFormatField);
self::assertEquals(now()->format('j.n.Y H:i'), (string) $model->datetimeWithFormatField);
$model->update(['datetimeWithFormatField' => now()->subDay()]);
self::assertInstanceOf(Carbon::class, $model->datetimeWithFormatField);
self::assertEquals(now()->subDay()->format('j.n.Y H:i'), (string) $model->datetimeWithFormatField);
$model = Casting::query()->create();
$this->assertNull($model->datetimeWithFormatField);
$model->update(['datetimeWithFormatField' => null]);
$this->assertNull($model->datetimeWithFormatField);
}
public function testImmutableDatetime(): void
{
$model = Casting::query()->create(['immutableDatetimeField' => DateTime::createFromInterface(now())]);
self::assertInstanceOf(CarbonImmutable::class, $model->immutableDatetimeField);
self::assertEquals(now()->format('Y-m-d H:i:s'), (string) $model->immutableDatetimeField);
$model->update(['immutableDatetimeField' => now()->subDay()]);
self::assertInstanceOf(CarbonImmutable::class, $model->immutableDatetimeField);
self::assertEquals(now()->subDay()->format('Y-m-d H:i:s'), (string) $model->immutableDatetimeField);
$model->update(['immutableDatetimeField' => '2023-10-28 11:04:03']);
self::assertInstanceOf(CarbonImmutable::class, $model->immutableDatetimeField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('immutableDatetimeField'));
self::assertEquals(
Carbon::createFromTimestamp(1698577443)->subDay()->format('Y-m-d H:i:s'),
(string) $model->immutableDatetimeField,
);
$model = Casting::query()->create();
$this->assertNull($model->immutableDatetimeField);
$model->update(['immutableDatetimeField' => null]);
$this->assertNull($model->immutableDatetimeField);
}
public function testImmutableDatetimeWithCustomFormat(): void
{
$model = Casting::query()->create(['immutableDatetimeWithFormatField' => DateTime::createFromInterface(now())]);
self::assertInstanceOf(CarbonImmutable::class, $model->immutableDatetimeWithFormatField);
self::assertEquals(now()->format('j.n.Y H:i'), (string) $model->immutableDatetimeWithFormatField);
$model->update(['immutableDatetimeWithFormatField' => now()->subDay()]);
self::assertInstanceOf(CarbonImmutable::class, $model->immutableDatetimeWithFormatField);
self::assertEquals(now()->subDay()->format('j.n.Y H:i'), (string) $model->immutableDatetimeWithFormatField);
$model->update(['immutableDatetimeWithFormatField' => '2023-10-28 11:04:03']);
self::assertInstanceOf(CarbonImmutable::class, $model->immutableDatetimeWithFormatField);
self::assertEquals(
Carbon::createFromTimestamp(1698577443)->subDay()->format('j.n.Y H:i'),
(string) $model->immutableDatetimeWithFormatField,
);
$model = Casting::query()->create();
$this->assertNull($model->immutableDatetimeWithFormatField);
$model->update(['immutableDatetimeWithFormatField' => null]);
$this->assertNull($model->immutableDatetimeWithFormatField);
}
}
================================================
FILE: tests/Casts/DecimalTest.php
================================================
create(['decimalNumber' => 100.99]);
self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('100.99', $model->decimalNumber);
$model->update(['decimalNumber' => 9999.9]);
self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('9999.90', $model->decimalNumber);
$model->update(['decimalNumber' => 9999.00000009]);
self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('9999.00', $model->decimalNumber);
}
public function testDecimalAsString(): void
{
$model = Casting::query()->create(['decimalNumber' => '120.79']);
self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('120.79', $model->decimalNumber);
$model->update(['decimalNumber' => '795']);
self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('795.00', $model->decimalNumber);
$model->update(['decimalNumber' => '1234.99999999999']);
self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('1235.00', $model->decimalNumber);
}
public function testDecimalAsDecimal128(): void
{
$model = Casting::query()->create(['decimalNumber' => new Decimal128('100.99')]);
self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('100.99', $model->decimalNumber);
$model->update(['decimalNumber' => new Decimal128('9999.9')]);
self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('9999.90', $model->decimalNumber);
}
public function testOtherBSONTypes(): void
{
$modelId = $this->setBSONType(new Int64(100));
$model = Casting::query()->find($modelId);
self::assertIsString($model->decimalNumber);
self::assertIsInt($model->getRawOriginal('decimalNumber'));
self::assertEquals('100.00', $model->decimalNumber);
// Update decimalNumber to a Binary type
$this->setBSONType(new Binary('100.1234', Binary::TYPE_GENERIC), $modelId);
$model->refresh();
self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Binary::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('100.12', $model->decimalNumber);
$this->setBSONType(new Javascript('function() { return 100; }'), $modelId);
$model->refresh();
self::expectException(MathException::class);
self::expectExceptionMessage('Unable to cast value to a decimal.');
$model->decimalNumber;
self::assertInstanceOf(Javascript::class, $model->getRawOriginal('decimalNumber'));
$this->setBSONType(new UTCDateTime(now()), $modelId);
$model->refresh();
self::expectException(MathException::class);
self::expectExceptionMessage('Unable to cast value to a decimal.');
$model->decimalNumber;
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('decimalNumber'));
}
private function setBSONType($value, $id = null)
{
// Do a raw insert/update, so we can enforce the type we want
return Casting::raw(function (Collection $collection) use ($id, $value) {
if (! empty($id)) {
return $collection->updateOne(
['_id' => $id],
['$set' => ['decimalNumber' => $value]],
);
}
return $collection->insertOne(['decimalNumber' => $value])->getInsertedId();
});
}
}
================================================
FILE: tests/Casts/EncryptionTest.php
================================================
make(Encrypter::class)
->decryptString(
$model->getRawOriginal($key),
);
}
public function testEncryptedString(): void
{
$model = Casting::query()->create(['encryptedString' => 'encrypted']);
self::assertIsString($model->encryptedString);
self::assertEquals('encrypted', $model->encryptedString);
self::assertNotEquals('encrypted', $model->getRawOriginal('encryptedString'));
self::assertEquals('encrypted', $this->decryptRaw($model, 'encryptedString'));
$model->update(['encryptedString' => 'updated']);
self::assertIsString($model->encryptedString);
self::assertEquals('updated', $model->encryptedString);
self::assertNotEquals('updated', $model->getRawOriginal('encryptedString'));
self::assertEquals('updated', $this->decryptRaw($model, 'encryptedString'));
}
public function testEncryptedArray(): void
{
$expected = ['foo' => 'bar'];
$model = Casting::query()->create(['encryptedArray' => $expected]);
self::assertIsArray($model->encryptedArray);
self::assertEquals($expected, $model->encryptedArray);
self::assertNotEquals($expected, $model->getRawOriginal('encryptedArray'));
self::assertEquals($expected, Json::decode($this->decryptRaw($model, 'encryptedArray')));
$updated = ['updated' => 'array'];
$model->update(['encryptedArray' => $updated]);
self::assertIsArray($model->encryptedArray);
self::assertEquals($updated, $model->encryptedArray);
self::assertNotEquals($updated, $model->getRawOriginal('encryptedArray'));
self::assertEquals($updated, Json::decode($this->decryptRaw($model, 'encryptedArray')));
}
public function testEncryptedObject(): void
{
$expected = (object) ['foo' => 'bar'];
$model = Casting::query()->create(['encryptedObject' => $expected]);
self::assertIsObject($model->encryptedObject);
self::assertEquals($expected, $model->encryptedObject);
self::assertNotEquals($expected, $model->getRawOriginal('encryptedObject'));
self::assertEquals($expected, Json::decode($this->decryptRaw($model, 'encryptedObject'), false));
$updated = (object) ['updated' => 'object'];
$model->update(['encryptedObject' => $updated]);
self::assertIsObject($model->encryptedObject);
self::assertEquals($updated, $model->encryptedObject);
self::assertNotEquals($updated, $model->getRawOriginal('encryptedObject'));
self::assertEquals($updated, Json::decode($this->decryptRaw($model, 'encryptedObject'), false));
}
public function testEncryptedCollection(): void
{
$expected = collect(['foo' => 'bar']);
$model = Casting::query()->create(['encryptedCollection' => $expected]);
self::assertInstanceOf(Collection::class, $model->encryptedCollection);
self::assertEquals($expected, $model->encryptedCollection);
self::assertNotEquals($expected, $model->getRawOriginal('encryptedCollection'));
self::assertEquals($expected, collect(Json::decode($this->decryptRaw($model, 'encryptedCollection'), false)));
$updated = collect(['updated' => 'object']);
$model->update(['encryptedCollection' => $updated]);
self::assertIsObject($model->encryptedCollection);
self::assertEquals($updated, $model->encryptedCollection);
self::assertNotEquals($updated, $model->getRawOriginal('encryptedCollection'));
self::assertEquals($updated, collect(Json::decode($this->decryptRaw($model, 'encryptedCollection'), false)));
}
}
================================================
FILE: tests/Casts/FloatTest.php
================================================
create(['floatNumber' => 1.79]);
self::assertIsFloat($model->floatNumber);
self::assertEquals(1.79, $model->floatNumber);
$model->update(['floatNumber' => 7E-5]);
self::assertIsFloat($model->floatNumber);
self::assertEquals(7E-5, $model->floatNumber);
}
public function testFloatAsString(): void
{
$model = Casting::query()->create(['floatNumber' => '1.79']);
self::assertIsFloat($model->floatNumber);
self::assertEquals(1.79, $model->floatNumber);
$model->update(['floatNumber' => '7E-5']);
self::assertIsFloat($model->floatNumber);
self::assertEquals(7E-5, $model->floatNumber);
}
}
================================================
FILE: tests/Casts/IntegerTest.php
================================================
create(['intNumber' => 1]);
self::assertIsInt($model->intNumber);
self::assertEquals(1, $model->intNumber);
$model->update(['intNumber' => 2]);
self::assertIsInt($model->intNumber);
self::assertEquals(2, $model->intNumber);
$model->update(['intNumber' => 9.6]);
self::assertIsInt($model->intNumber);
self::assertEquals(9, $model->intNumber);
}
public function testIntAsString(): void
{
$model = Casting::query()->create(['intNumber' => '1']);
self::assertIsInt($model->intNumber);
self::assertEquals(1, $model->intNumber);
$model->update(['intNumber' => '2']);
self::assertIsInt($model->intNumber);
self::assertEquals(2, $model->intNumber);
$model->update(['intNumber' => '9.6']);
self::assertIsInt($model->intNumber);
self::assertEquals(9, $model->intNumber);
}
public function testIntAsFloat(): void
{
$model = Casting::query()->create(['intNumber' => 1.0]);
self::assertIsInt($model->intNumber);
self::assertEquals(1, $model->intNumber);
$model->update(['intNumber' => 2.0]);
self::assertIsInt($model->intNumber);
self::assertEquals(2, $model->intNumber);
$model->update(['intNumber' => 9.6]);
self::assertIsInt($model->intNumber);
self::assertEquals(9, $model->intNumber);
}
}
================================================
FILE: tests/Casts/JsonTest.php
================================================
create(['jsonValue' => ['g' => 'G-Eazy']]);
self::assertIsArray($model->jsonValue);
self::assertEquals(['g' => 'G-Eazy'], $model->jsonValue);
$model->update(['jsonValue' => ['Dont let me go' => 'Even the longest of nights turn days']]);
self::assertIsArray($model->jsonValue);
self::assertIsString($model->getRawOriginal('jsonValue'));
self::assertEquals(['Dont let me go' => 'Even the longest of nights turn days'], $model->jsonValue);
$json = json_encode(['it will encode json' => 'even if it is already json']);
$model->update(['jsonValue' => $json]);
self::assertIsString($model->jsonValue);
self::assertIsString($model->getRawOriginal('jsonValue'));
self::assertEquals($json, $model->jsonValue);
}
}
================================================
FILE: tests/Casts/ObjectIdTest.php
================================================
$saveObjectId]);
$model = CastObjectId::firstWhere('oid', $queryObjectId);
$this->assertNotNull($model);
$this->assertSame($stringObjectId, $model->oid);
}
public static function provideObjectIdCast(): Generator
{
$objectId = new ObjectId();
$stringObjectId = (string) $objectId;
yield 'Save ObjectId, Query ObjectId' => [$objectId, $objectId];
yield 'Save string, Query ObjectId' => [$stringObjectId, $objectId];
}
public function testQueryByStringDoesNotCast(): void
{
$objectId = new ObjectId();
$stringObjectId = (string) $objectId;
CastObjectId::create(['oid' => $objectId]);
$model = CastObjectId::firstWhere('oid', $stringObjectId);
$this->assertNull($model);
}
}
================================================
FILE: tests/Casts/ObjectTest.php
================================================
create(['objectValue' => ['g' => 'G-Eazy']]);
self::assertIsObject($model->objectValue);
self::assertIsString($model->getRawOriginal('objectValue'));
self::assertEquals((object) ['g' => 'G-Eazy'], $model->objectValue);
$model->update(['objectValue' => ['Dont let me go' => 'Even the brightest of colors turn greys']]);
self::assertIsObject($model->objectValue);
self::assertIsString($model->getRawOriginal('objectValue'));
self::assertEquals((object) ['Dont let me go' => 'Even the brightest of colors turn greys'], $model->objectValue);
}
}
================================================
FILE: tests/Casts/StringTest.php
================================================
create(['stringContent' => 'Home is behind The world ahead And there are many paths to tread']);
self::assertIsString($model->stringContent);
self::assertEquals('Home is behind The world ahead And there are many paths to tread', $model->stringContent);
$model->update(['stringContent' => "Losing hope, don't mean I'm hopeless And maybe all I need is time"]);
self::assertIsString($model->stringContent);
self::assertEquals("Losing hope, don't mean I'm hopeless And maybe all I need is time", $model->stringContent);
$now = now();
$model->update(['stringContent' => $now]);
self::assertIsString($model->stringContent);
self::assertEquals((string) $now, $model->stringContent);
}
}
================================================
FILE: tests/ConnectionTest.php
================================================
assertInstanceOf(Connection::class, $connection);
$this->assertSame('mongodb', $connection->getDriverName());
$this->assertSame('MongoDB', $connection->getDriverTitle());
}
public function testReconnect()
{
$c1 = DB::connection('mongodb');
$c2 = DB::connection('mongodb');
$this->assertEquals(spl_object_hash($c1), spl_object_hash($c2));
$c1 = DB::connection('mongodb');
DB::purge('mongodb');
$c2 = DB::connection('mongodb');
$this->assertNotEquals(spl_object_hash($c1), spl_object_hash($c2));
}
public function testDisconnectAndCreateNewConnection()
{
$connection = DB::connection('mongodb');
$this->assertInstanceOf(Connection::class, $connection);
$client = $connection->getClient();
$this->assertInstanceOf(Client::class, $client);
$connection->disconnect();
$client = $connection->getClient();
$this->assertNull($client);
DB::purge('mongodb');
$connection = DB::connection('mongodb');
$this->assertInstanceOf(Connection::class, $connection);
$client = $connection->getClient();
$this->assertInstanceOf(Client::class, $client);
}
public function testDb()
{
$connection = DB::connection('mongodb');
$this->assertInstanceOf(Database::class, $connection->getDatabase());
$this->assertInstanceOf(Client::class, $connection->getClient());
}
public static function dataConnectionConfig(): Generator
{
yield 'Single host' => [
'expectedUri' => 'mongodb://some-host',
'expectedDatabaseName' => 'tests',
'config' => [
'host' => 'some-host',
'database' => 'tests',
],
];
yield 'Host and port' => [
'expectedUri' => 'mongodb://some-host:12345',
'expectedDatabaseName' => 'tests',
'config' => [
'host' => 'some-host',
'port' => 12345,
'database' => 'tests',
],
];
yield 'IPv4' => [
'expectedUri' => 'mongodb://1.2.3.4',
'expectedDatabaseName' => 'tests',
'config' => [
'host' => '1.2.3.4',
'database' => 'tests',
],
];
yield 'IPv4 and port' => [
'expectedUri' => 'mongodb://1.2.3.4:1234',
'expectedDatabaseName' => 'tests',
'config' => [
'host' => '1.2.3.4',
'port' => 1234,
'database' => 'tests',
],
];
yield 'IPv6' => [
'expectedUri' => 'mongodb://[2001:db8:3333:4444:5555:6666:7777:8888]',
'expectedDatabaseName' => 'tests',
'config' => [
'host' => '2001:db8:3333:4444:5555:6666:7777:8888',
'database' => 'tests',
],
];
yield 'IPv6 and port' => [
'expectedUri' => 'mongodb://[2001:db8:3333:4444:5555:6666:7777:8888]:1234',
'expectedDatabaseName' => 'tests',
'config' => [
'host' => '2001:db8:3333:4444:5555:6666:7777:8888',
'port' => 1234,
'database' => 'tests',
],
];
yield 'multiple IPv6' => [
'expectedUri' => 'mongodb://[::1],[2001:db8::1:0:0:1]',
'expectedDatabaseName' => 'tests',
'config' => [
'host' => ['::1', '2001:db8::1:0:0:1'],
'port' => null,
'database' => 'tests',
],
];
yield 'Port in host name takes precedence' => [
'expectedUri' => 'mongodb://some-host:12345',
'expectedDatabaseName' => 'tests',
'config' => [
'host' => 'some-host:12345',
'port' => 54321,
'database' => 'tests',
],
];
yield 'Multiple hosts' => [
'expectedUri' => 'mongodb://host-1,host-2',
'expectedDatabaseName' => 'tests',
'config' => [
'host' => ['host-1', 'host-2'],
'database' => 'tests',
],
];
yield 'Multiple hosts with same port' => [
'expectedUri' => 'mongodb://host-1:12345,host-2:12345',
'expectedDatabaseName' => 'tests',
'config' => [
'host' => ['host-1', 'host-2'],
'port' => 12345,
'database' => 'tests',
],
];
yield 'Multiple hosts with port' => [
'expectedUri' => 'mongodb://host-1:12345,host-2:54321',
'expectedDatabaseName' => 'tests',
'config' => [
'host' => ['host-1:12345', 'host-2:54321'],
'database' => 'tests',
],
];
yield 'DSN takes precedence over host/port config' => [
'expectedUri' => 'mongodb://some-host:12345/auth-database',
'expectedDatabaseName' => 'tests',
'config' => [
'dsn' => 'mongodb://some-host:12345/auth-database',
'host' => 'wrong-host',
'port' => 54321,
'database' => 'tests',
],
];
yield 'Database is extracted from DSN if not specified' => [
'expectedUri' => 'mongodb://some-host:12345/tests',
'expectedDatabaseName' => 'tests',
'config' => ['dsn' => 'mongodb://some-host:12345/tests'],
];
yield 'Database is extracted from DSN with CA path in options' => [
'expectedUri' => 'mongodb://some-host:12345/tests?tls=true&tlsCAFile=/path/to/ca.pem&retryWrites=false',
'expectedDatabaseName' => 'tests',
'config' => ['dsn' => 'mongodb://some-host:12345/tests?tls=true&tlsCAFile=/path/to/ca.pem&retryWrites=false'],
];
}
#[DataProvider('dataConnectionConfig')]
public function testConnectionConfig(string $expectedUri, string $expectedDatabaseName, array $config): void
{
$connection = new Connection($config);
$client = $connection->getClient();
$this->assertSame($expectedUri, (string) $client);
$this->assertSame($expectedDatabaseName, $connection->getDatabase()->getDatabaseName());
$this->assertSame('foo', $connection->getCollection('foo')->getCollectionName());
$this->assertSame('foo', $connection->table('foo')->raw()->getCollectionName());
}
public function testLegacyGetMongoClient(): void
{
$connection = DB::connection('mongodb');
$expected = $connection->getClient();
$this->assertSame($expected, $connection->getMongoClient());
}
public function testLegacyGetMongoDB(): void
{
$connection = DB::connection('mongodb');
$expected = $connection->getDatabase();
$this->assertSame($expected, $connection->getMongoDB());
}
public function testGetDatabase(): void
{
$connection = DB::connection('mongodb');
$defaultName = env('MONGODB_DATABASE', 'unittest');
$database = $connection->getDatabase();
$this->assertInstanceOf(Database::class, $database);
$this->assertSame($defaultName, $database->getDatabaseName());
$this->assertSame($database, $connection->getDatabase($defaultName), 'Same instance for the default database');
}
public function testGetOtherDatabase(): void
{
$connection = DB::connection('mongodb');
$name = 'other_random_database';
$database = $connection->getDatabase($name);
$this->assertInstanceOf(Database::class, $database);
$this->assertSame($name, $database->getDatabaseName($name));
}
public function testConnectionWithoutConfiguredDatabase(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Database is not properly configured.');
new Connection(['dsn' => 'mongodb://some-host']);
}
public function testConnectionWithoutConfiguredDsnOrHost(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('MongoDB connection configuration requires "dsn" or "host" key.');
new Connection(['database' => 'hello']);
}
public function testCollection()
{
$collection = DB::connection('mongodb')->getCollection('unittest');
$this->assertInstanceOf(Collection::class, $collection);
$collection = DB::connection('mongodb')->table('unittests');
$this->assertInstanceOf(Builder::class, $collection);
}
public function testPrefix()
{
$config = [
'dsn' => 'mongodb://127.0.0.1/',
'database' => 'tests',
'prefix' => 'prefix_',
];
$connection = new Connection($config);
$this->assertSame('prefix_foo', $connection->getCollection('foo')->getCollectionName());
$this->assertSame('prefix_foo', $connection->table('foo')->raw()->getCollectionName());
}
public function testQueryLog()
{
DB::enableQueryLog();
$this->assertCount(0, DB::getQueryLog());
DB::table('items')->get();
$this->assertCount(1, $logs = DB::getQueryLog());
$this->assertJsonStringEqualsJsonString('{"find":"items","filter":{}}', $logs[0]['query']);
$this->assertLessThan(10, $logs[0]['time'], 'Query time is in milliseconds');
$this->assertGreaterThan(0.01, $logs[0]['time'], 'Query time is in milliseconds');
DB::table('items')->insert(['id' => $id = new ObjectId(), 'name' => 'test']);
$this->assertCount(2, $logs = DB::getQueryLog());
$this->assertJsonStringEqualsJsonString('{"insert":"items","ordered":true,"documents":[{"name":"test","_id":{"$oid":"' . $id . '"}}]}', $logs[1]['query']);
DB::table('items')->count();
$this->assertCount(3, DB::getQueryLog());
DB::table('items')->where('name', 'test')->update(['name' => 'test']);
$this->assertCount(4, DB::getQueryLog());
DB::table('items')->where('name', 'test')->delete();
$this->assertCount(5, DB::getQueryLog());
// Error
try {
DB::table('items')->where('name', 'test')->update(
['$set' => ['embed' => ['foo' => 'bar']], '$unset' => ['embed' => ['foo']]],
);
self::fail('Expected BulkWriteException');
} catch (BulkWriteException) {
$this->assertCount(6, DB::getQueryLog());
}
}
public function testQueryLogWithMultipleClients()
{
$connection = DB::connection('mongodb');
$this->assertInstanceOf(Connection::class, $connection);
// Create a second connection with the same config as the first
// Make sure to change the name as it's used as a connection identifier
$config = $connection->getConfig();
$config['name'] = 'mongodb2';
$secondConnection = new Connection($config);
$connection->enableQueryLog();
$secondConnection->enableQueryLog();
$this->assertCount(0, $connection->getQueryLog());
$this->assertCount(0, $secondConnection->getQueryLog());
$connection->table('items')->get();
$this->assertCount(1, $connection->getQueryLog());
$this->assertCount(0, $secondConnection->getQueryLog());
$secondConnection->table('items')->get();
$this->assertCount(1, $connection->getQueryLog());
$this->assertCount(1, $secondConnection->getQueryLog());
}
public function testDisableQueryLog()
{
// Disabled by default
DB::table('items')->get();
$this->assertCount(0, DB::getQueryLog());
DB::enableQueryLog();
DB::table('items')->get();
$this->assertCount(1, DB::getQueryLog());
// Enable twice should only log once
DB::enableQueryLog();
DB::table('items')->get();
$this->assertCount(2, DB::getQueryLog());
DB::disableQueryLog();
DB::table('items')->get();
$this->assertCount(2, DB::getQueryLog());
// Disable twice should not log
DB::disableQueryLog();
DB::table('items')->get();
$this->assertCount(2, DB::getQueryLog());
}
public function testSchemaBuilder()
{
$schema = DB::connection('mongodb')->getSchemaBuilder();
$this->assertInstanceOf(SchemaBuilder::class, $schema);
}
public function testDriverName()
{
$driver = DB::connection('mongodb')->getDriverName();
$this->assertEquals('mongodb', $driver);
}
public function testPingMethod()
{
$config = [
'name' => 'mongodb',
'driver' => 'mongodb',
'dsn' => env('MONGODB_URI', 'mongodb://127.0.0.1/'),
'database' => 'unittest',
'options' => [
'connectTimeoutMS' => 1000,
'serverSelectionTimeoutMS' => 6000,
],
];
$instance = new Connection($config);
$instance->ping();
$this->expectException(ConnectionTimeoutException::class);
$this->expectExceptionMessage("No suitable servers found (`serverSelectionTryOnce` set): [Failed to resolve 'wrong-host']");
$config['dsn'] = 'mongodb://wrong-host/';
$instance = new Connection($config);
$instance->ping();
}
public function testServerVersion()
{
$version = DB::connection('mongodb')->getServerVersion();
$this->assertIsString($version);
}
public function testThreadsCount()
{
$threads = DB::connection('mongodb')->threadCount();
$this->assertIsInt($threads);
$this->assertGreaterThanOrEqual(1, $threads);
}
}
================================================
FILE: tests/DateTimeImmutableTest.php
================================================
'John',
'anniversary' => new CarbonImmutable('2020-01-01 00:00:00'),
]);
$anniversary = Anniversary::sole();
assert($anniversary instanceof Anniversary);
self::assertInstanceOf(CarbonImmutable::class, $anniversary->anniversary);
}
}
================================================
FILE: tests/Eloquent/CallBuilderTest.php
================================================
newQuery();
assert($builder instanceof Builder);
self::assertNotInstanceOf(expected: $className, actual: $builder->{$method}(...$parameters));
}
public static function provideFunctionNames(): Generator
{
yield 'does not exist' => ['doesntExist', Builder::class];
yield 'get bindings' => ['getBindings', Builder::class];
yield 'get connection' => ['getConnection', Builder::class];
yield 'get grammar' => ['getGrammar', Builder::class];
yield 'insert get id' => ['insertGetId', Builder::class, [['user' => 'foo']]];
yield 'to Mql' => ['toMql', Builder::class];
yield 'average' => ['average', Builder::class, ['name']];
yield 'avg' => ['avg', Builder::class, ['name']];
yield 'count' => ['count', Builder::class, ['name']];
yield 'exists' => ['exists', Builder::class];
yield 'insert' => ['insert', Builder::class, [['name']]];
yield 'max' => ['max', Builder::class, ['name']];
yield 'min' => ['min', Builder::class, ['name']];
yield 'pluck' => ['pluck', Builder::class, ['name']];
yield 'pull' => ['pull', Builder::class, ['name']];
yield 'push' => ['push', Builder::class, ['name']];
yield 'raw' => ['raw', Builder::class];
yield 'sum' => ['sum', Builder::class, ['name']];
}
#[Test]
#[DataProvider('provideUnsupportedMethods')]
public function callingUnsupportedMethodThrowsAnException(string $method, string $exceptionClass, string $exceptionMessage, $parameters = []): void
{
$builder = User::query()->newQuery();
assert($builder instanceof Builder);
$this->expectException($exceptionClass);
$this->expectExceptionMessage($exceptionMessage);
$builder->{$method}(...$parameters);
}
public static function provideUnsupportedMethods(): Generator
{
yield 'insert or ignore' => [
'insertOrIgnore',
RuntimeException::class,
'This database engine does not support inserting while ignoring errors',
[['name' => 'Jane']],
];
yield 'insert using' => [
'insertUsing',
BadMethodCallException::class,
'This method is not supported by MongoDB. Try "toMql()" instead',
[[['name' => 'Jane']], fn (QueryBuilder $builder) => $builder],
];
yield 'to sql' => [
'toSql',
BadMethodCallException::class,
'This method is not supported by MongoDB. Try "toMql()" instead',
[[['name' => 'Jane']], fn (QueryBuilder $builder) => $builder],
];
}
}
================================================
FILE: tests/Eloquent/MassPrunableTest.php
================================================
assertTrue($this->isPrunable(User::class));
User::insert([
['name' => 'John Doe', 'age' => 35],
['name' => 'Jane Doe', 'age' => 32],
['name' => 'Tomy Doe', 'age' => 11],
]);
$model = new User();
$total = $model->pruneAll();
$this->assertEquals(2, $total);
$this->assertEquals(1, User::count());
}
public function testPruneSoftDelete(): void
{
$this->assertTrue($this->isPrunable(Soft::class));
Soft::insert([
['name' => 'John Doe'],
['name' => 'Jane Doe'],
]);
$model = new Soft();
$total = $model->pruneAll();
$this->assertEquals(2, $total);
$this->assertEquals(0, Soft::count());
$this->assertEquals(0, Soft::withTrashed()->count());
}
/** @see PruneCommand::isPrunable() */
protected function isPrunable($model)
{
$uses = class_uses_recursive($model);
return in_array(Prunable::class, $uses) || in_array(MassPrunable::class, $uses);
}
}
================================================
FILE: tests/Eloquent/ModelTest.php
================================================
assertSame($expected, Model::isDocumentModel($classOrObject));
}
public static function provideDocumentModelClasses(): Generator
{
// Test classes
yield [false, SqlBook::class];
yield [true, Casting::class];
yield [true, Book::class];
// Provided by the Laravel MongoDB package.
yield [true, User::class];
// Instances of objects
yield [false, new SqlBook()];
yield [true, new Book()];
// Anonymous classes
yield [
true,
new class extends Model {
},
];
yield [
true,
new class extends BaseModel {
use DocumentModel;
},
];
yield [
false,
new class {
use DocumentModel;
},
];
yield [
false,
new class extends BaseModel {
},
];
}
}
================================================
FILE: tests/EmbeddedRelationsTest.php
================================================
'John Doe']);
$address = new Address(['city' => 'London']);
$address->setEventDispatcher($events = Mockery::mock(Dispatcher::class));
$events->shouldReceive('dispatch')->with('eloquent.retrieved: ' . $address::class, Mockery::any());
$events->shouldReceive('until')
->once()
->with('eloquent.saving: ' . $address::class, $address)
->andReturn(true);
$events->shouldReceive('until')
->once()
->with('eloquent.creating: ' . $address::class, $address)
->andReturn(true);
$events->shouldReceive('dispatch')->once()->with('eloquent.created: ' . $address::class, $address);
$events->shouldReceive('dispatch')->once()->with('eloquent.saved: ' . $address::class, $address);
$address = $user->addresses()->save($address);
$address->unsetEventDispatcher();
$this->assertNotNull($user->addresses);
$this->assertInstanceOf(Collection::class, $user->addresses);
$this->assertEquals(['London'], $user->addresses->pluck('city')->all());
$this->assertInstanceOf(DateTime::class, $address->created_at);
$this->assertInstanceOf(DateTime::class, $address->updated_at);
$this->assertNotNull($address->id);
$this->assertIsString($address->id);
$raw = $address->getAttributes();
$this->assertInstanceOf(ObjectId::class, $raw['id']);
$address = $user->addresses()->save(new Address(['city' => 'Paris']));
$user = User::find($user->id);
$this->assertEquals(['London', 'Paris'], $user->addresses->pluck('city')->all());
$address->setEventDispatcher($events = Mockery::mock(Dispatcher::class));
$events->shouldReceive('dispatch')->with('eloquent.retrieved: ' . $address::class, Mockery::any());
$events->shouldReceive('until')
->once()
->with('eloquent.saving: ' . $address::class, $address)
->andReturn(true);
$events->shouldReceive('until')
->once()
->with('eloquent.updating: ' . $address::class, $address)
->andReturn(true);
$events->shouldReceive('dispatch')->once()->with('eloquent.updated: ' . $address::class, $address);
$events->shouldReceive('dispatch')->once()->with('eloquent.saved: ' . $address::class, $address);
$address->city = 'New York';
$user->addresses()->save($address);
$address->unsetEventDispatcher();
$this->assertCount(2, $user->addresses);
$this->assertCount(2, $user->addresses()->get());
$this->assertEquals(2, $user->addresses->count());
$this->assertEquals(2, $user->addresses()->count());
$this->assertEquals(['London', 'New York'], $user->addresses->pluck('city')->all());
$freshUser = User::find($user->id);
$this->assertEquals(['London', 'New York'], $freshUser->addresses->pluck('city')->all());
$address = $user->addresses->first();
$this->assertEquals('London', $address->city);
$this->assertInstanceOf(DateTime::class, $address->created_at);
$this->assertInstanceOf(DateTime::class, $address->updated_at);
$this->assertInstanceOf(User::class, $address->user);
$this->assertEmpty($address->relationsToArray()); // prevent infinite loop
$user = User::find($user->id);
$user->addresses()->save(new Address(['city' => 'Bruxelles']));
$this->assertEquals(['London', 'New York', 'Bruxelles'], $user->addresses->pluck('city')->all());
$address = $user->addresses[1];
$address->city = 'Manhattan';
$user->addresses()->save($address);
$this->assertEquals(['London', 'Manhattan', 'Bruxelles'], $user->addresses->pluck('city')->all());
$freshUser = User::find($user->id);
$this->assertEquals(['London', 'Manhattan', 'Bruxelles'], $freshUser->addresses->pluck('city')->all());
}
public function testEmbedsToArray()
{
$user = User::create(['name' => 'John Doe']);
$user->addresses()->saveMany([new Address(['city' => 'London']), new Address(['city' => 'Bristol'])]);
$array = $user->toArray();
$this->assertArrayNotHasKey('_addresses', $array);
$this->assertArrayHasKey('addresses', $array);
}
public function testEmbedsManyAssociate()
{
$user = User::create(['name' => 'John Doe']);
$address = new Address(['city' => 'London']);
$user->addresses()->associate($address);
$this->assertEquals(['London'], $user->addresses->pluck('city')->all());
$this->assertNotNull($address->id);
$freshUser = User::find($user->id);
$this->assertEquals([], $freshUser->addresses->pluck('city')->all());
$address->city = 'Londinium';
$user->addresses()->associate($address);
$this->assertEquals(['Londinium'], $user->addresses->pluck('city')->all());
$freshUser = User::find($user->id);
$this->assertEquals([], $freshUser->addresses->pluck('city')->all());
}
public function testEmbedsManySaveMany()
{
$user = User::create(['name' => 'John Doe']);
$user->addresses()->saveMany([new Address(['city' => 'London']), new Address(['city' => 'Bristol'])]);
$this->assertEquals(['London', 'Bristol'], $user->addresses->pluck('city')->all());
$freshUser = User::find($user->id);
$this->assertEquals(['London', 'Bristol'], $freshUser->addresses->pluck('city')->all());
}
public function testEmbedsManyDuplicate()
{
$user = User::create(['name' => 'John Doe']);
$address = new Address(['city' => 'London']);
$user->addresses()->save($address);
$user->addresses()->save($address);
$this->assertEquals(1, $user->addresses->count());
$this->assertEquals(['London'], $user->addresses->pluck('city')->all());
$user = User::find($user->id);
$this->assertEquals(1, $user->addresses->count());
$address->city = 'Paris';
$user->addresses()->save($address);
$this->assertEquals(1, $user->addresses->count());
$this->assertEquals(['Paris'], $user->addresses->pluck('city')->all());
$user->addresses()->create(['id' => $address->id, 'city' => 'Bruxelles']);
$this->assertEquals(1, $user->addresses->count());
$this->assertEquals(['Bruxelles'], $user->addresses->pluck('city')->all());
}
public function testEmbedsManyCreate()
{
$user = User::create([]);
$address = $user->addresses()->create(['city' => 'Bruxelles']);
$this->assertInstanceOf(Address::class, $address);
$this->assertIsString($address->id);
$this->assertEquals(['Bruxelles'], $user->addresses->pluck('city')->all());
$raw = $address->getAttributes();
$this->assertInstanceOf(ObjectId::class, $raw['id']);
$freshUser = User::find($user->id);
$this->assertEquals(['Bruxelles'], $freshUser->addresses->pluck('city')->all());
$user = User::create([]);
$address = $user->addresses()->create(['id' => '', 'city' => 'Bruxelles']);
$this->assertIsString($address->id);
$raw = $address->getAttributes();
$this->assertInstanceOf(ObjectId::class, $raw['id']);
}
public function testEmbedsManyCreateMany()
{
$user = User::create([]);
[$bruxelles, $paris] = $user->addresses()->createMany([['city' => 'Bruxelles'], ['city' => 'Paris']]);
$this->assertInstanceOf(Address::class, $bruxelles);
$this->assertEquals('Bruxelles', $bruxelles->city);
$this->assertEquals(['Bruxelles', 'Paris'], $user->addresses->pluck('city')->all());
$freshUser = User::find($user->id);
$this->assertEquals(['Bruxelles', 'Paris'], $freshUser->addresses->pluck('city')->all());
}
public function testEmbedsManyDestroy()
{
$user = User::create(['name' => 'John Doe']);
$user->addresses()->saveMany([
new Address(['city' => 'London']),
new Address(['city' => 'Bristol']),
new Address(['city' => 'Bruxelles']),
]);
$address = $user->addresses->first();
$address->setEventDispatcher($events = Mockery::mock(Dispatcher::class));
$events->shouldReceive('dispatch')->with('eloquent.retrieved: ' . $address::class, Mockery::any());
$events->shouldReceive('until')
->once()
->with('eloquent.deleting: ' . $address::class, Mockery::type(Address::class))
->andReturn(true);
$events->shouldReceive('dispatch')
->once()
->with('eloquent.deleted: ' . $address::class, Mockery::type(Address::class));
$user->addresses()->destroy($address->id);
$this->assertEquals(['Bristol', 'Bruxelles'], $user->addresses->pluck('city')->all());
$address->unsetEventDispatcher();
$address = $user->addresses->first();
$user->addresses()->destroy($address);
$this->assertEquals(['Bruxelles'], $user->addresses->pluck('city')->all());
$user->addresses()->create(['city' => 'Paris']);
$user->addresses()->create(['city' => 'San Francisco']);
$freshUser = User::find($user->id);
$this->assertEquals(['Bruxelles', 'Paris', 'San Francisco'], $freshUser->addresses->pluck('city')->all());
$ids = $user->addresses->pluck('id');
$user->addresses()->destroy($ids);
$this->assertEquals([], $user->addresses->pluck('city')->all());
$freshUser = User::find($user->id);
$this->assertEquals([], $freshUser->addresses->pluck('city')->all());
[$london, $bristol, $bruxelles] = $user->addresses()->saveMany([
new Address(['city' => 'London']),
new Address(['city' => 'Bristol']),
new Address(['city' => 'Bruxelles']),
]);
$user->addresses()->destroy([$london, $bruxelles]);
$this->assertEquals(['Bristol'], $user->addresses->pluck('city')->all());
}
public function testEmbedsManyDelete()
{
$user = User::create(['name' => 'John Doe']);
$user->addresses()->saveMany([
new Address(['city' => 'London']),
new Address(['city' => 'Bristol']),
new Address(['city' => 'Bruxelles']),
]);
$address = $user->addresses->first();
$address->setEventDispatcher($events = Mockery::mock(Dispatcher::class));
$events->shouldReceive('dispatch')->with('eloquent.retrieved: ' . $address::class, Mockery::any());
$events->shouldReceive('until')
->once()
->with('eloquent.deleting: ' . $address::class, Mockery::type(Address::class))
->andReturn(true);
$events->shouldReceive('dispatch')
->once()
->with('eloquent.deleted: ' . $address::class, Mockery::type(Address::class));
$address->delete();
$this->assertEquals(2, $user->addresses()->count());
$this->assertEquals(2, $user->addresses->count());
$address->unsetEventDispatcher();
$address = $user->addresses->first();
$address->delete();
$user = User::where('name', 'John Doe')->first();
$this->assertEquals(1, $user->addresses()->count());
$this->assertEquals(1, $user->addresses->count());
}
public function testEmbedsManyDissociate()
{
$user = User::create([]);
$cordoba = $user->addresses()->create(['city' => 'Cordoba']);
$user->addresses()->dissociate($cordoba->id);
$freshUser = User::find($user->id);
$this->assertEquals(0, $user->addresses->count());
$this->assertEquals(1, $freshUser->addresses->count());
$brokenAddress = Address::make(['name' => 'Broken']);
$user->update([
'addresses' => array_merge(
[$brokenAddress->toArray()],
$user->addresses()->toArray(),
),
]);
$curitiba = $user->addresses()->create(['city' => 'Curitiba']);
$user->addresses()->dissociate($curitiba->id);
$this->assertEquals(1, $user->addresses->where('name', $brokenAddress->name)->count());
$this->assertEquals(1, $user->addresses->count());
}
public function testEmbedsManyAliases()
{
$user = User::create(['name' => 'John Doe']);
$address = new Address(['city' => 'London']);
$address = $user->addresses()->attach($address);
$this->assertEquals(['London'], $user->addresses->pluck('city')->all());
$user->addresses()->detach($address);
$this->assertEquals([], $user->addresses->pluck('city')->all());
}
public function testEmbedsManyCreatingEventReturnsFalse()
{
$user = User::create(['name' => 'John Doe']);
$address = new Address(['city' => 'London']);
$address->setEventDispatcher($events = Mockery::mock(Dispatcher::class));
$events->shouldReceive('dispatch')->with('eloquent.retrieved: ' . $address::class, Mockery::any());
$events->shouldReceive('until')
->once()
->with('eloquent.saving: ' . $address::class, $address)
->andReturn(true);
$events->shouldReceive('until')
->once()
->with('eloquent.creating: ' . $address::class, $address)
->andReturn(false);
$this->assertFalse($user->addresses()->save($address));
$address->unsetEventDispatcher();
}
public function testEmbedsManySavingEventReturnsFalse()
{
$user = User::create(['name' => 'John Doe']);
$address = new Address(['city' => 'Paris']);
$address->exists = true;
$address->setEventDispatcher($events = Mockery::mock(Dispatcher::class));
$events->shouldReceive('dispatch')->with('eloquent.retrieved: ' . $address::class, Mockery::any());
$events->shouldReceive('until')
->once()
->with('eloquent.saving: ' . $address::class, $address)
->andReturn(false);
$this->assertFalse($user->addresses()->save($address));
$address->unsetEventDispatcher();
}
public function testEmbedsManyUpdatingEventReturnsFalse()
{
$user = User::create(['name' => 'John Doe']);
$address = new Address(['city' => 'New York']);
$user->addresses()->save($address);
$address->setEventDispatcher($events = Mockery::mock(Dispatcher::class));
$events->shouldReceive('dispatch')->with('eloquent.retrieved: ' . $address::class, Mockery::any());
$events->shouldReceive('until')
->once()
->with('eloquent.saving: ' . $address::class, $address)
->andReturn(true);
$events->shouldReceive('until')
->once()
->with('eloquent.updating: ' . $address::class, $address)
->andReturn(false);
$address->city = 'Warsaw';
$this->assertFalse($user->addresses()->save($address));
$address->unsetEventDispatcher();
}
public function testEmbedsManyDeletingEventReturnsFalse()
{
$user = User::create(['name' => 'John Doe']);
$user->addresses()->save(new Address(['city' => 'New York']));
$address = $user->addresses->first();
$address->setEventDispatcher($events = Mockery::mock(Dispatcher::class));
$events->shouldReceive('dispatch')->with('eloquent.retrieved: ' . $address::class, Mockery::any());
$events->shouldReceive('until')
->once()
->with('eloquent.deleting: ' . $address::class, Mockery::mustBe($address))
->andReturn(false);
$this->assertEquals(0, $user->addresses()->destroy($address));
$this->assertEquals(['New York'], $user->addresses->pluck('city')->all());
$address->unsetEventDispatcher();
}
public function testEmbedsManyFindOrContains()
{
$user = User::create(['name' => 'John Doe']);
$address1 = $user->addresses()->save(new Address(['city' => 'New York']));
$address2 = $user->addresses()->save(new Address(['city' => 'Paris']));
$address = $user->addresses()->find($address1->id);
$this->assertEquals($address->city, $address1->city);
$address = $user->addresses()->find($address2->id);
$this->assertEquals($address->city, $address2->city);
$this->assertTrue($user->addresses()->contains($address2->id));
$this->assertFalse($user->addresses()->contains('123'));
}
public function testEmbedsManyEagerLoading()
{
$user1 = User::create(['name' => 'John Doe']);
$user1->addresses()->save(new Address(['city' => 'New York']));
$user1->addresses()->save(new Address(['city' => 'Paris']));
$user2 = User::create(['name' => 'Jane Doe']);
$user2->addresses()->save(new Address(['city' => 'Berlin']));
$user2->addresses()->save(new Address(['city' => 'Paris']));
$user = User::find($user1->id);
$relations = $user->getRelations();
$this->assertArrayNotHasKey('addresses', $relations);
$this->assertArrayHasKey('addresses', $user->toArray());
$this->assertIsArray($user->toArray()['addresses']);
$user = User::with('addresses')->get()->first();
$relations = $user->getRelations();
$this->assertArrayHasKey('addresses', $relations);
$this->assertEquals(2, $relations['addresses']->count());
$this->assertArrayHasKey('addresses', $user->toArray());
$this->assertIsArray($user->toArray()['addresses']);
}
public function testEmbedsManyDeleteAll()
{
$user1 = User::create(['name' => 'John Doe']);
$user1->addresses()->save(new Address(['city' => 'New York']));
$user1->addresses()->save(new Address(['city' => 'Paris']));
$user2 = User::create(['name' => 'Jane Doe']);
$user2->addresses()->save(new Address(['city' => 'Berlin']));
$user2->addresses()->save(new Address(['city' => 'Paris']));
$user1->addresses()->delete();
$this->assertEquals(0, $user1->addresses()->count());
$this->assertEquals(0, $user1->addresses->count());
$this->assertEquals(2, $user2->addresses()->count());
$this->assertEquals(2, $user2->addresses->count());
$user1 = User::find($user1->id);
$user2 = User::find($user2->id);
$this->assertEquals(0, $user1->addresses()->count());
$this->assertEquals(0, $user1->addresses->count());
$this->assertEquals(2, $user2->addresses()->count());
$this->assertEquals(2, $user2->addresses->count());
}
public function testEmbedsManyCollectionMethods()
{
$user = User::create(['name' => 'John Doe']);
$user->addresses()->save(new Address([
'city' => 'Paris',
'country' => 'France',
'visited' => 4,
'created_at' => new DateTime('3 days ago'),
]));
$user->addresses()->save(new Address([
'city' => 'Bruges',
'country' => 'Belgium',
'visited' => 7,
'created_at' => new DateTime('5 days ago'),
]));
$user->addresses()->save(new Address([
'city' => 'Brussels',
'country' => 'Belgium',
'visited' => 2,
'created_at' => new DateTime('4 days ago'),
]));
$user->addresses()->save(new Address([
'city' => 'Ghent',
'country' => 'Belgium',
'visited' => 13,
'created_at' => new DateTime('2 days ago'),
]));
$this->assertEquals(['Paris', 'Bruges', 'Brussels', 'Ghent'], $user->addresses()->pluck('city')->all());
$this->assertEquals(['Bruges', 'Brussels', 'Ghent', 'Paris'], $user->addresses()
->sortBy('city')
->pluck('city')
->all());
$this->assertEquals([], $user->addresses()->where('city', 'New York')->pluck('city')->all());
$this->assertEquals(['Bruges', 'Brussels', 'Ghent'], $user->addresses()
->where('country', 'Belgium')
->pluck('city')
->all());
$this->assertEquals(['Bruges', 'Brussels', 'Ghent'], $user->addresses()
->where('country', 'Belgium')
->sortBy('city')
->pluck('city')
->all());
$results = $user->addresses->first();
$this->assertInstanceOf(Address::class, $results);
$results = $user->addresses()->where('country', 'Belgium');
$this->assertInstanceOf(Collection::class, $results);
$this->assertEquals(3, $results->count());
$results = $user->addresses()->whereIn('visited', [7, 13]);
$this->assertEquals(2, $results->count());
}
public function testEmbedsOne()
{
$user = User::create(['name' => 'John Doe']);
$father = new User(['name' => 'Mark Doe']);
$father->setEventDispatcher($events = Mockery::mock(Dispatcher::class));
$events->shouldReceive('dispatch')->with('eloquent.retrieved: ' . $father::class, Mockery::any());
$events->shouldReceive('until')
->once()
->with('eloquent.saving: ' . $father::class, $father)
->andReturn(true);
$events->shouldReceive('until')
->once()
->with('eloquent.creating: ' . $father::class, $father)
->andReturn(true);
$events->shouldReceive('dispatch')->once()->with('eloquent.created: ' . $father::class, $father);
$events->shouldReceive('dispatch')->once()->with('eloquent.saved: ' . $father::class, $father);
$father = $user->father()->save($father);
$father->unsetEventDispatcher();
$this->assertNotNull($user->father);
$this->assertEquals('Mark Doe', $user->father->name);
$this->assertInstanceOf(DateTime::class, $father->created_at);
$this->assertInstanceOf(DateTime::class, $father->updated_at);
$this->assertNotNull($father->id);
$this->assertIsString($father->id);
$raw = $father->getAttributes();
$this->assertInstanceOf(ObjectId::class, $raw['id']);
$father->setEventDispatcher($events = Mockery::mock(Dispatcher::class));
$events->shouldReceive('dispatch')->with('eloquent.retrieved: ' . $father::class, Mockery::any());
$events->shouldReceive('until')
->once()
->with('eloquent.saving: ' . $father::class, $father)
->andReturn(true);
$events->shouldReceive('until')
->once()
->with('eloquent.updating: ' . $father::class, $father)
->andReturn(true);
$events->shouldReceive('dispatch')->once()->with('eloquent.updated: ' . $father::class, $father);
$events->shouldReceive('dispatch')->once()->with('eloquent.saved: ' . $father::class, $father);
$father->name = 'Tom Doe';
$user->father()->save($father);
$father->unsetEventDispatcher();
$this->assertNotNull($user->father);
$this->assertEquals('Tom Doe', $user->father->name);
$father = new User(['name' => 'Jim Doe']);
$father->setEventDispatcher($events = Mockery::mock(Dispatcher::class));
$events->shouldReceive('dispatch')->with('eloquent.retrieved: ' . $father::class, Mockery::any());
$events->shouldReceive('until')
->once()
->with('eloquent.saving: ' . $father::class, $father)
->andReturn(true);
$events->shouldReceive('until')
->once()
->with('eloquent.creating: ' . $father::class, $father)
->andReturn(true);
$events->shouldReceive('dispatch')->once()->with('eloquent.created: ' . $father::class, $father);
$events->shouldReceive('dispatch')->once()->with('eloquent.saved: ' . $father::class, $father);
$father = $user->father()->save($father);
$father->unsetEventDispatcher();
$this->assertNotNull($user->father);
$this->assertEquals('Jim Doe', $user->father->name);
}
public function testEmbedsOneAssociate()
{
$user = User::create(['name' => 'John Doe']);
$father = new User(['name' => 'Mark Doe']);
$father->setEventDispatcher($events = Mockery::mock(Dispatcher::class));
$events->shouldReceive('dispatch')->with('eloquent.retrieved: ' . $father::class, Mockery::any());
$events->shouldReceive('until')->times(0)->with('eloquent.saving: ' . $father::class, $father);
$father = $user->father()->associate($father);
$father->unsetEventDispatcher();
$this->assertNotNull($user->father);
$this->assertEquals('Mark Doe', $user->father->name);
}
public function testEmbedsOneNullAssociation()
{
$user = User::create();
$this->assertNull($user->father);
}
public function testEmbedsOneDelete()
{
$user = User::create(['name' => 'John Doe']);
$father = $user->father()->save(new User(['name' => 'Mark Doe']));
$user->father()->delete();
$this->assertNull($user->father);
}
public function testEmbedsOneRefresh()
{
$user = User::create(['name' => 'John Doe']);
$father = new User(['name' => 'Mark Doe']);
$user->father()->associate($father);
$user->save();
$user->refresh();
$this->assertNotNull($user->father);
$this->assertEquals('Mark Doe', $user->father->name);
}
public function testEmbedsOneEmptyRefresh()
{
$user = User::create(['name' => 'John Doe']);
$father = new User(['name' => 'Mark Doe']);
$user->father()->associate($father);
$user->save();
$user->father()->dissociate();
$user->save();
$user->refresh();
$this->assertNull($user->father);
}
public function testEmbedsManyToArray()
{
$user = User::create(['name' => 'John Doe']);
$this->assertInstanceOf(User::class, $user);
$user->addresses()->save(new Address(['city' => 'New York']));
$user->addresses()->save(new Address(['city' => 'Paris']));
$user->addresses()->save(new Address(['city' => 'Brussels']));
$array = $user->toArray();
$this->assertArrayHasKey('addresses', $array);
$this->assertIsArray($array['addresses']);
}
public function testEmbedsManyRefresh()
{
$user = User::create(['name' => 'John Doe']);
$this->assertInstanceOf(User::class, $user);
$user->addresses()->save(new Address(['city' => 'New York']));
$user->addresses()->save(new Address(['city' => 'Paris']));
$user->addresses()->save(new Address(['city' => 'Brussels']));
$user->refresh();
$array = $user->toArray();
$this->assertArrayHasKey('addresses', $array);
$this->assertIsArray($array['addresses']);
}
public function testEmbeddedSave()
{
$user = User::create(['name' => 'John Doe']);
$this->assertInstanceOf(User::class, $user);
$address = $user->addresses()->create(['city' => 'New York']);
$this->assertInstanceOf(Address::class, $address);
$father = $user->father()->create(['name' => 'Mark Doe']);
$address->city = 'Paris';
$address->save();
$father->name = 'Steve Doe';
$father->save();
$this->assertEquals('Paris', $user->addresses->first()->city);
$this->assertEquals('Steve Doe', $user->father->name);
$user = User::where('name', 'John Doe')->first();
$this->assertEquals('Paris', $user->addresses->first()->city);
$this->assertEquals('Steve Doe', $user->father->name);
$address = $user->addresses()->first();
$father = $user->father;
$address->city = 'Ghent';
$address->save();
$father->name = 'Mark Doe';
$father->save();
$this->assertEquals('Ghent', $user->addresses->first()->city);
$this->assertEquals('Mark Doe', $user->father->name);
$user = User::where('name', 'John Doe')->first();
$this->assertEquals('Ghent', $user->addresses->first()->city);
$this->assertEquals('Mark Doe', $user->father->name);
}
public function testNestedEmbedsOne()
{
$user = User::create(['name' => 'John Doe']);
$father = $user->father()->create(['name' => 'Mark Doe']);
$grandfather = $father->father()->create(['name' => 'Steve Doe']);
$greatgrandfather = $grandfather->father()->create(['name' => 'Tom Doe']);
$user->name = 'Tim Doe';
$user->save();
$father->name = 'Sven Doe';
$father->save();
$greatgrandfather->name = 'Ron Doe';
$greatgrandfather->save();
$this->assertEquals('Tim Doe', $user->name);
$this->assertEquals('Sven Doe', $user->father->name);
$this->assertEquals('Steve Doe', $user->father->father->name);
$this->assertEquals('Ron Doe', $user->father->father->father->name);
$user = User::where('name', 'Tim Doe')->first();
$this->assertEquals('Tim Doe', $user->name);
$this->assertEquals('Sven Doe', $user->father->name);
$this->assertEquals('Steve Doe', $user->father->father->name);
$this->assertEquals('Ron Doe', $user->father->father->father->name);
}
public function testNestedEmbedsMany()
{
$user = User::create(['name' => 'John Doe']);
$country1 = $user->addresses()->create(['country' => 'France']);
$country2 = $user->addresses()->create(['country' => 'Belgium']);
$city1 = $country1->addresses()->create(['city' => 'Paris']);
$city2 = $country2->addresses()->create(['city' => 'Ghent']);
$city3 = $country2->addresses()->create(['city' => 'Brussels']);
$city3->city = 'Bruges';
$city3->save();
$this->assertEquals(2, $user->addresses()->count());
$this->assertEquals(1, $user->addresses()->first()->addresses()->count());
$this->assertEquals(2, $user->addresses()->last()->addresses()->count());
$user = User::where('name', 'John Doe')->first();
$this->assertEquals(2, $user->addresses()->count());
$this->assertEquals(1, $user->addresses()->first()->addresses()->count());
$this->assertEquals(2, $user->addresses()->last()->addresses()->count());
}
public function testNestedMixedEmbeds()
{
$user = User::create(['name' => 'John Doe']);
$father = $user->father()->create(['name' => 'Mark Doe']);
$country1 = $father->addresses()->create(['country' => 'France']);
$country2 = $father->addresses()->create(['country' => 'Belgium']);
$country2->country = 'England';
$country2->save();
$father->name = 'Steve Doe';
$father->save();
$this->assertEquals('France', $user->father->addresses()->first()->country);
$this->assertEquals('England', $user->father->addresses()->last()->country);
$this->assertEquals('Steve Doe', $user->father->name);
$user = User::where('name', 'John Doe')->first();
$this->assertEquals('France', $user->father->addresses()->first()->country);
$this->assertEquals('England', $user->father->addresses()->last()->country);
$this->assertEquals('Steve Doe', $user->father->name);
}
public function testNestedEmbedsOneDelete()
{
$user = User::create(['name' => 'John Doe']);
$father = $user->father()->create(['name' => 'Mark Doe']);
$grandfather = $father->father()->create(['name' => 'Steve Doe']);
$greatgrandfather = $grandfather->father()->create(['name' => 'Tom Doe']);
$grandfather->delete();
$this->assertNull($user->father->father);
$user = User::where(['name' => 'John Doe'])->first();
$this->assertNull($user->father->father);
}
public function testNestedEmbedsManyDelete()
{
$user = User::create(['name' => 'John Doe']);
$country = $user->addresses()->create(['country' => 'France']);
$city1 = $country->addresses()->create(['city' => 'Paris']);
$city2 = $country->addresses()->create(['city' => 'Nice']);
$city3 = $country->addresses()->create(['city' => 'Lyon']);
$city2->delete();
$this->assertEquals(2, $user->addresses()->first()->addresses()->count());
$this->assertEquals('Lyon', $country->addresses()->last()->city);
$user = User::where('name', 'John Doe')->first();
$this->assertEquals(2, $user->addresses()->first()->addresses()->count());
$this->assertEquals('Lyon', $country->addresses()->last()->city);
}
public function testNestedMixedEmbedsDelete()
{
$user = User::create(['name' => 'John Doe']);
$father = $user->father()->create(['name' => 'Mark Doe']);
$country1 = $father->addresses()->create(['country' => 'France']);
$country2 = $father->addresses()->create(['country' => 'Belgium']);
$country1->delete();
$this->assertEquals(1, $user->father->addresses()->count());
$this->assertEquals('Belgium', $user->father->addresses()->last()->country);
$user = User::where('name', 'John Doe')->first();
$this->assertEquals(1, $user->father->addresses()->count());
$this->assertEquals('Belgium', $user->father->addresses()->last()->country);
}
public function testDoubleAssociate()
{
$user = User::create(['name' => 'John Doe']);
$address = new Address(['city' => 'Paris']);
$user->addresses()->associate($address);
$user->addresses()->associate($address);
$address = $user->addresses()->first();
$user->addresses()->associate($address);
$this->assertEquals(1, $user->addresses()->count());
$user = User::where('name', 'John Doe')->first();
$user->addresses()->associate($address);
$this->assertEquals(1, $user->addresses()->count());
$user->save();
$user->addresses()->associate($address);
$this->assertEquals(1, $user->addresses()->count());
}
public function testSaveEmptyModel()
{
$user = User::create(['name' => 'John Doe']);
$user->addresses()->save(new Address());
$this->assertNotNull($user->addresses);
$this->assertEquals(1, $user->addresses()->count());
}
public function testIncrementEmbedded()
{
$user = User::create(['name' => 'John Doe']);
$address = $user->addresses()->create(['city' => 'New York', 'visited' => 5]);
$address->increment('visited');
$this->assertEquals(6, $address->visited);
$this->assertEquals(6, $user->addresses()->first()->visited);
$user = User::where('name', 'John Doe')->first();
$this->assertEquals(6, $user->addresses()->first()->visited);
$user = User::where('name', 'John Doe')->first();
$address = $user->addresses()->first();
$address->decrement('visited');
$this->assertEquals(5, $address->visited);
$this->assertEquals(5, $user->addresses()->first()->visited);
$user = User::where('name', 'John Doe')->first();
$this->assertEquals(5, $user->addresses()->first()->visited);
}
public function testPaginateEmbedsMany()
{
$user = User::create(['name' => 'John Doe']);
$user->addresses()->save(new Address(['city' => 'New York']));
$user->addresses()->save(new Address(['city' => 'Paris']));
$user->addresses()->save(new Address(['city' => 'Brussels']));
$results = $user->addresses()->paginate(2);
$this->assertEquals(2, $results->count());
$this->assertEquals(3, $results->total());
// With Closures
$results = $user->addresses()->paginate(fn () => 3, page: 1, total: fn () => 5);
$this->assertEquals(3, $results->count());
$this->assertEquals(5, $results->total());
$this->assertEquals(3, $results->perPage());
}
public function testGetQueueableRelationsEmbedsMany()
{
$user = User::create(['name' => 'John Doe']);
$user->addresses()->save(new Address(['city' => 'New York']));
$user->addresses()->save(new Address(['city' => 'Paris']));
$this->assertEquals(['addresses'], $user->getQueueableRelations());
$this->assertEquals([], $user->addresses->getQueueableRelations());
}
public function testGetQueueableRelationsEmbedsOne()
{
$user = User::create(['name' => 'John Doe']);
$user->father()->save(new User(['name' => 'Mark Doe']));
$this->assertEquals(['father'], $user->getQueueableRelations());
$this->assertEquals([], $user->father->getQueueableRelations());
}
public function testUnsetPropertyOnEmbed()
{
$user = User::create(['name' => 'John Doe']);
$user->addresses()->save(new Address(['city' => 'New York']));
$user->addresses()->save(new Address(['city' => 'Tokyo']));
// Set property
$user->addresses->first()->city = 'Paris';
$user->addresses->first()->save();
$user = User::where('name', 'John Doe')->first();
$this->assertSame('Paris', $user->addresses->get(0)->city);
$this->assertSame('Tokyo', $user->addresses->get(1)->city);
// Unset property
unset($user->addresses->first()->city);
$user->addresses->first()->save();
$user = User::where('name', 'John Doe')->first();
$this->assertNull($user->addresses->get(0)->city);
$this->assertSame('Tokyo', $user->addresses->get(1)->city);
// Unset and reset property
unset($user->addresses->get(1)->city);
$user->addresses->get(1)->city = 'Kyoto';
$user->addresses->get(1)->save();
$user = User::where('name', 'John Doe')->first();
$this->assertNull($user->addresses->get(0)->city);
$this->assertSame('Kyoto', $user->addresses->get(1)->city);
}
}
================================================
FILE: tests/ExternalPackageTest.php
================================================
'Jimmy Doe', 'birthday' => '2012-11-12', 'role' => 'user'],
['name' => 'John Doe', 'birthday' => '1980-07-08', 'role' => 'admin'],
['name' => 'Jane Doe', 'birthday' => '1983-09-10', 'role' => 'admin'],
['name' => 'Jess Doe', 'birthday' => '2014-05-06', 'role' => 'user'],
]);
$request = Request::create('/users', 'GET', ['filter' => ['role' => 'admin'], 'sort' => '-birthday']);
$result = QueryBuilder::for(User::class, $request)
->allowedFilters([
AllowedFilter::exact('role'),
])
->allowedSorts([
AllowedSort::field('birthday'),
])
->get();
$this->assertCount(2, $result);
$this->assertSame('Jane Doe', $result[0]->name);
}
}
================================================
FILE: tests/FilesystemsTest.php
================================================
getBucket()->drop();
parent::tearDown();
}
public static function provideValidOptions(): Generator
{
yield 'connection-minimal' => [
[
'driver' => 'gridfs',
'connection' => 'mongodb',
],
];
yield 'connection-full' => [
[
'driver' => 'gridfs',
'connection' => 'mongodb',
'database' => env('MONGODB_DATABASE', 'unittest'),
'bucket' => 'fs',
'prefix' => 'foo/',
],
];
yield 'bucket-service' => [
[
'driver' => 'gridfs',
'bucket' => 'bucket',
],
];
yield 'bucket-factory' => [
[
'driver' => 'gridfs',
'bucket' => static fn (Application $app) => $app['db']
->connection('mongodb')
->getDatabase()
->selectGridFSBucket(),
],
];
}
#[DataProvider('provideValidOptions')]
public function testValidOptions(array $options)
{
// Service used by "bucket-service"
$this->app->singleton('bucket', static fn (Application $app) => $app['db']
->connection('mongodb')
->getDatabase()
->selectGridFSBucket());
$this->app['config']->set('filesystems.disks.' . $this->dataName(), $options);
$disk = Storage::disk($this->dataName());
$disk->put($filename = $this->dataName() . '.txt', $value = microtime());
$this->assertEquals($value, $disk->get($filename), 'File saved');
}
public static function provideInvalidOptions(): Generator
{
yield 'not-mongodb-connection' => [
['driver' => 'gridfs', 'connection' => 'sqlite'],
'The database connection "sqlite" does not use the "mongodb" driver.',
];
yield 'factory-not-bucket' => [
['driver' => 'gridfs', 'bucket' => static fn () => new stdClass()],
'Unexpected value for GridFS "bucket" configuration. Expecting "MongoDB\GridFS\Bucket". Got "stdClass"',
];
yield 'service-not-bucket' => [
['driver' => 'gridfs', 'bucket' => 'bucket'],
'Unexpected value for GridFS "bucket" configuration. Expecting "MongoDB\GridFS\Bucket". Got "stdClass"',
];
}
#[DataProvider('provideInvalidOptions')]
public function testInvalidOptions(array $options, string $message)
{
// Service used by "service-not-bucket"
$this->app->singleton('bucket', static fn () => new stdClass());
$this->app['config']->set('filesystems.disks.' . $this->dataName(), $options);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage($message);
Storage::disk($this->dataName());
}
public function testReadOnlyAndThrowOptions()
{
$this->app['config']->set('filesystems.disks.gridfs-readonly', [
'driver' => 'gridfs',
'connection' => 'mongodb',
// Use ReadOnlyAdapter
'read-only' => true,
// Throw exceptions
'throw' => true,
]);
$disk = Storage::disk('gridfs-readonly');
$this->expectException(UnableToWriteFile::class);
$this->expectExceptionMessage('This is a readonly adapter.');
$disk->put('file.txt', '');
}
public function testPrefix()
{
$this->app['config']->set('filesystems.disks.gridfs-prefix', [
'driver' => 'gridfs',
'connection' => 'mongodb',
'prefix' => 'foo/bar/',
]);
$disk = Storage::disk('gridfs-prefix');
$disk->put('hello/world.txt', 'Hello World!');
$this->assertSame('Hello World!', $disk->get('hello/world.txt'));
$this->assertSame('Hello World!', stream_get_contents($this->getBucket()->openDownloadStreamByName('foo/bar/hello/world.txt')), 'File name is prefixed in the bucket');
}
private function getBucket(): Bucket
{
return DB::connection('mongodb')->getDatabase()->selectGridFSBucket();
}
}
================================================
FILE: tests/GeospatialTest.php
================================================
geospatial('location', '2dsphere');
});
Location::create([
'name' => 'Picadilly',
'location' => [
'type' => 'LineString',
'coordinates' => [
[
-0.1450383,
51.5069158,
],
[
-0.1367563,
51.5100913,
],
[
-0.1304123,
51.5112908,
],
],
],
]);
Location::create([
'name' => 'StJamesPalace',
'location' => [
'type' => 'Point',
'coordinates' => [
-0.139827,
51.504736,
],
],
]);
}
public function tearDown(): void
{
Schema::drop('locations');
parent::tearDown();
}
public function testGeoWithin()
{
$locations = Location::where('location', 'geoWithin', [
'$geometry' => [
'type' => 'Polygon',
'coordinates' => [
[
[
-0.1450383,
51.5069158,
],
[
-0.1367563,
51.5100913,
],
[
-0.1270247,
51.5013233,
],
[
-0.1460866,
51.4952136,
],
[
-0.1450383,
51.5069158,
],
],
],
],
]);
$this->assertEquals(1, $locations->count());
$locations->get()->each(function ($item, $key) {
$this->assertEquals('StJamesPalace', $item->name);
});
}
public function testGeoIntersects()
{
$locations = Location::where('location', 'geoIntersects', [
'$geometry' => [
'type' => 'LineString',
'coordinates' => [
[
-0.144044,
51.515215,
],
[
-0.1367563,
51.5100913,
],
[
-0.129545,
51.5078646,
],
],
],
]);
$this->assertEquals(1, $locations->count());
$locations->get()->each(function ($item, $key) {
$this->assertEquals('Picadilly', $item->name);
});
}
public function testNear()
{
$locations = Location::where('location', 'near', [
'$geometry' => [
'type' => 'Point',
'coordinates' => [
-0.1367563,
51.5100913,
],
],
'$maxDistance' => 50,
]);
$locations = $locations->get();
$this->assertEquals(1, $locations->count());
$locations->each(function ($item, $key) {
$this->assertEquals('Picadilly', $item->name);
});
}
}
================================================
FILE: tests/HybridRelationsTest.php
================================================
select('SELECT 1');
} catch (PDOException) {
$this->markTestSkipped('SQLite connection is not available.');
}
SqlUser::executeSchema();
SqlBook::executeSchema();
SqlRole::executeSchema();
}
public function tearDown(): void
{
SqlUser::truncate();
SqlBook::truncate();
SqlRole::truncate();
Skill::truncate();
Experience::truncate();
Label::truncate();
parent::tearDown();
}
public function testSqlRelations()
{
$user = new SqlUser();
$this->assertInstanceOf(SqlUser::class, $user);
$this->assertInstanceOf(SQLiteConnection::class, $user->getConnection());
// SQL User
$user->name = 'John Doe';
$user->save();
$this->assertIsInt($user->id);
// SQL has many
$book = new Book(['title' => 'Game of Thrones']);
$user->books()->save($book);
$user = SqlUser::find($user->id); // refetch
$this->assertCount(1, $user->books);
// MongoDB belongs to
$book = $user->books()->first(); // refetch
$this->assertEquals('John Doe', $book->sqlAuthor->name);
// SQL has one
$role = new Role(['type' => 'admin']);
$user->role()->save($role);
$user = SqlUser::find($user->id); // refetch
$this->assertEquals('admin', $user->role->type);
// MongoDB belongs to
$role = $user->role()->first(); // refetch
$this->assertEquals('John Doe', $role->sqlUser->name);
// MongoDB User
$user = new User();
$user->name = 'John Doe';
$user->save();
// MongoDB has many
$book = new SqlBook(['title' => 'Game of Thrones']);
$user->sqlBooks()->save($book);
$user = User::find($user->id); // refetch
$this->assertCount(1, $user->sqlBooks);
// SQL belongs to
$book = $user->sqlBooks()->first(); // refetch
$this->assertEquals('John Doe', $book->author->name);
// MongoDB has one
$role = new SqlRole(['type' => 'admin']);
$user->sqlRole()->save($role);
$user = User::find($user->id); // refetch
$this->assertEquals('admin', $user->sqlRole->type);
// SQL belongs to
$role = $user->sqlRole()->first(); // refetch
$this->assertEquals('John Doe', $role->user->name);
}
public function testHybridWhereHas()
{
$user = new SqlUser();
$otherUser = new SqlUser();
$this->assertInstanceOf(SqlUser::class, $user);
$this->assertInstanceOf(SQLiteConnection::class, $user->getConnection());
$this->assertInstanceOf(SqlUser::class, $otherUser);
$this->assertInstanceOf(SQLiteConnection::class, $otherUser->getConnection());
// SQL User
$user->name = 'John Doe';
$user->id = 2;
$user->save();
// Other user
$otherUser->name = 'Other User';
$otherUser->id = 3;
$otherUser->save();
// Make sure they are created
$this->assertIsInt($user->id);
$this->assertIsInt($otherUser->id);
// Clear to start
$user->books()->truncate();
$otherUser->books()->truncate();
// Create books
$otherUser->books()->saveMany([
new Book(['title' => 'Harry Plants']),
new Book(['title' => 'Harveys']),
]);
// SQL has many
$user->books()->saveMany([
new Book(['title' => 'Game of Thrones']),
new Book(['title' => 'Harry Potter']),
new Book(['title' => 'Harry Planter']),
]);
$users = SqlUser::whereHas('books', function ($query) {
return $query->where('title', 'LIKE', 'Har%');
})->get();
$this->assertEquals(2, $users->count());
$users = SqlUser::whereHas('books', function ($query) {
return $query->where('title', 'LIKE', 'Harry%');
}, '>=', 2)->get();
$this->assertEquals(1, $users->count());
$books = Book::whereHas('sqlAuthor', function ($query) {
return $query->where('name', 'LIKE', 'Other%');
})->get();
$this->assertEquals(2, $books->count());
}
public function testHybridWith()
{
$user = new SqlUser();
$otherUser = new SqlUser();
$this->assertInstanceOf(SqlUser::class, $user);
$this->assertInstanceOf(SQLiteConnection::class, $user->getConnection());
$this->assertInstanceOf(SqlUser::class, $otherUser);
$this->assertInstanceOf(SQLiteConnection::class, $otherUser->getConnection());
// SQL User
$user->name = 'John Doe';
$user->id = 2;
$user->save();
// Other user
$otherUser->name = 'Other User';
$otherUser->id = 3;
$otherUser->save();
// Make sure they are created
$this->assertIsInt($user->id);
$this->assertIsInt($otherUser->id);
// Clear to start
Book::truncate();
SqlBook::truncate();
// Create books
// SQL relation
$user->sqlBooks()->saveMany([
new SqlBook(['title' => 'Game of Thrones']),
new SqlBook(['title' => 'Harry Potter']),
]);
$otherUser->sqlBooks()->saveMany([
new SqlBook(['title' => 'Harry Plants']),
new SqlBook(['title' => 'Harveys']),
new SqlBook(['title' => 'Harry Planter']),
]);
// SQL has many Hybrid
$user->books()->saveMany([
new Book(['title' => 'Game of Thrones']),
new Book(['title' => 'Harry Potter']),
]);
$otherUser->books()->saveMany([
new Book(['title' => 'Harry Plants']),
new Book(['title' => 'Harveys']),
new Book(['title' => 'Harry Planter']),
]);
SqlUser::with('books')->get()
->each(function ($user) {
$this->assertEquals($user->id, $user->books->count());
});
SqlUser::whereHas('sqlBooks', function ($query) {
return $query->where('title', 'LIKE', 'Harry%');
})
->with('books')
->get()
->each(function ($user) {
$this->assertEquals($user->id, $user->books->count());
});
}
public function testHybridBelongsToMany()
{
$user = new SqlUser();
$user2 = new SqlUser();
$this->assertInstanceOf(SqlUser::class, $user);
$this->assertInstanceOf(SQLiteConnection::class, $user->getConnection());
$this->assertInstanceOf(SqlUser::class, $user2);
$this->assertInstanceOf(SQLiteConnection::class, $user2->getConnection());
// Create Mysql Users
$user->fill(['name' => 'John Doe'])->save();
$user = SqlUser::query()->find($user->id);
$user2->fill(['name' => 'Maria Doe'])->save();
$user2 = SqlUser::query()->find($user2->id);
// Create Mongodb Skills
$skill = Skill::query()->create(['name' => 'Laravel']);
$skill2 = Skill::query()->create(['name' => 'MongoDB']);
// sync (pivot is empty)
$skill->sqlUsers()->sync([$user->id, $user2->id]);
$check = Skill::query()->find($skill->id);
$this->assertEquals(2, $check->sqlUsers->count());
// sync (pivot is not empty)
$skill->sqlUsers()->sync($user);
$check = Skill::query()->find($skill->id);
$this->assertEquals(1, $check->sqlUsers->count());
// Inverse sync (pivot is empty)
$user->skills()->sync([$skill->id, $skill2->id]);
$check = SqlUser::find($user->id);
$this->assertEquals(2, $check->skills->count());
// Inverse sync (pivot is not empty)
$user->skills()->sync($skill);
$check = SqlUser::find($user->id);
$this->assertEquals(1, $check->skills->count());
// Inverse attach
$user->skills()->sync([]);
$check = SqlUser::find($user->id);
$this->assertEquals(0, $check->skills->count());
$user->skills()->attach($skill);
$check = SqlUser::find($user->id);
$this->assertEquals(1, $check->skills->count());
}
public function testQueryingHybridBelongsToManyRelationFails()
{
$user = new SqlUser();
$this->assertInstanceOf(SQLiteConnection::class, $user->getConnection());
// Create Mysql Users
$user->fill(['name' => 'John Doe'])->save();
$skill = Skill::query()->create(['name' => 'MongoDB']);
$user->skills()->save($skill);
$this->expectExceptionMessage('BelongsToMany is not supported for hybrid query constraints.');
SqlUser::whereHas('skills', function ($query) {
return $query->where('name', 'LIKE', 'MongoDB');
});
}
public function testHybridMorphToManySqlModelToMongoModel()
{
// SqlModel -> MorphToMany -> MongoModel
$user = new SqlUser();
$user2 = new SqlUser();
$this->assertInstanceOf(SqlUser::class, $user);
$this->assertInstanceOf(SQLiteConnection::class, $user->getConnection());
$this->assertInstanceOf(SqlUser::class, $user2);
$this->assertInstanceOf(SQLiteConnection::class, $user2->getConnection());
// Create Mysql Users
$user->fill(['name' => 'John Doe'])->save();
$user = SqlUser::query()->find($user->id);
$user2->fill(['name' => 'Maria Doe'])->save();
$user2 = SqlUser::query()->find($user2->id);
// Create Mongodb skills
$label = Label::query()->create(['name' => 'Laravel']);
$label2 = Label::query()->create(['name' => 'MongoDB']);
// MorphToMany (pivot is empty)
$user->labels()->sync([$label->id, $label2->id]);
$check = SqlUser::query()->find($user->id);
$this->assertEquals(2, $check->labels->count());
// MorphToMany (pivot is not empty)
$user->labels()->sync($label);
$check = SqlUser::query()->find($user->id);
$this->assertEquals(1, $check->labels->count());
// Attach MorphToMany
$user->labels()->sync([]);
$check = SqlUser::query()->find($user->id);
$this->assertEquals(0, $check->labels->count());
$user->labels()->attach($label);
$user->labels()->attach($label); // ignore duplicates
$check = SqlUser::query()->find($user->id);
$this->assertEquals(1, $check->labels->count());
// Inverse MorphToMany (pivot is empty)
$label->sqlUsers()->sync([$user->id, $user2->id]);
$check = Label::query()->find($label->id);
$this->assertEquals(2, $check->sqlUsers->count());
// Inverse MorphToMany (pivot is empty)
$label->sqlUsers()->sync([$user->id, $user2->id]);
$check = Label::query()->find($label->id);
$this->assertEquals(2, $check->sqlUsers->count());
}
public function testHybridMorphToManyMongoModelToSqlModel()
{
// MongoModel -> MorphToMany -> SqlModel
$user = new SqlUser();
$user2 = new SqlUser();
$this->assertInstanceOf(SqlUser::class, $user);
$this->assertInstanceOf(SQLiteConnection::class, $user->getConnection());
$this->assertInstanceOf(SqlUser::class, $user2);
$this->assertInstanceOf(SQLiteConnection::class, $user2->getConnection());
// Create Mysql Users
$user->fill(['name' => 'John Doe'])->save();
$user = SqlUser::query()->find($user->id);
$user2->fill(['name' => 'Maria Doe'])->save();
$user2 = SqlUser::query()->find($user2->id);
// Create Mongodb experiences
$experience = Experience::query()->create(['title' => 'DB expert']);
$experience2 = Experience::query()->create(['title' => 'MongoDB']);
// MorphToMany (pivot is empty)
$experience->sqlUsers()->sync([$user->id, $user2->id]);
$check = Experience::query()->find($experience->id);
$this->assertEquals(2, $check->sqlUsers->count());
// MorphToMany (pivot is not empty)
$experience->sqlUsers()->sync([$user->id]);
$check = Experience::query()->find($experience->id);
$this->assertEquals(1, $check->sqlUsers->count());
// Inverse MorphToMany (pivot is empty)
$user->experiences()->sync([$experience->id, $experience2->id]);
$check = SqlUser::query()->find($user->id);
$this->assertEquals(2, $check->experiences->count());
// Inverse MorphToMany (pivot is not empty)
$user->experiences()->sync([$experience->id]);
$check = SqlUser::query()->find($user->id);
$this->assertEquals(1, $check->experiences->count());
// Inverse MorphToMany (pivot is not empty)
$user->experiences()->sync([]);
$check = SqlUser::query()->find($user->id);
$this->assertEquals(0, $check->experiences->count());
$user->experiences()->attach($experience);
$user->experiences()->attach($experience); // ignore duplicates
$check = SqlUser::query()->find($user->id);
$this->assertEquals(1, $check->experiences->count());
}
}
================================================
FILE: tests/ModelTest.php
================================================
getCollection('users')->drop();
Soft::truncate();
Book::truncate();
Item::truncate();
Guarded::truncate();
NonIncrementing::truncate();
parent::tearDown();
}
public function testNewModel(): void
{
$user = new User();
$this->assertTrue(Model::isDocumentModel($user));
$this->assertInstanceOf(Connection::class, $user->getConnection());
$this->assertFalse($user->exists);
$this->assertEquals('users', $user->getTable());
$this->assertEquals('id', $user->getKeyName());
}
public function testQualifyColumn(): void
{
// Don't qualify field names in document models
$user = new User();
$this->assertEquals('name', $user->qualifyColumn('name'));
// Qualify column names in hybrid SQL models
$sqlUser = new SqlUser();
$this->assertEquals('users.name', $sqlUser->qualifyColumn('name'));
}
private function makeUser(): User
{
$user = new User();
$user->name = 'John Doe';
$user->title = 'admin';
$user->age = 35;
$user->save();
return $user;
}
public function testInsert(): void
{
$user = $this->makeUser();
$this->assertTrue($user->exists);
$this->assertEquals(1, User::count());
$this->assertTrue(isset($user->id));
$this->assertIsString($user->id);
$this->assertNotEquals('', (string) $user->id);
$this->assertNotEquals(0, strlen((string) $user->id));
$this->assertInstanceOf(Carbon::class, $user->created_at);
$raw = $user->getAttributes();
$this->assertInstanceOf(ObjectID::class, $raw['id']);
$this->assertEquals('John Doe', $user->name);
$this->assertEquals(35, $user->age);
}
public function testInsertNonIncrementable(): void
{
$connection = DB::connection('mongodb');
$connection->setRenameEmbeddedIdField(false);
$nonIncrementing = new NonIncrementing();
$nonIncrementing->id = '12345';
$nonIncrementing->name = 'John Doe';
$nonIncrementing->save();
$this->assertTrue($nonIncrementing->exists);
$this->assertEquals(1, NonIncrementing::count());
$check = NonIncrementing::find($nonIncrementing->id);
$this->assertInstanceOf(NonIncrementing::class, $check);
$this->assertSame('12345', $check->id);
$this->assertEquals('John Doe', $check->name);
}
public function testUpdate(): void
{
$user = $this->makeUser();
$raw = $user->getAttributes();
$this->assertInstanceOf(ObjectID::class, $raw['id']);
$check = User::find($user->id);
$this->assertInstanceOf(User::class, $check);
$check->age = 36;
$check->save();
$this->assertTrue($check->exists);
$this->assertInstanceOf(Carbon::class, $check->created_at);
$this->assertInstanceOf(Carbon::class, $check->updated_at);
$this->assertEquals(1, User::count());
$this->assertEquals('John Doe', $check->name);
$this->assertEquals(36, $check->age);
$user->update(['age' => 20]);
$raw = $user->getAttributes();
$this->assertInstanceOf(ObjectID::class, $raw['id']);
$check = User::find($user->id);
$this->assertEquals(20, $check->age);
$check->age = 24;
$check->fullname = 'Hans Thomas'; // new field
$check->save();
$check = User::find($user->id);
$this->assertEquals(24, $check->age);
$this->assertEquals('Hans Thomas', $check->fullname);
}
public function testUpdateTroughSetUpdatedAt(): void
{
$user = new User();
$user->name = 'John Doe';
$user->title = 'admin';
$user->age = 35;
$user->save();
$updatedAt = Carbon::yesterday();
User::query()->update(['$set' => ['updated_at' => new UTCDateTime($updatedAt)]]);
$user->refresh();
$this->assertEquals($updatedAt, $user->updated_at);
}
public function testUpsert()
{
$result = User::upsert([
['email' => 'foo', 'name' => 'bar'],
['name' => 'bar2', 'email' => 'foo2'],
], ['email']);
$this->assertSame(2, $result);
$this->assertSame(2, $result);
$this->assertSame(2, User::count());
$this->assertSame('bar', User::where('email', 'foo')->first()->name);
// Update 1 document
$result = User::upsert([
['email' => 'foo', 'name' => 'bar2'],
['name' => 'bar2', 'email' => 'foo2'],
], 'email', ['name']);
// Even if the same value is set for the 2nd document, the "updated_at" field is updated
$this->assertSame(2, $result);
$this->assertSame(2, User::count());
$this->assertSame('bar2', User::where('email', 'foo')->first()->name);
// If no update fields are specified, all fields are updated
// Test single document update
$result = User::upsert(['email' => 'foo', 'name' => 'bar3'], 'email');
$this->assertSame(1, $result);
$this->assertSame(2, User::count());
$this->assertSame('bar3', User::where('email', 'foo')->first()->name);
}
public function testManualStringId(): void
{
$user = new User();
$user->id = '4af9f23d8ead0e1d32000000';
$user->name = 'John Doe';
$user->title = 'admin';
$user->age = 35;
$user->save();
$this->assertTrue($user->exists);
$this->assertEquals('4af9f23d8ead0e1d32000000', $user->id);
$raw = $user->getAttributes();
$this->assertInstanceOf(ObjectID::class, $raw['id']);
$user = new User();
$user->id = 'customId';
$user->name = 'John Doe';
$user->title = 'admin';
$user->age = 35;
$user->save();
$this->assertTrue($user->exists);
$this->assertEquals('customId', $user->id);
$raw = $user->getAttributes();
$this->assertIsString($raw['id']);
}
public function testManualIntId(): void
{
$user = new User();
$user->id = 1;
$user->name = 'John Doe';
$user->title = 'admin';
$user->age = 35;
$user->save();
$this->assertTrue($user->exists);
$this->assertEquals(1, $user->id);
$raw = $user->getAttributes();
$this->assertIsInt($raw['id']);
}
public function testDelete(): void
{
$user = $this->makeUser();
$this->assertTrue($user->exists);
$this->assertEquals(1, User::count());
$user->delete();
$this->assertEquals(0, User::count());
}
public function testAll(): void
{
$user = $this->makeUser();
$user = new User();
$user->name = 'Jane Doe';
$user->title = 'user';
$user->age = 32;
$user->save();
$all = User::all();
$this->assertCount(2, $all);
$this->assertContains('John Doe', $all->pluck('name'));
$this->assertContains('Jane Doe', $all->pluck('name'));
}
public function testFind(): void
{
$user = $this->makeUser();
$check = User::find($user->id);
$this->assertInstanceOf(User::class, $check);
$this->assertTrue(Model::isDocumentModel($check));
$this->assertTrue($check->exists);
$this->assertEquals($user->id, $check->id);
$this->assertEquals('John Doe', $check->name);
$this->assertEquals(35, $check->age);
}
public function testInsertEmpty(): void
{
$success = User::insert([]);
$this->assertTrue($success);
}
public function testGet(): void
{
User::insert([
['name' => 'John Doe'],
['name' => 'Jane Doe'],
]);
$users = User::get();
$this->assertCount(2, $users);
$this->assertInstanceOf(EloquentCollection::class, $users);
$this->assertInstanceOf(User::class, $users[0]);
}
public function testFirst(): void
{
User::insert([
['name' => 'John Doe'],
['name' => 'Jane Doe'],
]);
$user = User::first();
$this->assertInstanceOf(User::class, $user);
$this->assertTrue(Model::isDocumentModel($user));
$this->assertEquals('John Doe', $user->name);
}
public function testNoDocument(): void
{
$items = Item::where('name', 'nothing')->get();
$this->assertInstanceOf(EloquentCollection::class, $items);
$this->assertEquals(0, $items->count());
$item = Item::where('name', 'nothing')->first();
$this->assertNull($item);
$item = Item::find('51c33d8981fec6813e00000a');
$this->assertNull($item);
}
public function testFindOrFail(): void
{
$this->expectException(ModelNotFoundException::class);
User::findOrFail('51c33d8981fec6813e00000a');
}
public function testCreate(): void
{
$user = User::create(['name' => 'Jane Poe']);
$this->assertInstanceOf(User::class, $user);
$this->assertTrue(Model::isDocumentModel($user));
$this->assertTrue($user->exists);
$this->assertEquals('Jane Poe', $user->name);
$check = User::where('name', 'Jane Poe')->first();
$this->assertInstanceOf(User::class, $check);
$this->assertEquals($user->id, $check->id);
}
public function testDestroy(): void
{
$user = $this->makeUser();
User::destroy((string) $user->id);
$this->assertEquals(0, User::count());
}
public function testTouch(): void
{
$user = $this->makeUser();
$old = $user->updated_at;
sleep(1);
$user->touch();
$check = User::find($user->id);
$this->assertInstanceOf(User::class, $check);
$this->assertNotEquals($old, $check->updated_at);
}
public function testSoftDelete(): void
{
Soft::create(['name' => 'John Doe']);
Soft::create(['name' => 'Jane Doe']);
$this->assertEquals(2, Soft::count());
$object = Soft::where('name', 'John Doe')->first();
$this->assertInstanceOf(Soft::class, $object);
$this->assertTrue($object->exists);
$this->assertFalse($object->trashed());
$this->assertNull($object->deleted_at);
$object->delete();
$this->assertTrue($object->trashed());
$this->assertNotNull($object->deleted_at);
$object = Soft::where('name', 'John Doe')->first();
$this->assertNull($object);
$this->assertEquals(1, Soft::count());
$this->assertEquals(2, Soft::withTrashed()->count());
$object = Soft::withTrashed()->where('name', 'John Doe')->first();
$this->assertNotNull($object);
$this->assertInstanceOf(Carbon::class, $object->deleted_at);
$this->assertTrue($object->trashed());
$object->restore();
$this->assertEquals(2, Soft::count());
}
/** @param class-string $model */
#[DataProvider('provideId')]
public function testPrimaryKey(string $model, mixed $id, mixed $expected, bool $expectedFound): void
{
$model::truncate();
$expectedType = get_debug_type($expected);
$document = new $model();
$this->assertEquals('id', $document->getKeyName());
$document->id = $id;
$document->save();
$this->assertSame($expectedType, get_debug_type($document->id));
$this->assertEquals($expected, $document->id);
$this->assertSame($expectedType, get_debug_type($document->getKey()));
$this->assertEquals($expected, $document->getKey());
$check = $model::find($id);
if ($expectedFound) {
$this->assertNotNull($check, 'Not found');
$this->assertSame($expectedType, get_debug_type($check->id));
$this->assertEquals($id, $check->id);
$this->assertSame($expectedType, get_debug_type($check->getKey()));
$this->assertEquals($id, $check->getKey());
} else {
$this->assertNull($check, 'Found');
}
}
public static function provideId(): iterable
{
yield 'int' => [
'model' => User::class,
'id' => 10,
'expected' => 10,
// Don't expect this to be found, as the int is cast to string for the query
'expectedFound' => false,
];
yield 'cast as int' => [
'model' => IdIsInt::class,
'id' => 10,
'expected' => 10,
'expectedFound' => true,
];
yield 'string' => [
'model' => User::class,
'id' => 'user-10',
'expected' => 'user-10',
'expectedFound' => true,
];
yield 'cast as string' => [
'model' => IdIsString::class,
'id' => 'user-10',
'expected' => 'user-10',
'expectedFound' => true,
];
$objectId = new ObjectID();
yield 'ObjectID' => [
'model' => User::class,
'id' => $objectId,
'expected' => (string) $objectId,
'expectedFound' => true,
];
$binaryUuid = new Binary(hex2bin('0c103357380648c9a84b867dcb625cfb'), Binary::TYPE_UUID);
yield 'BinaryUuid' => [
'model' => User::class,
'id' => $binaryUuid,
'expected' => (string) $binaryUuid,
'expectedFound' => true,
];
yield 'cast as BinaryUuid' => [
'model' => IdIsBinaryUuid::class,
'id' => $binaryUuid,
'expected' => (string) $binaryUuid,
'expectedFound' => true,
];
$date = new UTCDateTime();
yield 'UTCDateTime' => [
'model' => User::class,
'id' => $date,
'expected' => $date,
// Don't expect this to be found, as the original value is stored as UTCDateTime but then cast to string
'expectedFound' => false,
];
}
public function testCustomPrimaryKey(): void
{
$book = new Book();
$this->assertEquals('title', $book->getKeyName());
$book->title = 'A Game of Thrones';
$book->author = 'George R. R. Martin';
$book->save();
$this->assertEquals('A Game of Thrones', $book->getKey());
$check = Book::find('A Game of Thrones');
$this->assertInstanceOf(Book::class, $check);
$this->assertEquals('title', $check->getKeyName());
$this->assertEquals('A Game of Thrones', $check->getKey());
$this->assertEquals('A Game of Thrones', $check->title);
}
public function testScope(): void
{
Item::insert([
['name' => 'knife', 'type' => 'sharp'],
['name' => 'spoon', 'type' => 'round'],
]);
$sharp = Item::sharp()->get();
$this->assertEquals(1, $sharp->count());
}
public function testToArray(): void
{
$item = Item::create(['name' => 'fork', 'type' => 'sharp']);
$array = $item->toArray();
$keys = array_keys($array);
sort($keys);
$this->assertEquals(['created_at', 'id', 'name', 'type', 'updated_at'], $keys);
$this->assertIsString($array['created_at']);
$this->assertIsString($array['updated_at']);
$this->assertIsString($array['id']);
}
public function testUnset(): void
{
$user1 = User::create(['name' => 'John Doe', 'note1' => 'ABC', 'note2' => 'DEF']);
$user2 = User::create(['name' => 'Jane Doe', 'note1' => 'ABC', 'note2' => 'DEF']);
$user1->unset('note1');
$this->assertFalse(isset($user1->note1));
$this->assertTrue($user1->isDirty());
$user1->save();
$this->assertFalse($user1->isDirty());
$this->assertFalse(isset($user1->note1));
$this->assertTrue(isset($user1->note2));
$this->assertTrue(isset($user2->note1));
$this->assertTrue(isset($user2->note2));
// Re-fetch to be sure
$user1 = User::find($user1->id);
$user2 = User::find($user2->id);
$this->assertFalse(isset($user1->note1));
$this->assertTrue(isset($user1->note2));
$this->assertTrue(isset($user2->note1));
$this->assertTrue(isset($user2->note2));
$user2->unset(['note1', 'note2']);
$user2->save();
$this->assertFalse(isset($user2->note1));
$this->assertFalse(isset($user2->note2));
// Re-re-fetch to be sure
$user2 = User::find($user2->id);
$this->assertFalse(isset($user2->note1));
$this->assertFalse(isset($user2->note2));
}
public function testUnsetRefresh(): void
{
$user = User::create(['name' => 'John Doe', 'note' => 'ABC']);
$user->save();
$user->unset('note');
$this->assertTrue($user->isDirty());
$user->refresh();
$this->assertSame('ABC', $user->note);
$this->assertFalse($user->isDirty());
}
public function testUnsetAndSet(): void
{
$user = User::create(['name' => 'John Doe', 'note1' => 'ABC', 'note2' => 'DEF']);
$this->assertTrue($user->originalIsEquivalent('note1'));
// Unset the value
$user->unset('note1');
$this->assertFalse(isset($user->note1));
$this->assertNull($user['note1']);
$this->assertFalse($user->originalIsEquivalent('note1'));
$this->assertTrue($user->isDirty());
$this->assertSame(['$unset' => ['note1' => true]], $user->getDirty());
// Reset the previous value
$user->note1 = 'ABC';
$this->assertTrue($user->originalIsEquivalent('note1'));
$this->assertFalse($user->isDirty());
$this->assertSame([], $user->getDirty());
// Change the value
$user->note1 = 'GHI';
$this->assertTrue(isset($user->note1));
$this->assertSame('GHI', $user['note1']);
$this->assertFalse($user->originalIsEquivalent('note1'));
$this->assertTrue($user->isDirty());
$this->assertSame(['note1' => 'GHI'], $user->getDirty());
// Fetch to be sure the changes are not persisted yet
$userCheck = User::find($user->id);
$this->assertSame('ABC', $userCheck['note1']);
// Persist the changes
$user->save();
// Re-fetch to be sure
$user = User::find($user->id);
$this->assertTrue(isset($user->note1));
$this->assertSame('GHI', $user->note1);
$this->assertTrue($user->originalIsEquivalent('note1'));
$this->assertFalse($user->isDirty());
}
public function testUnsetDotAttributes(): void
{
$user = User::create(['name' => 'John Doe', 'notes' => ['note1' => 'ABC', 'note2' => 'DEF']]);
$user->unset('notes.note1');
$this->assertFalse(isset($user->notes['note1']));
$this->assertTrue(isset($user->notes['note2']));
$this->assertTrue($user->isDirty());
$dirty = $user->getDirty();
$this->assertArrayHasKey('notes', $dirty);
$this->assertArrayNotHasKey('$unset', $dirty);
$user->save();
$this->assertFalse(isset($user->notes['note1']));
$this->assertTrue(isset($user->notes['note2']));
// Re-fetch to be sure
$user = User::find($user->id);
$this->assertFalse(isset($user->notes['note1']));
$this->assertTrue(isset($user->notes['note2']));
// Unset the parent key
$user->unset('notes');
$this->assertFalse(isset($user->notes['note1']));
$this->assertFalse(isset($user->notes['note2']));
$this->assertFalse(isset($user->notes));
$user->save();
$this->assertFalse(isset($user->notes));
// Re-fetch to be sure
$user = User::find($user->id);
$this->assertFalse(isset($user->notes));
}
public function testUnsetDotAttributesAndSet(): void
{
$user = User::create(['name' => 'John Doe', 'notes' => ['note1' => 'ABC', 'note2' => 'DEF']]);
// notes.note2 is the last attribute of the document
$user->unset('notes.note2');
$this->assertTrue($user->isDirty());
$this->assertSame(['note1' => 'ABC'], $user->notes);
$user->setAttribute('notes.note2', 'DEF');
$this->assertFalse($user->isDirty());
$this->assertSame(['note1' => 'ABC', 'note2' => 'DEF'], $user->notes);
// Unsetting and resetting the 1st attribute of the document will change the order of the attributes
$user->unset('notes.note1');
$this->assertSame(['note2' => 'DEF'], $user->notes);
$this->assertTrue($user->isDirty());
$user->setAttribute('notes.note1', 'ABC');
$this->assertSame(['note2' => 'DEF', 'note1' => 'ABC'], $user->notes);
$this->assertTrue($user->isDirty());
$this->assertSame(['notes' => ['note2' => 'DEF', 'note1' => 'ABC']], $user->getDirty());
$user->save();
$this->assertSame(['note2' => 'DEF', 'note1' => 'ABC'], $user->notes);
// Re-fetch to be sure
$user = User::find($user->id);
$this->assertSame(['note2' => 'DEF', 'note1' => 'ABC'], $user->notes);
}
public function testDateUseLocalTimeZone(): void
{
// The default timezone is reset to UTC before every test in OrchestraTestCase
$tz = 'Australia/Sydney';
date_default_timezone_set($tz);
$date = new DateTime('1965/03/02 15:30:10');
$user = User::create(['birthday' => $date]);
$this->assertInstanceOf(Carbon::class, $user->birthday);
$this->assertEquals($tz, $user->birthday->getTimezone()->getName());
$user->save();
$user = User::find($user->id);
$this->assertEquals($date, $user->birthday);
$this->assertEquals($tz, $user->birthday->getTimezone()->getName());
$this->assertSame('1965-03-02T15:30:10+10:00', $user->birthday->format(DATE_ATOM));
$tz = 'America/New_York';
date_default_timezone_set($tz);
$user = User::find($user->id);
$this->assertEquals($date, $user->birthday);
$this->assertEquals($tz, $user->birthday->getTimezone()->getName());
$this->assertSame('1965-03-02T00:30:10-05:00', $user->birthday->format(DATE_ATOM));
date_default_timezone_set('UTC');
}
public function testDates(): void
{
$user = User::create(['name' => 'John Doe', 'birthday' => new DateTime('1965/1/1')]);
$this->assertInstanceOf(Carbon::class, $user->birthday);
$user = User::where('birthday', '<', new DateTime('1968/1/1'))->first();
$this->assertEquals('John Doe', $user->name);
$user = User::create(['name' => 'John Doe', 'birthday' => new DateTime('1980/1/1')]);
$this->assertInstanceOf(Carbon::class, $user->birthday);
$check = User::find($user->id);
$this->assertInstanceOf(Carbon::class, $check->birthday);
$this->assertEquals($user->birthday, $check->birthday);
$user = User::where('birthday', '>', new DateTime('1975/1/1'))->first();
$this->assertEquals('John Doe', $user->name);
// test custom date format for json output
$json = $user->toArray();
$this->assertEquals($user->birthday->format('l jS \of F Y h:i:s A'), $json['birthday']);
$this->assertEquals($user->created_at->format('l jS \of F Y h:i:s A'), $json['created_at']);
// test created_at
$item = Item::create(['name' => 'sword']);
$this->assertInstanceOf(UTCDateTime::class, $item->getRawOriginal('created_at'));
$this->assertEquals($item->getRawOriginal('created_at')
->toDateTime()
->getTimestamp(), $item->created_at->getTimestamp());
$this->assertLessThan(2, abs(time() - $item->created_at->getTimestamp()));
$item = Item::create(['name' => 'sword']);
$this->assertInstanceOf(Item::class, $item);
$json = $item->toArray();
$this->assertEquals($item->created_at->toISOString(), $json['created_at']);
}
public static function provideDate(): Generator
{
yield 'int timestamp' => [time()];
yield 'Carbon date' => [Date::now()];
yield 'Date in words' => ['Monday 8th August 2005 03:12:46 PM'];
yield 'Date in words before unix epoch' => ['Monday 8th August 1960 03:12:46 PM'];
yield 'Date' => ['2005-08-08'];
yield 'Date before unix epoch' => ['1965-08-08'];
yield 'DateTime date' => [new DateTime('2010-08-08')];
yield 'DateTime date before unix epoch' => [new DateTime('1965-08-08')];
yield 'DateTime date and time' => [new DateTime('2010-08-08 04.08.37')];
yield 'DateTime date and time before unix epoch' => [new DateTime('1965-08-08 04.08.37')];
yield 'DateTime date, time and ms' => [new DateTime('2010-08-08 04.08.37.324')];
yield 'DateTime date, time and ms before unix epoch' => [new DateTime('1965-08-08 04.08.37.324')];
}
#[DataProvider('provideDate')]
public function testDateInputs($date): void
{
// Test with create and standard property
$user = User::create(['name' => 'Jane Doe', 'birthday' => $date]);
$this->assertInstanceOf(User::class, $user);
$this->assertInstanceOf(Carbon::class, $user->birthday);
//Test with setAttribute and standard property
$user->setAttribute('birthday', null);
$this->assertNull($user->birthday);
$user->setAttribute('birthday', $date);
$this->assertInstanceOf(Carbon::class, $user->birthday);
// Test with create and array property
$user = User::create(['name' => 'Jane Doe', 'entry' => ['date' => $date]]);
$this->assertInstanceOf(Carbon::class, $user->getAttribute('entry.date'));
// Test with setAttribute and array property
$user->setAttribute('entry.date', null);
$this->assertNull($user->birthday);
$user->setAttribute('entry.date', $date);
$this->assertInstanceOf(Carbon::class, $user->getAttribute('entry.date'));
// Test with create and array property
$data = $user->toArray();
$this->assertIsString($data['entry']['date']);
}
public function testDateNull(): void
{
$user = User::create(['name' => 'Jane Doe', 'birthday' => null]);
$this->assertNull($user->birthday);
$user->setAttribute('birthday', new DateTime());
$user->setAttribute('birthday', null);
$this->assertNull($user->birthday);
$user->save();
// Re-fetch to be sure
$user = User::find($user->id);
$this->assertNull($user->birthday);
// Nested field with dot notation
$user = User::create(['name' => 'Jane Doe', 'entry' => ['date' => null]]);
$this->assertNull($user->getAttribute('entry.date'));
$user->setAttribute('entry.date', new DateTime());
$user->setAttribute('entry.date', null);
$this->assertNull($user->getAttribute('entry.date'));
// Re-fetch to be sure
$user = User::find($user->id);
$this->assertNull($user->getAttribute('entry.date'));
}
public function testCarbonDateMockingWorks()
{
$fakeDate = Carbon::createFromDate(2000, 01, 01);
Carbon::setTestNow($fakeDate);
$item = Item::create(['name' => 'sword']);
$this->assertLessThan(1, $fakeDate->diffInSeconds($item->created_at));
}
public function testIdAttribute(): void
{
$user = User::create(['name' => 'John Doe']);
$this->assertInstanceOf(User::class, $user);
$this->assertSame($user->id, $user->_id);
$user = User::create(['id' => 'custom_id', 'name' => 'John Doe']);
$this->assertSame($user->id, $user->_id);
}
public function testPushPull(): void
{
$user = User::create(['name' => 'John Doe']);
$this->assertInstanceOf(User::class, $user);
$user->push('tags', 'tag1');
$user->push('tags', ['tag1', 'tag2']);
$user->push('tags', 'tag2', true);
$this->assertEquals(['tag1', 'tag1', 'tag2'], $user->tags);
$user = User::where('id', $user->id)->first();
$this->assertEquals(['tag1', 'tag1', 'tag2'], $user->tags);
$user->pull('tags', 'tag1');
$this->assertEquals(['tag2'], $user->tags);
$user = User::where('id', $user->id)->first();
$this->assertEquals(['tag2'], $user->tags);
$user->push('tags', 'tag3');
$user->pull('tags', ['tag2', 'tag3']);
$this->assertEquals([], $user->tags);
$user = User::where('id', $user->id)->first();
$this->assertEquals([], $user->tags);
}
public function testRaw(): void
{
User::create(['name' => 'John Doe', 'age' => 35]);
User::create(['name' => 'Jane Doe', 'age' => 35]);
User::create(['name' => 'Harry Hoe', 'age' => 15]);
$users = User::raw(function (Collection $collection) {
return $collection->find(['age' => 35]);
});
$this->assertInstanceOf(EloquentCollection::class, $users);
$this->assertInstanceOf(User::class, $users[0]);
$count = User::raw(function (Collection $collection) {
return $collection->estimatedDocumentCount();
});
$this->assertEquals(3, $count);
$result = User::raw(function (Collection $collection) {
return $collection->insertOne(['name' => 'Yvonne Yoe', 'age' => 35]);
});
$this->assertNotNull($result);
}
#[DataProvider('provideTypeMap')]
public function testRawHyradeModel(array $typeMap): void
{
User::insert([
['name' => 'John Doe', 'age' => 35, 'embed' => ['foo' => 'bar'], 'list' => [1, 2, 3]],
['name' => 'Jane Doe', 'age' => 35, 'embed' => ['foo' => 'bar'], 'list' => [1, 2, 3]],
['name' => 'Harry Hoe', 'age' => 15, 'embed' => ['foo' => 'bar'], 'list' => [1, 2, 3]],
]);
// Single document result
$user = User::raw(fn (Collection $collection) => $collection->findOne(
['age' => 35],
[
'projection' => ['_id' => 1, 'name' => 1, 'age' => 1, 'now' => '$$NOW', 'embed' => 1, 'list' => 1],
'typeMap' => $typeMap,
],
));
$this->assertInstanceOf(User::class, $user);
$this->assertArrayNotHasKey('_id', $user->getAttributes());
$this->assertArrayHasKey('id', $user->getAttributes());
$this->assertNotEmpty($user->id);
$this->assertInstanceOf(Carbon::class, $user->now);
$this->assertEquals(['foo' => 'bar'], (array) $user->embed);
$this->assertEquals([1, 2, 3], (array) $user->list);
// Cursor result
$result = User::raw(fn (Collection $collection) => $collection->aggregate([
['$set' => ['now' => '$$NOW']],
['$limit' => 2],
], ['typeMap' => $typeMap]));
$this->assertInstanceOf(EloquentCollection::class, $result);
$this->assertCount(2, $result);
$user = $result->first();
$this->assertInstanceOf(User::class, $user);
$this->assertArrayNotHasKey('_id', $user->getAttributes());
$this->assertArrayHasKey('id', $user->getAttributes());
$this->assertNotEmpty($user->id);
$this->assertInstanceOf(Carbon::class, $user->now);
$this->assertEquals(['foo' => 'bar'], $user->embed);
$this->assertEquals([1, 2, 3], $user->list);
}
public static function provideTypeMap(): Generator
{
yield 'default' => [[]];
yield 'array' => [['root' => 'array', 'document' => 'array', 'array' => 'array']];
yield 'object' => [['root' => 'object', 'document' => 'object', 'array' => 'array']];
yield 'Library BSON' => [['root' => BSONDocument::class, 'document' => BSONDocument::class, 'array' => BSONArray::class]];
yield 'Driver BSON' => [['root' => 'bson', 'document' => 'bson', 'array' => 'bson']];
}
public function testDotNotation(): void
{
$user = User::create([
'name' => 'John Doe',
'address' => [
'city' => 'Paris',
'country' => 'France',
],
]);
$this->assertEquals('Paris', $user->getAttribute('address.city'));
$this->assertEquals('Paris', $user['address.city']);
$this->assertEquals('Paris', $user->{'address.city'});
// Fill
$user->fill(['address.city' => 'Strasbourg']);
$this->assertEquals('Strasbourg', $user['address.city']);
}
public function testAttributeMutator(): void
{
$username = 'JaneDoe';
$usernameSlug = Str::slug($username);
$user = User::create([
'name' => 'Jane Doe',
'username' => $username,
]);
$this->assertNotEquals($username, $user->getAttribute('username'));
$this->assertNotEquals($username, $user['username']);
$this->assertNotEquals($username, $user->username);
$this->assertEquals($usernameSlug, $user->getAttribute('username'));
$this->assertEquals($usernameSlug, $user['username']);
$this->assertEquals($usernameSlug, $user->username);
}
public function testMultipleLevelDotNotation(): void
{
$book = Book::create([
'title' => 'A Game of Thrones',
'chapters' => [
'one' => ['title' => 'The first chapter'],
],
]);
$this->assertInstanceOf(Book::class, $book);
$this->assertEquals(['one' => ['title' => 'The first chapter']], $book->chapters);
$this->assertEquals(['title' => 'The first chapter'], $book['chapters.one']);
$this->assertEquals('The first chapter', $book['chapters.one.title']);
}
public function testGetDirtyDates(): void
{
$user = new User();
$user->setRawAttributes(['name' => 'John Doe', 'birthday' => new DateTime('19 august 1989')], true);
$this->assertEmpty($user->getDirty());
$user->birthday = new DateTime('19 august 1989');
$this->assertEmpty($user->getDirty());
}
public function testChunkById(): void
{
User::create(['name' => 'fork', 'tags' => ['sharp', 'pointy']]);
User::create(['name' => 'spork', 'tags' => ['sharp', 'pointy', 'round', 'bowl']]);
User::create(['name' => 'spoon', 'tags' => ['round', 'bowl']]);
$names = [];
User::chunkById(2, function (EloquentCollection $items) use (&$names) {
$names = array_merge($names, $items->pluck('name')->all());
});
$this->assertEquals(['fork', 'spork', 'spoon'], $names);
}
public function testTruncateModel(): void
{
User::create(['name' => 'John Doe']);
User::truncate();
$this->assertEquals(0, User::count());
}
public function testGuardedModel(): void
{
$model = new Guarded();
// foobar is properly guarded
$model->fill(['foobar' => 'ignored', 'name' => 'John Doe']);
$this->assertFalse(isset($model->foobar));
$this->assertSame('John Doe', $model->name);
// foobar is guarded to any level
$model->fill(['foobar->level2' => 'v2']);
$this->assertNull($model->getAttribute('foobar->level2'));
// multi level statement also guarded
$model->fill(['level1->level2' => 'v1']);
$this->assertNull($model->getAttribute('level1->level2'));
// level1 is still writable
$dataValues = ['array', 'of', 'values'];
$model->fill(['level1' => $dataValues]);
$this->assertEquals($dataValues, $model->getAttribute('level1'));
}
public function testFirstOrCreate(): void
{
$name = 'Jane Poe';
$user = User::where('name', $name)->first();
$this->assertNull($user);
$user = User::firstOrCreate(['name' => $name]);
$this->assertInstanceOf(User::class, $user);
$this->assertTrue(Model::isDocumentModel($user));
$this->assertTrue($user->exists);
$this->assertEquals($name, $user->name);
$check = User::where('name', $name)->first();
$this->assertInstanceOf(User::class, $check);
$this->assertEquals($user->id, $check->id);
}
public function testFirstOrCreateWithValues(): void
{
$name = 'Jane Poe';
$user = User::where('name', $name)->first();
$this->assertNull($user);
$user = User::firstOrCreate(['name' => $name], static fn () => ['age' => 30]);
$this->assertInstanceOf(User::class, $user);
$this->assertTrue(Model::isDocumentModel($user));
$this->assertTrue($user->exists);
$this->assertEquals($name, $user->name);
$check = User::where('name', $name)->first();
$this->assertInstanceOf(User::class, $check);
$this->assertEquals($user->id, $check->id);
$this->assertSame(30, $check->age);
}
public function testEnumCast(): void
{
$name = 'John Member';
$user = new User();
$user->name = $name;
$user->member_status = MemberStatus::Member;
$user->save();
$check = User::where('name', $name)->first();
$this->assertInstanceOf(User::class, $check);
$this->assertSame(MemberStatus::Member->value, $check->getRawOriginal('member_status'));
$this->assertSame(MemberStatus::Member, $check->member_status);
}
public function testNumericFieldName(): void
{
$user = new User();
$user->{1} = 'one';
$user->{2} = ['3' => 'two.three'];
$user->save();
$found = User::where(1, 'one')->first();
$this->assertInstanceOf(User::class, $found);
$this->assertEquals('one', $found[1]);
$found = User::where('2.3', 'two.three')->first();
$this->assertInstanceOf(User::class, $found);
$this->assertEquals([3 => 'two.three'], $found[2]);
}
#[TestWith([true])]
#[TestWith([false])]
public function testCreateOrFirst(bool $transaction)
{
$connection = DB::connection('mongodb');
$connection
->getCollection('users')
->createIndex(['email' => 1], ['unique' => true]);
if ($transaction) {
$connection->beginTransaction();
}
Carbon::setTestNow('2010-06-22');
$createdAt = Carbon::now()->getTimestamp();
$events = [];
self::registerModelEvents(User::class, $events);
$user1 = User::createOrFirst(['email' => 'john.doe@example.com']);
$this->assertSame('john.doe@example.com', $user1->email);
$this->assertNull($user1->name);
$this->assertTrue($user1->wasRecentlyCreated);
$this->assertEquals($createdAt, $user1->created_at->getTimestamp());
$this->assertEquals($createdAt, $user1->updated_at->getTimestamp());
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
Carbon::setTestNow('2020-12-28');
$events = [];
$user2 = User::createOrFirst(
['email' => 'john.doe@example.com'],
['name' => 'John Doe', 'birthday' => new DateTime('1987-05-28')],
);
$this->assertEquals($user1->id, $user2->id);
$this->assertSame('john.doe@example.com', $user2->email);
$this->assertNull($user2->name);
$this->assertNull($user2->birthday);
$this->assertFalse($user2->wasRecentlyCreated);
$this->assertEquals($createdAt, $user1->created_at->getTimestamp());
$this->assertEquals($createdAt, $user1->updated_at->getTimestamp());
if ($transaction) {
// In a transaction, firstOrCreate is used instead.
// Since a document is found, "save" is not called.
$this->assertEquals([], $events);
} else {
// The "duplicate key error" exception interrupts the save process
// before triggering "created" and "saved". Consistent with Laravel
$this->assertEquals(['saving', 'creating'], $events);
}
$events = [];
$user3 = User::createOrFirst(
['email' => 'jane.doe@example.com'],
static fn () => ['name' => 'Jane Doe', 'birthday' => new DateTime('1987-05-28')],
);
$this->assertNotEquals($user3->id, $user1->id);
$this->assertSame('jane.doe@example.com', $user3->email);
$this->assertSame('Jane Doe', $user3->name);
$this->assertEquals(new DateTime('1987-05-28'), $user3->birthday);
$this->assertTrue($user3->wasRecentlyCreated);
$this->assertEquals($createdAt, $user1->created_at->getTimestamp());
$this->assertEquals($createdAt, $user1->updated_at->getTimestamp());
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
$events = [];
$user4 = User::createOrFirst(
['name' => 'Robert Doe'],
['name' => 'Maria Doe', 'email' => 'maria.doe@example.com'],
);
$this->assertSame('Maria Doe', $user4->name);
$this->assertTrue($user4->wasRecentlyCreated);
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
if ($transaction) {
$connection->commit();
}
}
#[TestWith([['_id' => new ObjectID()]])]
#[TestWith([['id' => new ObjectID()]])]
#[TestWith([['foo' => 'bar']])]
public function testUpdateOrCreate(array $criteria)
{
// Insert data to ensure we filter on the correct criteria, and not getting
// the first document randomly.
User::insert([
['email' => 'fixture@example.com'],
['email' => 'john.doe@example.com'],
]);
Carbon::setTestNow('2010-01-01');
$createdAt = Carbon::now()->getTimestamp();
$events = [];
self::registerModelEvents(User::class, $events);
// Create
$user = User::updateOrCreate(
$criteria,
['email' => 'john.doe@example.com', 'birthday' => new DateTime('1987-05-28')],
);
$this->assertInstanceOf(User::class, $user);
$this->assertEquals('john.doe@example.com', $user->email);
$this->assertEquals(new DateTime('1987-05-28'), $user->birthday);
$this->assertEquals($createdAt, $user->created_at->getTimestamp());
$this->assertEquals($createdAt, $user->updated_at->getTimestamp());
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
Carbon::setTestNow('2010-02-01');
$updatedAt = Carbon::now()->getTimestamp();
// Update
$events = [];
$user = User::updateOrCreate(
$criteria,
['birthday' => new DateTime('1990-01-12'), 'foo' => 'bar'],
);
$this->assertInstanceOf(User::class, $user);
$this->assertEquals('john.doe@example.com', $user->email);
$this->assertEquals(new DateTime('1990-01-12'), $user->birthday);
$this->assertEquals($createdAt, $user->created_at->getTimestamp());
$this->assertEquals($updatedAt, $user->updated_at->getTimestamp());
$this->assertEquals(['saving', 'updating', 'updated', 'saved'], $events);
// Stored data
$checkUser = User::where($criteria)->first();
$this->assertInstanceOf(User::class, $checkUser);
$this->assertEquals('john.doe@example.com', $checkUser->email);
$this->assertEquals(new DateTime('1990-01-12'), $checkUser->birthday);
$this->assertEquals($createdAt, $checkUser->created_at->getTimestamp());
$this->assertEquals($updatedAt, $checkUser->updated_at->getTimestamp());
}
#[TestWith(['_id'])]
#[TestWith(['id'])]
public function testCreateWithNullId(string $id)
{
User::truncate();
$user = User::create([$id => null, 'email' => 'foo@bar']);
$this->assertIsString($user->id);
$this->assertEquals([['id' => $user->id, 'email' => 'foo@bar']], User::all(['id', 'email'])->toArray());
}
/** @param class-string $modelClass */
private static function registerModelEvents(string $modelClass, array &$events): void
{
$modelClass::creating(function () use (&$events) {
$events[] = 'creating';
});
$modelClass::created(function () use (&$events) {
$events[] = 'created';
});
$modelClass::updating(function () use (&$events) {
$events[] = 'updating';
});
$modelClass::updated(function () use (&$events) {
$events[] = 'updated';
});
$modelClass::saving(function () use (&$events) {
$events[] = 'saving';
});
$modelClass::saved(function () use (&$events) {
$events[] = 'saved';
});
}
}
================================================
FILE: tests/Models/Address.php
================================================
embedsMany(self::class);
}
}
================================================
FILE: tests/Models/Anniversary.php
================================================
'immutable_datetime'];
}
================================================
FILE: tests/Models/Birthday.php
================================================
'datetime'];
}
================================================
FILE: tests/Models/Book.php
================================================
belongsTo(User::class, 'author_id');
}
public function sqlAuthor(): BelongsTo
{
return $this->belongsTo(SqlUser::class, 'author_id');
}
}
================================================
FILE: tests/Models/CastObjectId.php
================================================
ObjectId::class,
];
}
================================================
FILE: tests/Models/Casting.php
================================================
BinaryUuid::class,
'intNumber' => 'int',
'floatNumber' => 'float',
'decimalNumber' => 'decimal:2',
'stringContent' => 'string',
'booleanValue' => 'boolean',
'objectValue' => 'object',
'jsonValue' => 'json',
'collectionValue' => 'collection',
'dateField' => 'date',
'dateWithFormatField' => 'date:j.n.Y H:i',
'immutableDateField' => 'immutable_date',
'immutableDateWithFormatField' => 'immutable_date:j.n.Y H:i',
'datetimeField' => 'datetime',
'datetimeWithFormatField' => 'datetime:j.n.Y H:i',
'immutableDatetimeField' => 'immutable_datetime',
'immutableDatetimeWithFormatField' => 'immutable_datetime:j.n.Y H:i',
'encryptedString' => 'encrypted',
'encryptedArray' => 'encrypted:array',
'encryptedObject' => 'encrypted:object',
'encryptedCollection' => 'encrypted:collection',
];
}
================================================
FILE: tests/Models/Client.php
================================================
belongsToMany(User::class);
}
public function skillsWithCustomKeys()
{
return $this->belongsToMany(
Skill::class,
foreignPivotKey: 'cclient_ids',
relatedPivotKey: 'cskill_ids',
parentKey: 'cclient_id',
relatedKey: 'cskill_id',
);
}
public function photo(): MorphOne
{
return $this->morphOne(Photo::class, 'has_image');
}
public function addresses(): HasMany
{
return $this->hasMany(Address::class, 'data.client_id', 'data.client_id');
}
public function labels()
{
return $this->morphToMany(Label::class, 'labelled');
}
public function labelsWithCustomKeys()
{
return $this->morphToMany(
Label::class,
'clabelled',
'clabelleds',
'cclabelled_id',
'clabel_ids',
'cclient_id',
'clabel_id',
);
}
}
================================================
FILE: tests/Models/Experience.php
================================================
'int'];
public function sqlUsers(): MorphToMany
{
return $this->morphToMany(SqlUser::class, 'experienced');
}
}
================================================
FILE: tests/Models/Group.php
================================================
belongsToMany(User::class, 'users', 'groups', 'users', 'id', 'id', 'users');
}
}
================================================
FILE: tests/Models/Guarded.php
================================================
level2'];
}
================================================
FILE: tests/Models/HiddenAnimal.php
================================================
BinaryUuid::class,
];
}
================================================
FILE: tests/Models/IdIsInt.php
================================================
'int'];
}
================================================
FILE: tests/Models/IdIsString.php
================================================
'string'];
}
================================================
FILE: tests/Models/Item.php
================================================
belongsTo(User::class);
}
public function scopeSharp(Builder $query)
{
return $query->where('type', 'sharp');
}
}
================================================
FILE: tests/Models/Label.php
================================================
morphedByMany(User::class, 'labelled');
}
public function sqlUsers(): MorphToMany
{
return $this->morphedByMany(SqlUser::class, 'labeled');
}
public function clients()
{
return $this->morphedByMany(Client::class, 'labelled');
}
public function clientsWithCustomKeys()
{
return $this->morphedByMany(
Client::class,
'clabelled',
'clabelleds',
'clabel_ids',
'cclabelled_id',
'clabel_id',
'cclient_id',
);
}
}
================================================
FILE: tests/Models/Location.php
================================================
morphTo();
}
public function hasImageWithCustomOwnerKey(): MorphTo
{
return $this->morphTo(ownerKey: 'cclient_id');
}
}
================================================
FILE: tests/Models/Role.php
================================================
belongsTo(User::class);
}
public function sqlUser(): BelongsTo
{
return $this->belongsTo(SqlUser::class);
}
}
================================================
FILE: tests/Models/SchemaVersion.php
================================================
age = 35;
}
}
}
================================================
FILE: tests/Models/Scoped.php
================================================
where('favorite', true);
});
}
}
================================================
FILE: tests/Models/Skill.php
================================================
belongsToMany(SqlUser::class);
}
}
================================================
FILE: tests/Models/Soft.php
================================================
'datetime'];
public function prunable(): Builder
{
return $this->newQuery();
}
public function user()
{
return $this->belongsTo(User::class);
}
}
================================================
FILE: tests/Models/SqlBook.php
================================================
belongsTo(User::class, 'author_id');
}
/**
* Check if we need to run the schema.
*/
public static function executeSchema(): void
{
$schema = Schema::connection('sqlite');
assert($schema instanceof SQLiteBuilder);
$schema->dropIfExists('books');
$schema->create('books', function (Blueprint $table) {
$table->string('title');
$table->string('author_id')->nullable();
$table->integer('sql_user_id')->unsigned()->nullable();
$table->timestamps();
});
}
}
================================================
FILE: tests/Models/SqlRole.php
================================================
belongsTo(User::class);
}
public function sqlUser(): BelongsTo
{
return $this->belongsTo(SqlUser::class);
}
/**
* Check if we need to run the schema.
*/
public static function executeSchema()
{
$schema = Schema::connection('sqlite');
assert($schema instanceof SQLiteBuilder);
$schema->dropIfExists('roles');
$schema->create('roles', function (Blueprint $table) {
$table->string('type');
$table->string('user_id');
$table->timestamps();
});
}
}
================================================
FILE: tests/Models/SqlUser.php
================================================
hasMany(Book::class, 'author_id');
}
public function role(): HasOne
{
return $this->hasOne(Role::class);
}
public function skills(): BelongsToMany
{
return $this->belongsToMany(Skill::class, relatedPivotKey: 'skills');
}
public function sqlBooks(): HasMany
{
return $this->hasMany(SqlBook::class);
}
public function labels(): MorphToMany
{
return $this->morphToMany(Label::class, 'labeled');
}
public function experiences(): MorphToMany
{
return $this->morphedByMany(Experience::class, 'experienced');
}
/**
* Check if we need to run the schema.
*/
public static function executeSchema(): void
{
$schema = Schema::connection('sqlite');
assert($schema instanceof SQLiteBuilder);
$schema->dropIfExists('users');
$schema->create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->timestamps();
});
// Pivot table for BelongsToMany relationship with Skill
if (! $schema->hasTable('skill_sql_user')) {
$schema->create('skill_sql_user', function (Blueprint $table) {
$table->foreignIdFor(self::class)->constrained()->cascadeOnDelete();
$table->string((new Skill())->getForeignKey());
$table->primary([(new self())->getForeignKey(), (new Skill())->getForeignKey()]);
});
}
// Pivot table for MorphToMany relationship with Label
if (! $schema->hasTable('labeleds')) {
$schema->create('labeleds', function (Blueprint $table) {
$table->foreignIdFor(self::class)->constrained()->cascadeOnDelete();
$table->morphs('labeled');
});
}
// Pivot table for MorphedByMany relationship with Experience
if (! $schema->hasTable('experienceds')) {
$schema->create('experienceds', function (Blueprint $table) {
$table->foreignIdFor(self::class)->constrained()->cascadeOnDelete();
$table->morphs('experienced');
});
}
}
}
================================================
FILE: tests/Models/User.php
================================================
'datetime',
'entry.date' => 'datetime',
'member_status' => MemberStatus::class,
];
protected $fillable = [
'name',
'email',
'title',
'age',
'birthday',
'username',
'member_status',
];
protected static $unguarded = true;
public function books()
{
return $this->hasMany(Book::class, 'author_id');
}
public function softs()
{
return $this->hasMany(Soft::class);
}
public function softsWithTrashed()
{
return $this->hasMany(Soft::class)->withTrashed();
}
public function sqlBooks()
{
return $this->hasMany(SqlBook::class, 'author_id');
}
public function items()
{
return $this->hasMany(Item::class);
}
public function role()
{
return $this->hasOne(Role::class);
}
public function sqlRole()
{
return $this->hasOne(SqlRole::class);
}
public function clients()
{
return $this->belongsToMany(Client::class);
}
public function groups()
{
return $this->belongsToMany(Group::class, 'groups', 'users', 'groups', 'id', 'id', 'groups');
}
public function photos()
{
return $this->morphMany(Photo::class, 'has_image');
}
public function labels()
{
return $this->morphToMany(Label::class, 'labelled');
}
public function addresses()
{
return $this->embedsMany(Address::class);
}
public function father()
{
return $this->embedsOne(self::class);
}
protected function serializeDate(DateTimeInterface $date)
{
return $date->format('l jS \of F Y h:i:s A');
}
protected function username(): Attribute
{
return Attribute::make(
get: fn ($value) => $value,
set: fn ($value) => Str::slug($value),
);
}
public function prunable(): Builder
{
return $this->where('age', '>', 18);
}
}
================================================
FILE: tests/PHPStan/SarifErrorFormatter.php
================================================
[
'uri' => 'file://' . $this->currentWorkingDirectory . '/',
],
];
$results = [];
$rules = [];
foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) {
$ruleId = $fileSpecificError->getIdentifier();
$rules[$ruleId] = ['id' => $ruleId];
$result = [
'ruleId' => $ruleId,
'level' => 'error',
'message' => [
'text' => $fileSpecificError->getMessage(),
],
'locations' => [
[
'physicalLocation' => [
'artifactLocation' => [
'uri' => $this->relativePathHelper->getRelativePath($fileSpecificError->getFile()),
'uriBaseId' => self::URI_BASE_ID,
],
],
],
],
'properties' => [
'ignorable' => $fileSpecificError->canBeIgnored(),
],
];
if ($fileSpecificError->getTip() !== null) {
$result['properties']['tip'] = $fileSpecificError->getTip();
}
if ($fileSpecificError->getLine() !== null) {
$result['locations'][0]['physicalLocation']['region']['startLine'] = $fileSpecificError->getLine();
}
$results[] = $result;
}
foreach ($analysisResult->getNotFileSpecificErrors() as $notFileSpecificError) {
$results[] = [
'level' => 'error',
'message' => ['text' => $notFileSpecificError],
];
}
foreach ($analysisResult->getWarnings() as $warning) {
$results[] = [
'level' => 'warning',
'message' => ['text' => $warning],
];
}
$sarif = [
'$schema' => 'https://json.schemastore.org/sarif-2.1.0.json',
'version' => '2.1.0',
'runs' => [
[
'tool' => [
'driver' => [
'name' => 'PHPStan',
'fullName' => 'PHP Static Analysis Tool',
'informationUri' => 'https://phpstan.org',
'version' => $phpstanVersion,
'semanticVersion' => $phpstanVersion,
'rules' => array_values($rules),
],
],
'originalUriBaseIds' => $originalUriBaseIds,
'results' => $results,
],
],
];
$json = Json::encode($sarif, $this->pretty ? Json::PRETTY : 0);
$output->writeRaw($json);
return $analysisResult->hasErrors() ? 1 : 0;
}
}
================================================
FILE: tests/PropertyTest.php
================================================
'Sheep',
'country' => 'Ireland',
'can_be_eaten' => true,
]);
$hiddenAnimal = HiddenAnimal::sole();
assert($hiddenAnimal instanceof HiddenAnimal);
self::assertSame('Ireland', $hiddenAnimal->country);
self::assertTrue($hiddenAnimal->can_be_eaten);
self::assertArrayHasKey('name', $hiddenAnimal->toArray());
self::assertArrayNotHasKey('country', $hiddenAnimal->toArray(), 'the country column should be hidden');
self::assertArrayHasKey('can_be_eaten', $hiddenAnimal->toArray());
}
}
================================================
FILE: tests/Query/AggregationBuilderTest.php
================================================
'John Doe', 'birthday' => new DateTimeImmutable('1989-01-01')],
['name' => 'Jane Doe', 'birthday' => new DateTimeImmutable('1990-01-01')],
]);
// Create the aggregation pipeline from the query builder
$pipeline = User::aggregate();
$this->assertInstanceOf(AggregationBuilder::class, $pipeline);
$pipeline
->match(name: 'John Doe')
->limit(10)
->addFields(
// Requires MongoDB 5.0+
year: Expression::year(
Expression::dateFieldPath('birthday'),
),
)
->sort(year: Sort::Desc, name: Sort::Asc)
->unset('birthday');
// Compare with the expected pipeline
$expected = [
['$match' => ['name' => 'John Doe']],
['$limit' => 10],
[
'$addFields' => [
'year' => ['$year' => ['date' => '$birthday']],
],
],
['$sort' => ['year' => -1, 'name' => 1]],
['$unset' => ['birthday']],
];
$this->assertSamePipeline($expected, $pipeline->getPipeline());
// Execute the pipeline and validate the results
$results = $pipeline->get();
$this->assertInstanceOf(Collection::class, $results);
$this->assertCount(1, $results);
$this->assertInstanceOf(ObjectId::class, $results->first()['_id']);
$this->assertSame('John Doe', $results->first()['name']);
$this->assertIsInt($results->first()['year']);
$this->assertArrayNotHasKey('birthday', $results->first());
// Execute the pipeline and validate the results in a lazy collection
$results = $pipeline->cursor();
$this->assertInstanceOf(LazyCollection::class, $results);
// Execute the pipeline and return the first result
$result = $pipeline->first();
$this->assertIsArray($result);
$this->assertInstanceOf(ObjectId::class, $result['_id']);
$this->assertSame('John Doe', $result['name']);
}
public function testAddRawStage(): void
{
$collection = $this->createMock(MongoDBCollection::class);
$pipeline = new AggregationBuilder($collection);
$pipeline
->addRawStage('$match', ['name' => 'John Doe'])
->addRawStage('$limit', 10)
->addRawStage('$replaceRoot', (object) ['newRoot' => '$$ROOT']);
$expected = [
['$match' => ['name' => 'John Doe']],
['$limit' => 10],
['$replaceRoot' => ['newRoot' => '$$ROOT']],
];
$this->assertSamePipeline($expected, $pipeline->getPipeline());
}
public function testAddRawStageInvalid(): void
{
$collection = $this->createMock(MongoDBCollection::class);
$pipeline = new AggregationBuilder($collection);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The stage name "match" is invalid. It must start with a "$" sign.');
$pipeline->addRawStage('match', ['name' => 'John Doe']);
}
public function testColumnsCannotBeSpecifiedToCreateAnAggregationBuilder(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Columns cannot be specified to create an aggregation builder.');
User::aggregate(null, ['name']);
}
public function testAggrecationBuilderDoesNotSupportPreviousQueryBuilderInstructions(): void
{
$this->expectException(BadMethodCallException::class);
$this->expectExceptionMessage('Aggregation builder does not support previous query-builder instructions.');
User::where('name', 'John Doe')->aggregate();
}
private static function assertSamePipeline(array $expected, Pipeline $pipeline): void
{
$expected = Document::fromPHP(['pipeline' => $expected])->toCanonicalExtendedJSON();
$codec = new BuilderEncoder();
$actual = $codec->encode($pipeline);
// Normalize with BSON round-trip
$actual = Document::fromPHP(['pipeline' => $actual])->toCanonicalExtendedJSON();
self::assertJsonStringEqualsJsonString($expected, $actual);
}
}
================================================
FILE: tests/Query/BuilderTest.php
================================================
markTestSkipped(sprintf('Method "%s::%s()" does not exist.', Builder::class, $requiredMethod));
}
$builder = $build($this->getBuilder());
$this->assertInstanceOf(Builder::class, $builder);
$mql = $builder->toMql();
// Operations that return a Cursor expect a "typeMap" option.
if (isset($expected['find'][1])) {
$expected['find'][1]['typeMap'] = ['root' => 'object', 'document' => 'array'];
}
if (isset($expected['aggregate'][1])) {
$expected['aggregate'][1]['typeMap'] = ['root' => 'object', 'document' => 'array'];
}
// Compare with assertEquals because the query can contain BSON objects.
$this->assertEquals($expected, $mql, var_export($mql, true));
}
public static function provideQueryBuilderToMql(): iterable
{
/**
* Builder::aggregate() and Builder::count() cannot be tested because they return the result,
* without modifying the builder.
*/
$date = new DateTimeImmutable('2016-07-12 15:30:00');
yield 'select replaces previous select' => [
['find' => [[], ['projection' => ['bar' => 1]]]],
fn (Builder $builder) => $builder->select('foo')->select('bar'),
];
yield 'select array' => [
['find' => [[], ['projection' => ['foo' => 1, 'bar' => 1]]]],
fn (Builder $builder) => $builder->select(['foo', 'bar']),
];
/** @see DatabaseQueryBuilderTest::testAddingSelects */
yield 'addSelect' => [
['find' => [[], ['projection' => ['foo' => 1, 'bar' => 1, 'baz' => 1, 'boom' => 1]]]],
fn (Builder $builder) => $builder->select('foo')
->addSelect('bar')
->addSelect(['baz', 'boom'])
->addSelect('bar'),
];
yield 'select all' => [
['find' => [[], []]],
fn (Builder $builder) => $builder->select('*'),
];
yield 'find all with select' => [
['find' => [[], ['projection' => ['foo' => 1, 'bar' => 1]]]],
fn (Builder $builder) => $builder->select('foo', 'bar'),
];
yield 'find equals' => [
['find' => [['foo' => 'bar'], []]],
fn (Builder $builder) => $builder->where('foo', 'bar'),
];
yield 'find with numeric field name' => [
['find' => [['123' => 'bar'], []]],
fn (Builder $builder) => $builder->where(123, 'bar'),
];
yield 'where with single array of conditions' => [
[
'find' => [
[
'$and' => [
['foo' => 1],
['bar' => 2],
],
],
[], // options
],
],
fn (Builder $builder) => $builder->where(['foo' => 1, 'bar' => 2]),
];
yield 'find > date' => [
['find' => [['foo' => ['$gt' => new UTCDateTime($date)]], []]],
fn (Builder $builder) => $builder->where('foo', '>', $date),
];
/** @see DatabaseQueryBuilderTest::testBasicWhereIns */
yield 'whereIn' => [
['find' => [['foo' => ['$in' => ['bar', 'baz']]], []]],
fn (Builder $builder) => $builder->whereIn('foo', ['bar', 'baz']),
];
// Nested array are not flattened like in the Eloquent builder. MongoDB can compare objects.
yield 'whereIn nested array' => [
['find' => [['_id' => ['$in' => [['issue' => 45582], ['_id' => 2], [3]]]], []]],
fn (Builder $builder) => $builder->whereIn('id', [['issue' => 45582], ['id' => 2], [3]]),
];
yield 'orWhereIn' => [
[
'find' => [
[
'$or' => [
['foo' => 1],
['foo' => ['$in' => [1, 2, 3]]],
],
],
[], // options
],
],
fn (Builder $builder) => $builder->where('foo', '=', 1)
->orWhereIn('foo', [1, 2, 3]),
];
/** @see DatabaseQueryBuilderTest::testBasicWhereNotIns */
yield 'whereNotIn' => [
['find' => [['foo' => ['$nin' => [1, 2, 3]]], []]],
fn (Builder $builder) => $builder->whereNotIn('foo', [1, 2, 3]),
];
yield 'orWhereNotIn' => [
[
'find' => [
[
'$or' => [
['foo' => 1],
['foo' => ['$nin' => [1, 2, 3]]],
],
],
[], // options
],
],
fn (Builder $builder) => $builder->where('foo', '=', 1)
->orWhereNotIn('foo', [1, 2, 3]),
];
/** @see DatabaseQueryBuilderTest::testEmptyWhereIns */
yield 'whereIn empty array' => [
['find' => [['_id' => ['$in' => []]], []]],
fn (Builder $builder) => $builder->whereIn('id', []),
];
yield 'find limit offset select' => [
['find' => [[], ['limit' => 10, 'skip' => 5, 'projection' => ['foo' => 1, 'bar' => 1]]]],
fn (Builder $builder) => $builder->limit(10)->offset(5)->select('foo', 'bar'),
];
yield 'where accepts $ in operators' => [
[
'find' => [
[
'$or' => [
['foo' => ['$type' => 2]],
['foo' => ['$type' => 4]],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->where('foo', '$type', 2)
->orWhere('foo', '$type', 4),
];
/** @see DatabaseQueryBuilderTest::testBasicWhereNot() */
yield 'whereNot (multiple)' => [
[
'find' => [
[
'$and' => [
['$nor' => [['name' => 'foo']]],
['$nor' => [['name' => ['$ne' => 'bar']]]],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->whereNot('name', 'foo')
->whereNot('name', '<>', 'bar'),
];
/** @see DatabaseQueryBuilderTest::testBasicOrWheres() */
yield 'where orWhere' => [
[
'find' => [
[
'$or' => [
['age' => 1],
['email' => 'foo'],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->where('age', '=', 1)
->orWhere('email', '=', 'foo'),
];
/** @see DatabaseQueryBuilderTest::testBasicOrWhereNot() */
yield 'orWhereNot' => [
[
'find' => [
[
'$or' => [
['$nor' => [['name' => 'foo']]],
['$nor' => [['name' => ['$ne' => 'bar']]]],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->orWhereNot('name', 'foo')
->orWhereNot('name', '<>', 'bar'),
];
yield 'whereNot orWhere' => [
[
'find' => [
[
'$or' => [
['$nor' => [['name' => 'foo']]],
['name' => ['$ne' => 'bar']],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->whereNot('name', 'foo')
->orWhere('name', '<>', 'bar'),
];
/** @see DatabaseQueryBuilderTest::testWhereNot() */
yield 'whereNot callable' => [
[
'find' => [
['$nor' => [['name' => 'foo']]],
[], // options
],
],
fn (Builder $builder) => $builder
->whereNot(fn (Builder $q) => $q->where('name', 'foo')),
];
yield 'where whereNot' => [
[
'find' => [
[
'$and' => [
['name' => 'bar'],
['$nor' => [['email' => 'foo']]],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->where('name', '=', 'bar')
->whereNot(function (Builder $q) {
$q->where('email', '=', 'foo');
}),
];
yield 'whereNot (nested)' => [
[
'find' => [
[
'$nor' => [
[
'$and' => [
['name' => 'foo'],
['$nor' => [['email' => ['$ne' => 'bar']]]],
],
],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->whereNot(function (Builder $q) {
$q->where('name', '=', 'foo')
->whereNot('email', '<>', 'bar');
}),
];
yield 'orWhere orWhereNot' => [
[
'find' => [
[
'$or' => [
['name' => 'bar'],
['$nor' => [['email' => 'foo']]],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->orWhere('name', '=', 'bar')
->orWhereNot(function (Builder $q) {
$q->where('email', '=', 'foo');
}),
];
yield 'where orWhereNot' => [
[
'find' => [
[
'$or' => [
['name' => 'bar'],
['$nor' => [['email' => 'foo']]],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->where('name', '=', 'bar')
->orWhereNot('email', '=', 'foo'),
];
/** @see DatabaseQueryBuilderTest::testWhereNotWithArrayConditions() */
yield 'whereNot with arrays of single condition' => [
[
'find' => [
[
'$nor' => [
[
'$and' => [
['foo' => 1],
['bar' => 2],
],
],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->whereNot([['foo', 1], ['bar', 2]]),
];
yield 'whereNot with single array of conditions' => [
[
'find' => [
[
'$nor' => [
[
'$and' => [
['foo' => 1],
['bar' => 2],
],
],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->whereNot(['foo' => 1, 'bar' => 2]),
];
yield 'whereNot with arrays of single condition with operator' => [
[
'find' => [
[
'$nor' => [
[
'$and' => [
['foo' => 1],
['bar' => ['$lt' => 2]],
],
],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->whereNot([
['foo', 1],
['bar', '<', 2],
]),
];
yield 'where all' => [
['find' => [['tags' => ['$all' => ['ssl', 'security']]], []]],
fn (Builder $builder) => $builder->where('tags', 'all', ['ssl', 'security']),
];
yield 'where all nested operators' => [
[
'find' => [
[
'tags' => [
'$all' => [
['$elemMatch' => ['size' => 'M', 'num' => ['$gt' => 50]]],
['$elemMatch' => ['num' => 100, 'color' => 'green']],
],
],
],
[],
],
],
fn (Builder $builder) => $builder->where('tags', 'all', [
['$elemMatch' => ['size' => 'M', 'num' => ['$gt' => 50]]],
['$elemMatch' => ['num' => 100, 'color' => 'green']],
]),
];
/** @see DatabaseQueryBuilderTest::testForPage() */
yield 'forPage' => [
['find' => [[], ['limit' => 20, 'skip' => 40]]],
fn (Builder $builder) => $builder->forPage(3, 20),
];
/** @see DatabaseQueryBuilderTest::testLimitsAndOffsets() */
yield 'offset limit' => [
['find' => [[], ['skip' => 5, 'limit' => 10]]],
fn (Builder $builder) => $builder->offset(5)->limit(10),
];
yield 'offset limit zero (unset)' => [
['find' => [[], []]],
fn (Builder $builder) => $builder
->offset(0)->limit(0),
];
yield 'offset limit zero (reset)' => [
['find' => [[], []]],
fn (Builder $builder) => $builder
->offset(5)->limit(10)
->offset(0)->limit(0),
];
yield 'offset limit negative (unset)' => [
['find' => [[], []]],
fn (Builder $builder) => $builder
->offset(-5)->limit(-10),
];
yield 'offset limit null (reset)' => [
['find' => [[], []]],
fn (Builder $builder) => $builder
->offset(5)->limit(10)
->offset(null)->limit(null),
];
yield 'skip take (aliases)' => [
['find' => [[], ['skip' => 5, 'limit' => 10]]],
fn (Builder $builder) => $builder->skip(5)->limit(10),
];
/** @see DatabaseQueryBuilderTest::testOrderBys() */
yield 'orderBy multiple columns' => [
['find' => [[], ['sort' => ['email' => 1, 'age' => -1]]]],
fn (Builder $builder) => $builder
->orderBy('email')
->orderBy('age', 'desc'),
];
yield 'orders by id field' => [
['find' => [[], ['sort' => ['_id' => 1]]]],
fn (Builder $builder) => $builder->orderBy('id'),
];
yield 'orders = null' => [
['find' => [[], []]],
function (Builder $builder) {
$builder->orders = null;
return $builder;
},
];
yield 'orders = []' => [
['find' => [[], []]],
function (Builder $builder) {
$builder->orders = [];
return $builder;
},
];
yield 'multiple orders with direction' => [
['find' => [[], ['sort' => ['email' => -1, 'age' => 1]]]],
fn (Builder $builder) => $builder
->orderBy('email', -1)
->orderBy('age', 1),
];
yield 'chunked ordering' => [
[
'find' => [
['_id' => ['$gt' => 0]],
['sort' => ['_id' => 1, 'name' => 1], 'limit' => 2],
],
],
function (Builder $builder) {
$builder->orderBy('_id')->orderBy('name');
$builder->forPageAfterId(2);
return $builder;
},
];
yield 'chunked ordering with id alias' => [
[
'find' => [
['_id' => ['$gt' => 0]],
['sort' => ['_id' => 1, 'name' => 1], 'limit' => 2],
],
],
function (Builder $builder) {
$builder->orderBy('id')->orderBy('name');
$builder->forPageAfterId(2);
return $builder;
},
];
yield 'orderByDesc' => [
['find' => [[], ['sort' => ['email' => -1]]]],
fn (Builder $builder) => $builder->orderByDesc('email'),
];
/** @see DatabaseQueryBuilderTest::testReorder() */
yield 'reorder reset' => [
['find' => [[], []]],
fn (Builder $builder) => $builder->orderBy('name')->reorder(),
];
yield 'reorder column' => [
['find' => [[], ['sort' => ['name' => -1]]]],
fn (Builder $builder) => $builder->orderBy('name')->reorder('name', 'desc'),
];
/** @link https://www.mongodb.com/docs/manual/reference/method/cursor.sort/#text-score-metadata-sort */
yield 'orderBy array meta' => [
[
'find' => [
['$text' => ['$search' => 'operating']],
['sort' => ['score' => ['$meta' => 'textScore']]],
],
],
fn (Builder $builder) => $builder
->where('$text', ['$search' => 'operating'])
->orderBy('score', ['$meta' => 'textScore']),
];
/** @see DatabaseQueryBuilderTest::testWhereBetweens() */
yield 'whereBetween array of numbers' => [
['find' => [['_id' => ['$gte' => 1, '$lte' => 2]], []]],
fn (Builder $builder) => $builder->whereBetween('id', [1, 2]),
];
yield 'whereBetween nested array of numbers' => [
['find' => [['_id' => ['$gte' => [1], '$lte' => [2, 3]]], []]],
fn (Builder $builder) => $builder->whereBetween('id', [[1], [2, 3]]),
];
$date = new DateTimeImmutable('2018-09-30 15:00:00 +02:00');
yield 'where $lt DateTimeInterface' => [
['find' => [['created_at' => ['$lt' => new UTCDateTime($date)]], []]],
fn (Builder $builder) => $builder->where('created_at', '<', $date),
];
$period = now()->toPeriod(now()->addMonth());
yield 'whereBetween CarbonPeriod' => [
[
'find' => [
[
'created_at' => [
'$gte' => new UTCDateTime($period->getStartDate()),
'$lte' => new UTCDateTime($period->getEndDate()),
],
],
[], // options
],
],
fn (Builder $builder) => $builder->whereBetween('created_at', $period),
];
yield 'whereBetween collection' => [
['find' => [['_id' => ['$gte' => 1, '$lte' => 2]], []]],
fn (Builder $builder) => $builder->whereBetween('id', collect([1, 2])),
];
/** @see DatabaseQueryBuilderTest::testOrWhereBetween() */
yield 'orWhereBetween array of numbers' => [
[
'find' => [
[
'$or' => [
['age' => 1],
['age' => ['$gte' => 3, '$lte' => 5]],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->where('age', '=', 1)
->orWhereBetween('age', [3, 5]),
];
/** @link https://www.mongodb.com/docs/manual/reference/bson-type-comparison-order/#arrays */
yield 'orWhereBetween nested array of numbers' => [
[
'find' => [
[
'$or' => [
['age' => 1],
['age' => ['$gte' => [4], '$lte' => [6, 8]]],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->where('age', '=', 1)
->orWhereBetween('age', [[4], [6, 8]]),
];
yield 'orWhereBetween collection' => [
[
'find' => [
[
'$or' => [
['age' => 1],
['age' => ['$gte' => 3, '$lte' => 4]],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->where('age', '=', 1)
->orWhereBetween('age', collect([3, 4])),
];
yield 'whereNotBetween array of numbers' => [
[
'find' => [
[
'$or' => [
['age' => ['$lte' => 1]],
['age' => ['$gte' => 2]],
],
],
[], // options
],
],
fn (Builder $builder) => $builder->whereNotBetween('age', [1, 2]),
];
/** @see DatabaseQueryBuilderTest::testOrWhereNotBetween() */
yield 'orWhereNotBetween array of numbers' => [
[
'find' => [
[
'$or' => [
['age' => 1],
[
'$or' => [
['age' => ['$lte' => 3]],
['age' => ['$gte' => 5]],
],
],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->where('age', '=', 1)
->orWhereNotBetween('age', [3, 5]),
];
yield 'orWhereNotBetween nested array of numbers' => [
[
'find' => [
[
'$or' => [
['age' => 1],
[
'$or' => [
['age' => ['$lte' => [2, 3]]],
['age' => ['$gte' => [5]]],
],
],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->where('age', '=', 1)
->orWhereNotBetween('age', [[2, 3], [5]]),
];
yield 'orWhereNotBetween collection' => [
[
'find' => [
[
'$or' => [
['age' => 1],
[
'$or' => [
['age' => ['$lte' => 3]],
['age' => ['$gte' => 4]],
],
],
],
],
[], // options
],
],
fn (Builder $builder) => $builder
->where('age', '=', 1)
->orWhereNotBetween('age', collect([3, 4])),
];
yield 'where like' => [
['find' => [['name' => new Regex('^acme$', 'i')], []]],
fn (Builder $builder) => $builder->where('name', 'like', 'acme'),
];
yield 'where ilike' => [ // Alias for like
['find' => [['name' => new Regex('^acme$', 'i')], []]],
fn (Builder $builder) => $builder->where('name', 'ilike', 'acme'),
];
yield 'where like escape' => [
['find' => [['name' => new Regex('^\^ac\.me\$$', 'i')], []]],
fn (Builder $builder) => $builder->where('name', 'like', '^ac.me$'),
];
yield 'where like unescaped \% \_' => [
['find' => [['name' => new Regex('^a%cm_e$', 'i')], []]],
fn (Builder $builder) => $builder->where('name', 'like', 'a\%cm\_e'),
];
yield 'where like %' => [
['find' => [['name' => new Regex('^.*ac.*me.*$', 'i')], []]],
fn (Builder $builder) => $builder->where('name', 'like', '%ac%%me%'),
];
yield 'where like _' => [
['find' => [['name' => new Regex('^.ac..me.$', 'i')], []]],
fn (Builder $builder) => $builder->where('name', 'like', '_ac__me_'),
];
yield 'whereLike' => [
['find' => [['name' => new Regex('^1$', 'i')], []]],
fn
(Builder $builder) => $builder->whereLike('name', '1'),
'whereLike',
];
yield 'whereLike case not sensitive' => [
['find' => [['name' => new Regex('^1$', 'i')], []]],
fn
(Builder $builder) => $builder->whereLike('name', '1', false),
'whereLike',
];
yield 'whereLike case sensitive' => [
['find' => [['name' => new Regex('^1$', '')], []]],
fn
(Builder $builder) => $builder->whereLike('name', '1', true),
'whereLike',
];
yield 'whereNotLike' => [
['find' => [['name' => ['$not' => new Regex('^1$', 'i')]], []]],
fn
(Builder $builder) => $builder->whereNotLike('name', '1'),
'whereNotLike',
];
yield 'whereNotLike case not sensitive' => [
['find' => [['name' => ['$not' => new Regex('^1$', 'i')]], []]],
fn
(Builder $builder) => $builder->whereNotLike('name', '1', false),
'whereNotLike',
];
yield 'whereNotLike case sensitive' => [
['find' => [['name' => ['$not' => new Regex('^1$', '')]], []]],
fn
(Builder $builder) => $builder->whereNotLike('name', '1', true),
'whereNotLike',
];
$regex = new Regex('^acme$', 'si');
yield 'where BSON\Regex' => [
['find' => [['name' => $regex], []]],
fn (Builder $builder) => $builder->where('name', 'regex', $regex),
];
yield 'where regexp' => [ // Alias for regex
['find' => [['name' => $regex], []]],
fn (Builder $builder) => $builder->where('name', 'regex', '/^acme$/si'),
];
yield 'where regex delimiter /' => [
['find' => [['name' => $regex], []]],
fn (Builder $builder) => $builder->where('name', 'regex', '/^acme$/si'),
];
yield 'where regex delimiter #' => [
['find' => [['name' => $regex], []]],
fn (Builder $builder) => $builder->where('name', 'regex', '#^acme$#si'),
];
yield 'where regex delimiter ~' => [
['find' => [['name' => $regex], []]],
fn (Builder $builder) => $builder->where('name', 'regex', '#^acme$#si'),
];
yield 'where regex with escaped characters' => [
['find' => [['name' => new Regex('a\.c\/m\+e', '')], []]],
fn (Builder $builder) => $builder->where('name', 'regex', '/a\.c\/m\+e/'),
];
yield 'where not regex' => [
['find' => [['name' => ['$not' => $regex]], []]],
fn (Builder $builder) => $builder->where('name', 'not regex', '/^acme$/si'),
];
yield 'where date' => [
[
'find' => [
[
'created_at' => [
'$gte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 00:00:00.000 +00:00')),
'$lte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 23:59:59.999 +00:00')),
],
],
[],
],
],
fn (Builder $builder) => $builder->whereDate('created_at', '2018-09-30'),
];
yield 'where date DateTimeImmutable' => [
[
'find' => [
[
'created_at' => [
'$gte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 00:00:00.000 +00:00')),
'$lte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 23:59:59.999 +00:00')),
],
],
[],
],
],
fn (Builder $builder) => $builder->whereDate('created_at', '=', new DateTimeImmutable('2018-09-30 15:00:00 +00:00')),
];
yield 'where date !=' => [
[
'find' => [
[
'created_at' => [
'$not' => [
'$gte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 00:00:00.000 +00:00')),
'$lte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 23:59:59.999 +00:00')),
],
],
],
[],
],
],
fn (Builder $builder) => $builder->whereDate('created_at', '!=', '2018-09-30'),
];
yield 'where date <' => [
[
'find' => [
[
'created_at' => [
'$lt' => new UTCDateTime(new DateTimeImmutable('2018-09-30 00:00:00.000 +00:00')),
],
],
[],
],
],
fn (Builder $builder) => $builder->whereDate('created_at', '<', '2018-09-30'),
];
yield 'where date >=' => [
[
'find' => [
[
'created_at' => [
'$gte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 00:00:00.000 +00:00')),
],
],
[],
],
],
fn (Builder $builder) => $builder->whereDate('created_at', '>=', '2018-09-30'),
];
yield 'where date >' => [
[
'find' => [
[
'created_at' => [
'$gt' => new UTCDateTime(new DateTimeImmutable('2018-09-30 23:59:59.999 +00:00')),
],
],
[],
],
],
fn (Builder $builder) => $builder->whereDate('created_at', '>', '2018-09-30'),
];
yield 'where date <=' => [
[
'find' => [
[
'created_at' => [
'$lte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 23:59:59.999 +00:00')),
],
],
[],
],
],
fn (Builder $builder) => $builder->whereDate('created_at', '<=', '2018-09-30'),
];
yield 'where day' => [
[
'find' => [
[
'$expr' => [
'$eq' => [
['$dayOfMonth' => '$created_at'],
5,
],
],
],
[],
],
],
fn (Builder $builder) => $builder->whereDay('created_at', 5),
];
yield 'where day > string' => [
[
'find' => [
[
'$expr' => [
'$gt' => [
['$dayOfMonth' => '$created_at'],
5,
],
],
],
[],
],
],
fn (Builder $builder) => $builder->whereDay('created_at', '>', '05'),
];
yield 'where month' => [
[
'find' => [
[
'$expr' => [
'$eq' => [
['$month' => '$created_at'],
10,
],
],
],
[],
],
],
fn (Builder $builder) => $builder->whereMonth('created_at', 10),
];
yield 'where month > string' => [
[
'find' => [
[
'$expr' => [
'$gt' => [
['$month' => '$created_at'],
5,
],
],
],
[],
],
],
fn (Builder $builder) => $builder->whereMonth('created_at', '>', '05'),
];
yield 'where year' => [
[
'find' => [
[
'$expr' => [
'$eq' => [
['$year' => '$created_at'],
2023,
],
],
],
[],
],
],
fn (Builder $builder) => $builder->whereYear('created_at', 2023),
];
yield 'where year > string' => [
[
'find' => [
[
'$expr' => [
'$gt' => [
['$year' => '$created_at'],
2023,
],
],
],
[],
],
],
fn (Builder $builder) => $builder->whereYear('created_at', '>', '2023'),
];
yield 'where time HH:MM:SS' => [
[
'find' => [
[
'$expr' => [
'$eq' => [
['$dateToString' => ['date' => '$created_at', 'format' => '%H:%M:%S']],
'10:11:12',
],
],
],
[],
],
],
fn (Builder $builder) => $builder->whereTime('created_at', '10:11:12'),
];
yield 'where time HH:MM' => [
[
'find' => [
[
'$expr' => [
'$eq' => [
['$dateToString' => ['date' => '$created_at', 'format' => '%H:%M']],
'10:11',
],
],
],
[],
],
],
fn (Builder $builder) => $builder->whereTime('created_at', '10:11'),
];
yield 'where time HH' => [
[
'find' => [
[
'$expr' => [
'$eq' => [
['$dateToString' => ['date' => '$created_at', 'format' => '%H']],
'10',
],
],
],
[],
],
],
fn (Builder $builder) => $builder->whereTime('created_at', '10'),
];
yield 'where time DateTime' => [
[
'find' => [
[
'$expr' => [
'$eq' => [
['$dateToString' => ['date' => '$created_at', 'format' => '%H:%M:%S']],
'10:11:12',
],
],
],
[],
],
],
fn (Builder $builder) => $builder->whereTime('created_at', new DateTimeImmutable('2023-08-22 10:11:12')),
];
yield 'where time >' => [
[
'find' => [
[
'$expr' => [
'$gt' => [
['$dateToString' => ['date' => '$created_at', 'format' => '%H:%M:%S']],
'10:11:12',
],
],
],
[],
],
],
fn (Builder $builder) => $builder->whereTime('created_at', '>', '10:11:12'),
];
/** @see DatabaseQueryBuilderTest::testBasicSelectDistinct */
yield 'distinct' => [
['distinct' => ['foo', [], []]],
fn (Builder $builder) => $builder->distinct('foo'),
];
yield 'select distinct' => [
['distinct' => ['foo', [], []]],
fn (Builder $builder) => $builder->select('foo', 'bar')
->distinct(),
];
/** @see DatabaseQueryBuilderTest::testBasicSelectDistinctOnColumns */
yield 'select distinct on' => [
['distinct' => ['foo', [], []]],
fn (Builder $builder) => $builder->distinct('foo')
->select('foo', 'bar'),
];
/** @see DatabaseQueryBuilderTest::testLatest() */
yield 'latest' => [
['find' => [[], ['sort' => ['created_at' => -1]]]],
fn (Builder $builder) => $builder->latest(),
];
yield 'latest limit' => [
['find' => [[], ['sort' => ['created_at' => -1], 'limit' => 1]]],
fn (Builder $builder) => $builder->latest()->limit(1),
];
yield 'latest custom field' => [
['find' => [[], ['sort' => ['updated_at' => -1]]]],
fn (Builder $builder) => $builder->latest('updated_at'),
];
/** @see DatabaseQueryBuilderTest::testOldest() */
yield 'oldest' => [
['find' => [[], ['sort' => ['created_at' => 1]]]],
fn (Builder $builder) => $builder->oldest(),
];
yield 'oldest limit' => [
['find' => [[], ['sort' => ['created_at' => 1], 'limit' => 1]]],
fn (Builder $builder) => $builder->oldest()->limit(1),
];
yield 'oldest custom field' => [
['find' => [[], ['sort' => ['updated_at' => 1]]]],
fn (Builder $builder) => $builder->oldest('updated_at'),
];
yield 'groupBy' => [
[
'aggregate' => [
[['$group' => ['_id' => ['foo' => '$foo'], 'foo' => ['$last' => '$foo']]]],
[], // options
],
],
fn (Builder $builder) => $builder->groupBy('foo'),
];
yield 'sub-query' => [
[
'find' => [
[
'filters' => [
'$elemMatch' => [
'$and' => [
['search_by' => 'by search'],
['value' => 'foo'],
],
],
],
],
[], // options
],
],
fn (Builder $builder) => $builder->where(
'filters',
'elemMatch',
function (Builder $elemMatchQuery): void {
$elemMatchQuery->where([ 'search_by' => 'by search', 'value' => 'foo' ]);
},
),
];
yield 'id alias for _id' => [
['find' => [['_id' => 1], []]],
fn (Builder $builder) => $builder->where('id', 1),
];
yield 'id alias for _id with $or' => [
['find' => [['$or' => [['_id' => 1], ['_id' => 2]]], []]],
fn (Builder $builder) => $builder->where('id', 1)->orWhere('id', 2),
];
yield 'select colums with id alias' => [
['find' => [[], ['projection' => ['name' => 1, 'email' => 1, '_id' => 1]]]],
fn (Builder $builder) => $builder->select('name', 'email', 'id'),
];
// Method added in Laravel v10.47.0
/** @see DatabaseQueryBuilderTest::testWhereAll */
yield 'whereAll' => [
[
'find' => [
['$and' => [['last_name' => 'Doe'], ['email' => 'Doe']]],
[], // options
],
],
fn
(Builder $builder) => $builder->whereAll(['last_name', 'email'], 'Doe'),
'whereAll',
];
yield 'whereAll operator' => [
[
'find' => [
[
'$and' => [
['last_name' => ['$not' => new Regex('^.*Doe.*$', 'i')]],
['email' => ['$not' => new Regex('^.*Doe.*$', 'i')]],
],
],
[], // options
],
],
fn
(Builder $builder) => $builder->whereAll(['last_name', 'email'], 'not like', '%Doe%'),
'whereAll',
];
/** @see DatabaseQueryBuilderTest::testOrWhereAll */
yield 'orWhereAll' => [
[
'find' => [
[
'$or' => [
['first_name' => 'John'],
['$and' => [['last_name' => 'Doe'], ['email' => 'Doe']]],
],
],
[], // options
],
],
fn
(Builder $builder) => $builder
->where('first_name', 'John')
->orWhereAll(['last_name', 'email'], 'Doe'),
'orWhereAll',
];
yield 'orWhereAll operator' => [
[
'find' => [
[
'$or' => [
['first_name' => new Regex('^.*John.*$', 'i')],
[
'$and' => [
['last_name' => ['$not' => new Regex('^.*Doe.*$', 'i')]],
['email' => ['$not' => new Regex('^.*Doe.*$', 'i')]],
],
],
],
],
[], // options
],
],
fn
(Builder $builder) => $builder
->where('first_name', 'like', '%John%')
->orWhereAll(['last_name', 'email'], 'not like', '%Doe%'),
'orWhereAll',
];
// Method added in Laravel v10.47.0
/** @see DatabaseQueryBuilderTest::testWhereAny */
yield 'whereAny' => [
[
'find' => [
['$or' => [['last_name' => 'Doe'], ['email' => 'Doe']]],
[], // options
],
],
fn
(Builder $builder) => $builder->whereAny(['last_name', 'email'], 'Doe'),
'whereAny',
];
yield 'whereAny operator' => [
[
'find' => [
[
'$or' => [
['last_name' => ['$not' => new Regex('^.*Doe.*$', 'i')]],
['email' => ['$not' => new Regex('^.*Doe.*$', 'i')]],
],
],
[], // options
],
],
fn
(Builder $builder) => $builder->whereAny(['last_name', 'email'], 'not like', '%Doe%'),
'whereAny',
];
/** @see DatabaseQueryBuilderTest::testOrWhereAny */
yield 'orWhereAny' => [
[
'find' => [
[
'$or' => [
['first_name' => 'John'],
['$or' => [['last_name' => 'Doe'], ['email' => 'Doe']]],
],
],
[], // options
],
],
fn
(Builder $builder) => $builder
->where('first_name', 'John')
->orWhereAny(['last_name', 'email'], 'Doe'),
'whereAny',
];
yield 'orWhereAny operator' => [
[
'find' => [
[
'$or' => [
['first_name' => new Regex('^.*John.*$', 'i')],
[
'$or' => [
['last_name' => ['$not' => new Regex('^.*Doe.*$', 'i')]],
['email' => ['$not' => new Regex('^.*Doe.*$', 'i')]],
],
],
],
],
[], // options
],
],
fn
(Builder $builder) => $builder
->where('first_name', 'like', '%John%')
->orWhereAny(['last_name', 'email'], 'not like', '%Doe%'),
'orWhereAny',
];
yield 'raw filter with _id and date' => [
[
'find' => [
[
'$and' => [
[
'$or' => [
['foo._id' => 1],
['created_at' => ['$gte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 00:00:00.000 +00:00'))]],
],
],
['age' => 15],
],
],
[], // options
],
],
fn (Builder $builder) => $builder->where([
'$or' => [
['foo.id' => 1],
['created_at' => ['$gte' => new DateTimeImmutable('2018-09-30 00:00:00 +00:00')]],
],
])->where('age', 15),
];
yield 'arrow notation' => [
['find' => [['data.format' => 1], []]],
fn (Builder $builder) => $builder->where('data->format', 1),
];
yield 'arrow notation with id' => [
['find' => [['embedded._id' => 1], []]],
fn (Builder $builder) => $builder->where('embedded->id', 1),
];
yield 'options' => [
['find' => [[], ['comment' => 'hello']]],
fn (Builder $builder) => $builder->options(['comment' => 'hello']),
];
yield 'readPreference' => [
['find' => [[], ['readPreference' => new ReadPreference(ReadPreference::SECONDARY_PREFERRED)]]],
fn (Builder $builder) => $builder->readPreference(ReadPreference::SECONDARY_PREFERRED),
];
yield 'readPreference advanced' => [
['find' => [[], ['readPreference' => new ReadPreference(ReadPreference::NEAREST, [['dc' => 'ny']], ['maxStalenessSeconds' => 120])]]],
fn (Builder $builder) => $builder->readPreference(ReadPreference::NEAREST, [['dc' => 'ny']], ['maxStalenessSeconds' => 120]),
];
yield 'hint' => [
['find' => [[], ['hint' => ['foo' => 1]]]],
fn (Builder $builder) => $builder->hint(['foo' => 1]),
];
yield 'timeout' => [
['find' => [[], ['maxTimeMS' => 2000]]],
fn (Builder $builder) => $builder->timeout(2),
];
}
#[DataProvider('provideExceptions')]
public function testException($class, $message, Closure $build): void
{
$builder = $this->getBuilder();
$this->expectException($class);
$this->expectExceptionMessage($message);
$build($builder)->toMQL();
}
public static function provideExceptions(): iterable
{
yield 'orderBy invalid direction' => [
InvalidArgumentException::class,
'Order direction must be "asc" or "desc"',
fn (Builder $builder) => $builder->orderBy('_id', 'dasc'),
];
/** @see DatabaseQueryBuilderTest::testWhereBetweens */
yield 'whereBetween array too short' => [
InvalidArgumentException::class,
'Between $values must be a list with exactly two elements: [min, max]',
fn (Builder $builder) => $builder->whereBetween('id', [1]),
];
yield 'whereBetween array too short (nested)' => [
InvalidArgumentException::class,
'Between $values must be a list with exactly two elements: [min, max]',
fn (Builder $builder) => $builder->whereBetween('id', [[1, 2]]),
];
yield 'whereBetween array too long' => [
InvalidArgumentException::class,
'Between $values must be a list with exactly two elements: [min, max]',
fn (Builder $builder) => $builder->whereBetween('id', [1, 2, 3]),
];
yield 'whereBetween collection too long' => [
InvalidArgumentException::class,
'Between $values must be a list with exactly two elements: [min, max]',
fn (Builder $builder) => $builder->whereBetween('id', new Collection([1, 2, 3])),
];
yield 'whereBetween array is not a list' => [
InvalidArgumentException::class,
'Between $values must be a list with exactly two elements: [min, max]',
fn (Builder $builder) => $builder->whereBetween('id', ['min' => 1, 'max' => 2]),
];
yield 'find with single string argument' => [
ArgumentCountError::class,
'Too few arguments to function MongoDB\Laravel\Query\Builder::where(\'foo\'), 1 passed and at least 2 expected when the 1st is not an array',
fn (Builder $builder) => $builder->where('foo'),
];
yield 'find with single numeric argument' => [
ArgumentCountError::class,
'Too few arguments to function MongoDB\Laravel\Query\Builder::where(123), 1 passed and at least 2 expected when the 1st is not an array',
fn (Builder $builder) => $builder->where(123),
];
yield 'where regex not starting with /' => [
LogicException::class,
'Missing expected starting delimiter in regular expression "^ac/me$", supported delimiters are: / # ~',
fn (Builder $builder) => $builder->where('name', 'regex', '^ac/me$'),
];
yield 'where regex not ending with /' => [
LogicException::class,
'Missing expected ending delimiter "/" in regular expression "/foo#bar"',
fn (Builder $builder) => $builder->where('name', 'regex', '/foo#bar'),
];
yield 'whereTime with invalid time' => [
InvalidArgumentException::class,
'Invalid time format, expected HH:MM:SS, HH:MM or HH, got "10:11:12:13"',
fn (Builder $builder) => $builder->whereTime('created_at', '10:11:12:13'),
];
yield 'whereTime out of range' => [
InvalidArgumentException::class,
'Invalid time format, expected HH:MM:SS, HH:MM or HH, got "23:70"',
fn (Builder $builder) => $builder->whereTime('created_at', '23:70'),
];
yield 'whereTime invalid type' => [
InvalidArgumentException::class,
'Invalid time format, expected HH:MM:SS, HH:MM or HH, got "stdClass"',
fn (Builder $builder) => $builder->whereTime('created_at', new stdClass()),
];
yield 'where invalid column type' => [
InvalidArgumentException::class,
'First argument of MongoDB\Laravel\Query\Builder::where must be a field path as "string". Got "float"',
fn (Builder $builder) => $builder->where(2.3, '>', 1),
];
}
#[DataProvider('getEloquentMethodsNotSupported')]
public function testEloquentMethodsNotSupported(Closure $callback)
{
$builder = $this->getBuilder();
$this->expectException(BadMethodCallException::class);
$this->expectExceptionMessage('This method is not supported by MongoDB');
$callback($builder);
}
public static function getEloquentMethodsNotSupported()
{
// Most of this methods can be implemented using aggregation framework
// whereInRaw, whereNotInRaw, orWhereInRaw, orWhereNotInRaw, whereBetweenColumns
yield 'toSql' => [fn (Builder $builder) => $builder->toSql()];
yield 'toRawSql' => [fn (Builder $builder) => $builder->toRawSql()];
/** @see DatabaseQueryBuilderTest::testBasicWhereColumn() */
/** @see DatabaseQueryBuilderTest::testArrayWhereColumn() */
yield 'whereColumn' => [fn (Builder $builder) => $builder->whereColumn('first_name', 'last_name')];
yield 'orWhereColumn' => [fn (Builder $builder) => $builder->orWhereColumn('first_name', 'last_name')];
/** @see DatabaseQueryBuilderTest::testWhereFulltextMySql() */
yield 'whereFulltext' => [fn (Builder $builder) => $builder->whereFulltext('body', 'Hello World')];
/** @see DatabaseQueryBuilderTest::testGroupBys() */
yield 'groupByRaw' => [fn (Builder $builder) => $builder->groupByRaw('DATE(created_at)')];
/** @see DatabaseQueryBuilderTest::testOrderBys() */
yield 'orderByRaw' => [fn (Builder $builder) => $builder->orderByRaw('"age" ? desc', ['foo'])];
/** @see DatabaseQueryBuilderTest::testInRandomOrderMySql */
yield 'inRandomOrder' => [fn (Builder $builder) => $builder->inRandomOrder()];
yield 'union' => [fn (Builder $builder) => $builder->union($builder)];
yield 'unionAll' => [fn (Builder $builder) => $builder->unionAll($builder)];
/** @see DatabaseQueryBuilderTest::testRawHavings */
yield 'havingRaw' => [fn (Builder $builder) => $builder->havingRaw('user_foo < user_bar')];
yield 'having' => [fn (Builder $builder) => $builder->having('baz', '=', 1)];
yield 'havingBetween' => [fn (Builder $builder) => $builder->havingBetween('last_login_date', ['2018-11-16', '2018-12-16'])];
yield 'orHavingRaw' => [fn (Builder $builder) => $builder->orHavingRaw('user_foo < user_bar')];
/** @see DatabaseQueryBuilderTest::testWhereIntegerInRaw */
yield 'whereIntegerInRaw' => [fn (Builder $builder) => $builder->whereIntegerInRaw('id', ['1a', 2])];
/** @see DatabaseQueryBuilderTest::testOrWhereIntegerInRaw */
yield 'orWhereIntegerInRaw' => [fn (Builder $builder) => $builder->orWhereIntegerInRaw('id', ['1a', 2])];
/** @see DatabaseQueryBuilderTest::testWhereIntegerNotInRaw */
yield 'whereIntegerNotInRaw' => [fn (Builder $builder) => $builder->whereIntegerNotInRaw('id', ['1a', 2])];
/** @see DatabaseQueryBuilderTest::testOrWhereIntegerNotInRaw */
yield 'orWhereIntegerNotInRaw' => [fn (Builder $builder) => $builder->orWhereIntegerNotInRaw('id', ['1a', 2])];
}
#[DataProvider('provideDisableRenameEmbeddedIdField')]
public function testDisableRenameEmbeddedIdField(array $expected, Closure $build)
{
$builder = $this->getBuilder(false);
$this->assertFalse($builder->getConnection()->getRenameEmbeddedIdField());
$mql = $build($builder)->toMql();
$this->assertEquals($expected, $mql);
}
public static function provideDisableRenameEmbeddedIdField()
{
yield 'rename embedded id field' => [
[
'find' => [
[
'$and' => [
['_id' => 10],
['nested.id' => 20],
['embed' => ['id' => 30]],
],
],
['typeMap' => ['root' => 'object', 'document' => 'array']],
],
],
fn (Builder $builder) => $builder->where('id', '=', 10)
->where('nested.id', '=', 20)
->where('embed', '=', ['id' => 30]),
];
yield 'rename root id' => [
['find' => [['_id' => 10], ['typeMap' => ['root' => 'object', 'document' => 'array']]]],
fn (Builder $builder) => $builder->where('id', '=', 10),
];
yield 'nested id not renamed' => [
['find' => [['nested.id' => 20], ['typeMap' => ['root' => 'object', 'document' => 'array']]]],
fn (Builder $builder) => $builder->where('nested.id', '=', 20),
];
yield 'embed id not renamed' => [
['find' => [['embed' => ['id' => 30]], ['typeMap' => ['root' => 'object', 'document' => 'array']]]],
fn (Builder $builder) => $builder->where('embed', '=', ['id' => 30]),
];
yield 'nested $and in $or' => [
[
'find' => [
[
'$or' => [
[
'$and' => [
['_id' => 10],
['nested.id' => 20],
['embed' => ['id' => 30]],
],
],
[
'$and' => [
['_id' => 40],
['nested.id' => 50],
['embed' => ['id' => 60]],
],
],
],
],
['typeMap' => ['root' => 'object', 'document' => 'array']],
],
],
fn (Builder $builder) => $builder->orWhere(function (Builder $builder) {
return $builder->where('id', '=', 10)
->where('nested.id', '=', 20)
->where('embed', '=', ['id' => 30]);
})->orWhere(function (Builder $builder) {
return $builder->where('id', '=', 40)
->where('nested.id', '=', 50)
->where('embed', '=', ['id' => 60]);
}),
];
}
private function getBuilder(bool $renameEmbeddedIdField = true): Builder
{
$connection = $this->createStub(Connection::class);
$connection->method('getRenameEmbeddedIdField')->willReturn($renameEmbeddedIdField);
$processor = $this->createStub(Processor::class);
$connection->method('getSession')->willReturn(null);
$connection->method('getQueryGrammar')->willReturn(new Grammar($connection));
return new Builder($connection, null, $processor);
}
}
================================================
FILE: tests/QueryBuilderTest.php
================================================
truncate();
DB::table('items')->truncate();
parent::tearDown();
}
public function testDeleteWithId()
{
$user = DB::table('users')->insertGetId([
['name' => 'Jane Doe', 'age' => 20],
]);
$userId = (string) $user;
DB::table('items')->insert([
['name' => 'one thing', 'user_id' => $userId],
['name' => 'last thing', 'user_id' => $userId],
['name' => 'another thing', 'user_id' => $userId],
['name' => 'one more thing', 'user_id' => $userId],
]);
$product = DB::table('items')->first();
$pid = (string) ($product->id);
DB::table('items')->where('user_id', $userId)->delete($pid);
$this->assertEquals(3, DB::table('items')->count());
$product = DB::table('items')->first();
$pid = $product->id;
DB::table('items')->where('user_id', $userId)->delete($pid);
DB::table('items')->where('user_id', $userId)->delete(md5('random-id'));
$this->assertEquals(2, DB::table('items')->count());
}
public function testCollection()
{
$this->assertInstanceOf(Builder::class, DB::table('users'));
}
public function testGet()
{
$users = DB::table('users')->get();
$this->assertCount(0, $users);
DB::table('users')->insert(['name' => 'John Doe']);
$users = DB::table('users')->get();
$this->assertCount(1, $users);
}
public function testNoDocument()
{
$items = DB::table('items')->where('name', 'nothing')->get()->toArray();
$this->assertEquals([], $items);
$item = DB::table('items')->where('name', 'nothing')->first();
$this->assertNull($item);
$item = DB::table('items')->where('id', '51c33d8981fec6813e00000a')->first();
$this->assertNull($item);
}
public function testInsert()
{
DB::table('users')->insert([
'tags' => ['tag1', 'tag2'],
'name' => 'John Doe',
]);
$users = DB::table('users')->get();
$this->assertCount(1, $users);
$user = $users[0];
$this->assertEquals('John Doe', $user->name);
$this->assertIsArray($user->tags);
}
#[TestWith([true])]
#[TestWith([false])]
public function testInsertWithCustomId(bool $renameEmbeddedIdField)
{
$connection = DB::connection('mongodb');
$connection->setRenameEmbeddedIdField($renameEmbeddedIdField);
$data = ['id' => 'abcdef', 'name' => 'John Doe'];
DB::table('users')->insert($data);
$user = User::find('abcdef');
$this->assertInstanceOf(User::class, $user);
$this->assertSame('abcdef', $user->id);
}
public function testInsertGetId()
{
$id = DB::table('users')->insertGetId(['name' => 'John Doe']);
$this->assertInstanceOf(ObjectId::class, $id);
}
public function testBatchInsert()
{
DB::table('users')->insert([
[
'tags' => ['tag1', 'tag2'],
'name' => 'Jane Doe',
],
[
'tags' => ['tag3'],
'name' => 'John Doe',
],
]);
$users = DB::table('users')->get();
$this->assertCount(2, $users);
$this->assertIsArray($users[0]->tags);
}
public function testFind()
{
$id = DB::table('users')->insertGetId(['name' => 'John Doe']);
$user = DB::table('users')->find($id);
$this->assertEquals('John Doe', $user->name);
}
public function testFindWithTimeout()
{
$id = DB::table('users')->insertGetId(['name' => 'John Doe']);
$subscriber = new class implements CommandSubscriber {
public function commandStarted(CommandStartedEvent $event): void
{
if ($event->getCommandName() !== 'find') {
return;
}
// Expect the timeout to be converted to milliseconds
Assert::assertSame(1000, $event->getCommand()->maxTimeMS);
}
public function commandFailed(CommandFailedEvent $event): void
{
}
public function commandSucceeded(CommandSucceededEvent $event): void
{
}
};
DB::getMongoClient()->getManager()->addSubscriber($subscriber);
try {
DB::table('users')->timeout(1)->find($id);
} finally {
DB::getMongoClient()->getManager()->removeSubscriber($subscriber);
}
}
public function testFindNull()
{
$user = DB::table('users')->find(null);
$this->assertNull($user);
}
public function testCount()
{
DB::table('users')->insert([
['name' => 'Jane Doe'],
['name' => 'John Doe'],
]);
$this->assertEquals(2, DB::table('users')->count());
}
public function testUpdate()
{
DB::table('users')->insert([
['name' => 'Jane Doe', 'age' => 20],
['name' => 'John Doe', 'age' => 21],
]);
DB::table('users')->where('name', 'John Doe')->update(['age' => 100]);
$john = DB::table('users')->where('name', 'John Doe')->first();
$jane = DB::table('users')->where('name', 'Jane Doe')->first();
$this->assertEquals(100, $john->age);
$this->assertEquals(20, $jane->age);
}
public function testUpdateOperators()
{
DB::table('users')->insert([
['name' => 'Jane Doe', 'age' => 20],
['name' => 'John Doe', 'age' => 19],
]);
DB::table('users')->where('name', 'John Doe')->update(
[
'$unset' => ['age' => 1],
'ageless' => true,
],
);
DB::table('users')->where('name', 'Jane Doe')->update(
[
'$inc' => ['age' => 1],
'$set' => ['pronoun' => 'she'],
'ageless' => false,
],
);
$john = DB::table('users')->where('name', 'John Doe')->first();
$jane = DB::table('users')->where('name', 'Jane Doe')->first();
$this->assertObjectNotHasProperty('age', $john);
$this->assertTrue($john->ageless);
$this->assertEquals(21, $jane->age);
$this->assertEquals('she', $jane->pronoun);
$this->assertFalse($jane->ageless);
}
public function testDelete()
{
DB::table('users')->insert([
['name' => 'Jane Doe', 'age' => 20],
['name' => 'John Doe', 'age' => 25],
]);
DB::table('users')->where('age', '<', 10)->delete();
$this->assertEquals(2, DB::table('users')->count());
DB::table('users')->where('age', '<', 25)->delete();
$this->assertEquals(1, DB::table('users')->count());
}
public function testTruncate()
{
DB::table('users')->insert(['name' => 'John Doe']);
DB::table('users')->insert(['name' => 'John Doe']);
$this->assertEquals(2, DB::table('users')->count());
$result = DB::table('users')->truncate();
$this->assertTrue($result);
$this->assertEquals(0, DB::table('users')->count());
}
public function testSubKey()
{
DB::table('users')->insert([
[
'name' => 'John Doe',
'address' => ['country' => 'Belgium', 'city' => 'Ghent'],
],
[
'name' => 'Jane Doe',
'address' => ['country' => 'France', 'city' => 'Paris'],
],
]);
$users = DB::table('users')->where('address.country', 'Belgium')->get();
$this->assertCount(1, $users);
$this->assertEquals('John Doe', $users[0]->name);
}
public function testInArray()
{
DB::table('items')->insert([
[
'tags' => ['tag1', 'tag2', 'tag3', 'tag4'],
],
[
'tags' => ['tag2'],
],
]);
$items = DB::table('items')->where('tags', 'tag2')->get();
$this->assertCount(2, $items);
$items = DB::table('items')->where('tags', 'tag1')->get();
$this->assertCount(1, $items);
}
public function testRaw()
{
DB::table('users')->insert([
['name' => 'Jane Doe', 'age' => 20],
['name' => 'John Doe', 'age' => 25],
]);
$cursor = DB::table('users')->raw(function ($collection) {
return $collection->find(['age' => 20]);
});
$this->assertInstanceOf(Cursor::class, $cursor);
$this->assertCount(1, $cursor->toArray());
$collection = DB::table('users')->raw();
$this->assertInstanceOf(Collection::class, $collection);
$collection = User::raw();
$this->assertInstanceOf(Collection::class, $collection);
$results = DB::table('users')->whereRaw(['age' => 20])->get();
$this->assertCount(1, $results);
$this->assertEquals('Jane Doe', $results[0]->name);
}
public function testRawResultRenameId()
{
$connection = DB::connection('mongodb');
self::assertInstanceOf(Connection::class, $connection);
$date = Carbon::createFromDate(1986, 12, 31)->setTime(12, 0, 0);
User::insert([
['id' => 1, 'name' => 'Jane Doe', 'address' => ['id' => 11, 'city' => 'Ghent'], 'birthday' => $date],
['id' => 2, 'name' => 'John Doe', 'address' => ['id' => 12, 'city' => 'Brussels'], 'birthday' => $date],
]);
// Using raw database query, result is not altered
$results = $connection->table('users')->raw(fn (Collection $collection) => $collection->find([]));
self::assertInstanceOf(CursorInterface::class, $results);
$results = $results->toArray();
self::assertCount(2, $results);
self::assertObjectHasProperty('_id', $results[0]);
self::assertObjectNotHasProperty('id', $results[0]);
self::assertSame(1, $results[0]->_id);
self::assertObjectHasProperty('_id', $results[0]->address);
self::assertObjectNotHasProperty('id', $results[0]->address);
self::assertSame(11, $results[0]->address->_id);
self::assertInstanceOf(UTCDateTime::class, $results[0]->birthday);
// Using Eloquent query, result is transformed
self::assertTrue($connection->getRenameEmbeddedIdField());
$results = User::raw(fn (Collection $collection) => $collection->find([]));
self::assertInstanceOf(LaravelCollection::class, $results);
self::assertCount(2, $results);
$attributes = $results->first()->getAttributes();
self::assertArrayHasKey('id', $attributes);
self::assertArrayNotHasKey('_id', $attributes);
self::assertSame(1, $attributes['id']);
self::assertArrayHasKey('id', $attributes['address']);
self::assertArrayNotHasKey('_id', $attributes['address']);
self::assertSame(11, $attributes['address']['id']);
self::assertEquals($date, $attributes['birthday']);
// Single result
$result = User::raw(fn (Collection $collection) => $collection->findOne([], ['typeMap' => ['root' => 'object', 'document' => 'array']]));
self::assertInstanceOf(User::class, $result);
$attributes = $result->getAttributes();
self::assertArrayHasKey('id', $attributes);
self::assertArrayNotHasKey('_id', $attributes);
self::assertSame(1, $attributes['id']);
self::assertArrayHasKey('id', $attributes['address']);
self::assertArrayNotHasKey('_id', $attributes['address']);
self::assertSame(11, $attributes['address']['id']);
// Change the renameEmbeddedIdField option
$connection->setRenameEmbeddedIdField(false);
$results = User::raw(fn (Collection $collection) => $collection->find([]));
self::assertInstanceOf(LaravelCollection::class, $results);
self::assertCount(2, $results);
$attributes = $results->first()->getAttributes();
self::assertArrayHasKey('id', $attributes);
self::assertArrayNotHasKey('_id', $attributes);
self::assertSame(1, $attributes['id']);
self::assertArrayHasKey('_id', $attributes['address']);
self::assertArrayNotHasKey('id', $attributes['address']);
self::assertSame(11, $attributes['address']['_id']);
// Single result
$result = User::raw(fn (Collection $collection) => $collection->findOne([]));
self::assertInstanceOf(User::class, $result);
$attributes = $result->getAttributes();
self::assertArrayHasKey('id', $attributes);
self::assertArrayNotHasKey('_id', $attributes);
self::assertSame(1, $attributes['id']);
self::assertArrayHasKey('_id', $attributes['address']);
self::assertArrayNotHasKey('id', $attributes['address']);
self::assertSame(11, $attributes['address']['_id']);
}
public function testPush()
{
$id = DB::table('users')->insertGetId([
'name' => 'John Doe',
'tags' => [],
'messages' => [],
]);
DB::table('users')->where('id', $id)->push('tags', 'tag1');
$user = DB::table('users')->find($id);
$this->assertIsArray($user->tags);
$this->assertCount(1, $user->tags);
$this->assertEquals('tag1', $user->tags[0]);
DB::table('users')->where('id', $id)->push('tags', 'tag2');
$user = DB::table('users')->find($id);
$this->assertCount(2, $user->tags);
$this->assertEquals('tag2', $user->tags[1]);
// Add duplicate
DB::table('users')->where('id', $id)->push('tags', 'tag2');
$user = DB::table('users')->find($id);
$this->assertCount(3, $user->tags);
// Add unique
DB::table('users')->where('id', $id)->push('tags', 'tag1', true);
$user = DB::table('users')->find($id);
$this->assertCount(3, $user->tags);
$message = ['from' => 'Jane', 'body' => 'Hi John'];
DB::table('users')->where('id', $id)->push('messages', $message);
$user = DB::table('users')->find($id);
$this->assertIsArray($user->messages);
$this->assertCount(1, $user->messages);
$this->assertEquals($message, $user->messages[0]);
// Raw
DB::table('users')->where('id', $id)->push([
'tags' => 'tag3',
'messages' => ['from' => 'Mark', 'body' => 'Hi John'],
]);
$user = DB::table('users')->find($id);
$this->assertCount(4, $user->tags);
$this->assertCount(2, $user->messages);
DB::table('users')->where('id', $id)->push([
'messages' => [
'date' => new DateTime(),
'body' => 'Hi John',
],
]);
$user = DB::table('users')->find($id);
$this->assertCount(3, $user->messages);
}
public function testPushRefuses2ndArgumentWhen1stIsAnArray()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('2nd argument of MongoDB\Laravel\Query\Builder::push() must be "null" when 1st argument is an array. Got "string" instead.');
DB::table('users')->push(['tags' => 'tag1'], 'tag2');
}
public function testPull()
{
$message1 = ['from' => 'Jane', 'body' => 'Hi John'];
$message2 = ['from' => 'Mark', 'body' => 'Hi John'];
$id = DB::table('users')->insertGetId([
'name' => 'John Doe',
'tags' => ['tag1', 'tag2', 'tag3', 'tag4'],
'messages' => [$message1, $message2],
]);
DB::table('users')->where('id', $id)->pull('tags', 'tag3');
$user = DB::table('users')->find($id);
$this->assertIsArray($user->tags);
$this->assertCount(3, $user->tags);
$this->assertEquals('tag4', $user->tags[2]);
DB::table('users')->where('id', $id)->pull('messages', $message1);
$user = DB::table('users')->find($id);
$this->assertIsArray($user->messages);
$this->assertCount(1, $user->messages);
// Raw
DB::table('users')->where('id', $id)->pull(['tags' => 'tag2', 'messages' => $message2]);
$user = DB::table('users')->find($id);
$this->assertCount(2, $user->tags);
$this->assertCount(0, $user->messages);
}
public function testDistinct()
{
DB::table('items')->insert([
['name' => 'knife', 'type' => 'sharp'],
['name' => 'fork', 'type' => 'sharp'],
['name' => 'spoon', 'type' => 'round'],
['name' => 'spoon', 'type' => 'round'],
]);
$items = DB::table('items')->distinct('name')->get()->toArray();
sort($items);
$this->assertCount(3, $items);
$this->assertEquals(['fork', 'knife', 'spoon'], $items);
$types = DB::table('items')->distinct('type')->get()->toArray();
sort($types);
$this->assertCount(2, $types);
$this->assertEquals(['round', 'sharp'], $types);
}
public function testCustomId()
{
$tags = [['id' => 'sharp', 'name' => 'Sharp']];
DB::table('items')->insert([
['id' => 'knife', 'type' => 'sharp', 'amount' => 34, 'tags' => $tags],
['id' => 'fork', 'type' => 'sharp', 'amount' => 20, 'tags' => $tags],
['id' => 'spoon', 'type' => 'round', 'amount' => 3],
]);
$item = DB::table('items')->find('knife');
$this->assertEquals('knife', $item->id);
$this->assertObjectNotHasProperty('_id', $item);
$this->assertEquals('sharp', $item->tags[0]['id']);
$this->assertArrayNotHasKey('_id', $item->tags[0]);
$item = DB::table('items')->where('id', 'fork')->first();
$this->assertEquals('fork', $item->id);
$item = DB::table('items')->where('_id', 'fork')->first();
$this->assertEquals('fork', $item->id);
// tags.id is translated into tags._id in query
$items = DB::table('items')->whereIn('tags.id', ['sharp'])->get();
$this->assertCount(2, $items);
// Ensure the field _id is stored in the database
$items = DB::table('items')->whereIn('tags._id', ['sharp'])->get();
$this->assertCount(2, $items);
DB::table('users')->insert([
['id' => 1, 'name' => 'Jane Doe'],
['id' => 2, 'name' => 'John Doe'],
]);
$item = DB::table('users')->find(1);
$this->assertEquals(1, $item->id);
$this->assertObjectNotHasProperty('_id', $item);
}
public function testTake()
{
DB::table('items')->insert([
['name' => 'knife', 'type' => 'sharp', 'amount' => 34],
['name' => 'fork', 'type' => 'sharp', 'amount' => 20],
['name' => 'spoon', 'type' => 'round', 'amount' => 3],
['name' => 'spoon', 'type' => 'round', 'amount' => 14],
]);
$items = DB::table('items')->orderBy('name')->take(2)->get();
$this->assertCount(2, $items);
$this->assertEquals('fork', $items[0]->name);
}
public function testSkip()
{
DB::table('items')->insert([
['name' => 'knife', 'type' => 'sharp', 'amount' => 34],
['name' => 'fork', 'type' => 'sharp', 'amount' => 20],
['name' => 'spoon', 'type' => 'round', 'amount' => 3],
['name' => 'spoon', 'type' => 'round', 'amount' => 14],
]);
$items = DB::table('items')->orderBy('name')->skip(2)->get();
$this->assertCount(2, $items);
$this->assertEquals('spoon', $items[0]->name);
}
public function testPluck()
{
DB::table('users')->insert([
['name' => 'Jane Doe', 'age' => 20],
['name' => 'John Doe', 'age' => 25],
]);
$age = DB::table('users')->where('name', 'John Doe')->pluck('age')->toArray();
$this->assertEquals([25], $age);
}
public function testPluckObjectId()
{
$id = new ObjectId();
DB::table('users')->insert([
['id' => $id, 'name' => 'Jane Doe'],
]);
$names = DB::table('users')->pluck('name', 'id')->toArray();
$this->assertEquals([(string) $id => 'Jane Doe'], $names);
}
public function testList()
{
DB::table('items')->insert([
['name' => 'knife', 'type' => 'sharp', 'amount' => 34],
['name' => 'fork', 'type' => 'sharp', 'amount' => 20],
['name' => 'spoon', 'type' => 'round', 'amount' => 3],
['name' => 'spoon', 'type' => 'round', 'amount' => 14],
]);
$list = DB::table('items')->pluck('name')->toArray();
sort($list);
$this->assertCount(4, $list);
$this->assertEquals(['fork', 'knife', 'spoon', 'spoon'], $list);
$list = DB::table('items')->pluck('type', 'name')->toArray();
$this->assertCount(3, $list);
$this->assertEquals(['knife' => 'sharp', 'fork' => 'sharp', 'spoon' => 'round'], $list);
$list = DB::table('items')->pluck('name', 'id')->toArray();
$this->assertCount(4, $list);
$this->assertEquals(24, strlen(key($list)));
}
public function testAggregate()
{
DB::table('items')->insert([
['name' => 'knife', 'type' => 'sharp', 'amount' => 34],
['name' => 'fork', 'type' => 'sharp', 'amount' => 20],
['name' => 'spoon', 'type' => 'round', 'amount' => 3],
['name' => 'spoon', 'type' => 'round', 'amount' => 14],
]);
$this->assertEquals(71, DB::table('items')->sum('amount'));
$this->assertEquals(4, DB::table('items')->count('amount'));
$this->assertEquals(3, DB::table('items')->min('amount'));
$this->assertEquals(34, DB::table('items')->max('amount'));
$this->assertEquals(17.75, DB::table('items')->avg('amount'));
$this->assertEquals(2, DB::table('items')->where('name', 'spoon')->count('amount'));
$this->assertEquals(14, DB::table('items')->where('name', 'spoon')->max('amount'));
}
public function testSubdocumentAggregate()
{
DB::table('items')->insert([
['name' => 'knife', 'amount' => ['hidden' => 10, 'found' => 3]],
['name' => 'fork', 'amount' => ['hidden' => 35, 'found' => 12]],
['name' => 'spoon', 'amount' => ['hidden' => 14, 'found' => 21]],
['name' => 'spoon', 'amount' => ['hidden' => 6, 'found' => 4]],
]);
$this->assertEquals(65, DB::table('items')->sum('amount.hidden'));
$this->assertEquals(4, DB::table('items')->count('amount.hidden'));
$this->assertEquals(6, DB::table('items')->min('amount.hidden'));
$this->assertEquals(35, DB::table('items')->max('amount.hidden'));
$this->assertEquals(16.25, DB::table('items')->avg('amount.hidden'));
}
public function testSubdocumentArrayAggregate()
{
DB::table('items')->insert([
['name' => 'knife', 'amount' => [['hidden' => 10, 'found' => 3], ['hidden' => 5, 'found' => 2]]],
[
'name' => 'fork',
'amount' => [
['hidden' => 35, 'found' => 12],
['hidden' => 7, 'found' => 17],
['hidden' => 1, 'found' => 19],
],
],
['name' => 'spoon', 'amount' => [['hidden' => 14, 'found' => 21]]],
['name' => 'teaspoon', 'amount' => []],
]);
$this->assertEquals(72, DB::table('items')->sum('amount.*.hidden'));
$this->assertEquals(6, DB::table('items')->count('amount.*.hidden'));
$this->assertEquals(1, DB::table('items')->min('amount.*.hidden'));
$this->assertEquals(35, DB::table('items')->max('amount.*.hidden'));
$this->assertEquals(12, DB::table('items')->avg('amount.*.hidden'));
}
public function testAggregateGroupBy()
{
DB::table('users')->insert([
['name' => 'John Doe', 'role' => 'admin', 'score' => 1, 'active' => true],
['name' => 'Jane Doe', 'role' => 'admin', 'score' => 2, 'active' => true],
['name' => 'Robert Roe', 'role' => 'user', 'score' => 4],
]);
$results = DB::table('users')->groupBy('role')->orderBy('role')->aggregateByGroup('count');
$this->assertInstanceOf(LaravelCollection::class, $results);
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 2], (object) ['role' => 'user', 'aggregate' => 1]], $results->toArray());
$results = DB::table('users')->groupBy('role')->orderBy('role')->aggregateByGroup('count', ['active']);
$this->assertInstanceOf(LaravelCollection::class, $results);
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 1], (object) ['role' => 'user', 'aggregate' => 0]], $results->toArray());
$results = DB::table('users')->groupBy('role')->orderBy('role')->aggregateByGroup('max', ['score']);
$this->assertInstanceOf(LaravelCollection::class, $results);
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 2], (object) ['role' => 'user', 'aggregate' => 4]], $results->toArray());
if (! method_exists(Builder::class, 'countByGroup')) {
$this->markTestSkipped('*byGroup functions require Laravel v11.38+');
}
$results = DB::table('users')->groupBy('role')->orderBy('role')->countByGroup();
$this->assertInstanceOf(LaravelCollection::class, $results);
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 2], (object) ['role' => 'user', 'aggregate' => 1]], $results->toArray());
$results = DB::table('users')->groupBy('role')->orderBy('role')->maxByGroup('score');
$this->assertInstanceOf(LaravelCollection::class, $results);
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 2], (object) ['role' => 'user', 'aggregate' => 4]], $results->toArray());
$results = DB::table('users')->groupBy('role')->orderBy('role')->minByGroup('score');
$this->assertInstanceOf(LaravelCollection::class, $results);
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 1], (object) ['role' => 'user', 'aggregate' => 4]], $results->toArray());
$results = DB::table('users')->groupBy('role')->orderBy('role')->sumByGroup('score');
$this->assertInstanceOf(LaravelCollection::class, $results);
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 3], (object) ['role' => 'user', 'aggregate' => 4]], $results->toArray());
$results = DB::table('users')->groupBy('role')->orderBy('role')->avgByGroup('score');
$this->assertInstanceOf(LaravelCollection::class, $results);
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 1.5], (object) ['role' => 'user', 'aggregate' => 4]], $results->toArray());
}
public function testAggregateByGroupException(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Aggregating by group requires zero or one columns.');
DB::table('users')->aggregateByGroup('max', ['foo', 'bar']);
}
public function testUpdateWithUpsert()
{
DB::table('items')->where('name', 'knife')
->update(
['amount' => 1],
['upsert' => true],
);
$this->assertEquals(1, DB::table('items')->count());
Item::where('name', 'spoon')
->update(
['amount' => 1],
['upsert' => true],
);
$this->assertEquals(2, DB::table('items')->count());
}
public function testUpsert()
{
/** @see DatabaseQueryBuilderTest::testUpsertMethod() */
// Insert 2 documents
$result = DB::table('users')->upsert([
['email' => 'foo', 'name' => 'bar'],
['name' => 'bar2', 'email' => 'foo2'],
], 'email', 'name');
$this->assertSame(2, $result);
$this->assertSame(2, DB::table('users')->count());
$this->assertSame('bar', DB::table('users')->where('email', 'foo')->first()->name);
// Update 1 document
$result = DB::table('users')->upsert([
['email' => 'foo', 'name' => 'bar2'],
['name' => 'bar2', 'email' => 'foo2'],
], 'email', 'name');
$this->assertSame(1, $result);
$this->assertSame(2, DB::table('users')->count());
$this->assertSame('bar2', DB::table('users')->where('email', 'foo')->first()->name);
// If no update fields are specified, all fields are updated
// Test single document update
$result = DB::table('users')->upsert(['email' => 'foo', 'name' => 'bar3'], 'email');
$this->assertSame(1, $result);
$this->assertSame(2, DB::table('users')->count());
$this->assertSame('bar3', DB::table('users')->where('email', 'foo')->first()->name);
}
public function testUnset()
{
$id1 = DB::table('users')->insertGetId(['name' => 'John Doe', 'note1' => 'ABC', 'note2' => 'DEF']);
$id2 = DB::table('users')->insertGetId(['name' => 'Jane Doe', 'note1' => 'ABC', 'note2' => 'DEF']);
DB::table('users')->where('name', 'John Doe')->unset('note1');
$user1 = DB::table('users')->find($id1);
$user2 = DB::table('users')->find($id2);
$this->assertObjectNotHasProperty('note1', $user1);
$this->assertObjectHasProperty('note2', $user1);
$this->assertObjectHasProperty('note1', $user2);
$this->assertObjectHasProperty('note2', $user2);
DB::table('users')->where('name', 'Jane Doe')->unset(['note1', 'note2']);
$user2 = DB::table('users')->find($id2);
$this->assertObjectNotHasProperty('note1', $user2);
$this->assertObjectNotHasProperty('note2', $user2);
}
public function testUpdateSubdocument()
{
$id = DB::table('users')->insertGetId(['name' => 'John Doe', 'address' => ['country' => 'Belgium']]);
DB::table('users')->where('id', $id)->update(['address.country' => 'England']);
$check = DB::table('users')->find($id);
$this->assertEquals('England', $check->address['country']);
}
public function testDates()
{
DB::table('users')->insert([
['name' => 'John Doe', 'birthday' => Date::parse('1980-01-01 00:00:00')],
['name' => 'Robert Roe', 'birthday' => Date::parse('1982-01-01 00:00:00')],
['name' => 'Mark Moe', 'birthday' => Date::parse('1983-01-01 00:00:00.1')],
['name' => 'Frank White', 'birthday' => Date::parse('1975-01-01 12:12:12.1')],
]);
$user = DB::table('users')
->where('birthday', Date::parse('1980-01-01 00:00:00'))
->first();
$this->assertEquals('John Doe', $user->name);
$user = DB::table('users')
->where('birthday', Date::parse('1975-01-01 12:12:12.1'))
->first();
$this->assertEquals('Frank White', $user->name);
$this->assertInstanceOf(Carbon::class, $user->birthday);
$this->assertSame('1975-01-01 12:12:12.100000', $user->birthday->format('Y-m-d H:i:s.u'));
$user = DB::table('users')->where('birthday', '=', new DateTime('1980-01-01 00:00:00'))->first();
$this->assertEquals('John Doe', $user->name);
$this->assertInstanceOf(Carbon::class, $user->birthday);
$this->assertSame('1980-01-01 00:00:00.000000', $user->birthday->format('Y-m-d H:i:s.u'));
$start = new UTCDateTime(new DateTime('1950-01-01 00:00:00'));
$stop = new UTCDateTime(new DateTime('1981-01-01 00:00:00'));
$users = DB::table('users')->whereBetween('birthday', [$start, $stop])->get();
$this->assertCount(2, $users);
}
public function testImmutableDates()
{
DB::table('users')->insert([
['name' => 'John Doe', 'birthday' => new UTCDateTime(Date::parse('1980-01-01 00:00:00'))],
['name' => 'Robert Roe', 'birthday' => new UTCDateTime(Date::parse('1982-01-01 00:00:00'))],
]);
$users = DB::table('users')->where('birthday', '=', new DateTimeImmutable('1980-01-01 00:00:00'))->get();
$this->assertCount(1, $users);
$users = DB::table('users')->where('birthday', new DateTimeImmutable('1980-01-01 00:00:00'))->get();
$this->assertCount(1, $users);
$users = DB::table('users')->whereIn('birthday', [
new DateTimeImmutable('1980-01-01 00:00:00'),
new DateTimeImmutable('1982-01-01 00:00:00'),
])->get();
$this->assertCount(2, $users);
$users = DB::table('users')->whereBetween('birthday', [
new DateTimeImmutable('1979-01-01 00:00:00'),
new DateTimeImmutable('1983-01-01 00:00:00'),
])->get();
$this->assertCount(2, $users);
}
public function testOperators()
{
DB::table('users')->insert([
['name' => 'John Doe', 'age' => 30],
['name' => 'Jane Doe'],
['name' => 'Robert Roe', 'age' => 'thirty-one'],
]);
$results = DB::table('users')->where('age', 'exists', true)->get();
$this->assertCount(2, $results);
$resultsNames = [$results[0]->name, $results[1]->name];
$this->assertContains('John Doe', $resultsNames);
$this->assertContains('Robert Roe', $resultsNames);
$results = DB::table('users')->where('age', 'exists', false)->get();
$this->assertCount(1, $results);
$this->assertEquals('Jane Doe', $results[0]->name);
$results = DB::table('users')->where('age', 'type', 2)->get();
$this->assertCount(1, $results);
$this->assertEquals('Robert Roe', $results[0]->name);
$results = DB::table('users')->where('age', 'mod', [15, 0])->get();
$this->assertCount(1, $results);
$this->assertEquals('John Doe', $results[0]->name);
$results = DB::table('users')->where('age', 'mod', [29, 1])->get();
$this->assertCount(1, $results);
$this->assertEquals('John Doe', $results[0]->name);
$results = DB::table('users')->where('age', 'mod', [14, 0])->get();
$this->assertCount(0, $results);
DB::table('items')->insert([
['name' => 'fork', 'tags' => ['sharp', 'pointy']],
['name' => 'spork', 'tags' => ['sharp', 'pointy', 'round', 'bowl']],
['name' => 'spoon', 'tags' => ['round', 'bowl']],
]);
$results = DB::table('items')->where('tags', 'all', ['sharp', 'pointy'])->get();
$this->assertCount(2, $results);
$results = DB::table('items')->where('tags', 'all', ['sharp', 'round'])->get();
$this->assertCount(1, $results);
$results = DB::table('items')->where('tags', 'size', 2)->get();
$this->assertCount(2, $results);
$results = DB::table('items')->where('tags', '$size', 2)->get();
$this->assertCount(2, $results);
$results = DB::table('items')->where('tags', 'size', 3)->get();
$this->assertCount(0, $results);
$results = DB::table('items')->where('tags', 'size', 4)->get();
$this->assertCount(1, $results);
$regex = new Regex('.*doe', 'i');
$results = DB::table('users')->where('name', 'regex', $regex)->get();
$this->assertCount(2, $results);
$regex = new Regex('.*doe', 'i');
$results = DB::table('users')->where('name', 'regexp', $regex)->get();
$this->assertCount(2, $results);
$results = DB::table('users')->where('name', 'REGEX', $regex)->get();
$this->assertCount(2, $results);
$results = DB::table('users')->where('name', 'regexp', '/.*doe/i')->get();
$this->assertCount(2, $results);
$results = DB::table('users')->where('name', 'not regexp', '/.*doe/i')->get();
$this->assertCount(1, $results);
DB::table('users')->insert([
[
'name' => 'John Doe',
'addresses' => [
['city' => 'Ghent'],
['city' => 'Paris'],
],
],
[
'name' => 'Jane Doe',
'addresses' => [
['city' => 'Brussels'],
['city' => 'Paris'],
],
],
]);
$users = DB::table('users')->where('addresses', 'elemMatch', ['city' => 'Brussels'])->get();
$this->assertCount(1, $users);
$this->assertEquals('Jane Doe', $users[0]->name);
}
public function testIncrement()
{
DB::table('users')->insert([
['name' => 'John Doe', 'age' => 30, 'note' => 'adult'],
['name' => 'Jane Doe', 'age' => 10, 'note' => 'minor'],
['name' => 'Robert Roe', 'age' => null],
['name' => 'Mark Moe'],
]);
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(30, $user->age);
DB::table('users')->where('name', 'John Doe')->increment('age');
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(31, $user->age);
DB::table('users')->where('name', 'John Doe')->decrement('age');
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(30, $user->age);
DB::table('users')->where('name', 'John Doe')->increment('age', 5);
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(35, $user->age);
DB::table('users')->where('name', 'John Doe')->decrement('age', 5);
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(30, $user->age);
DB::table('users')->where('name', 'Jane Doe')->increment('age', 10, ['note' => 'adult']);
$user = DB::table('users')->where('name', 'Jane Doe')->first();
$this->assertEquals(20, $user->age);
$this->assertEquals('adult', $user->note);
DB::table('users')->where('name', 'John Doe')->decrement('age', 20, ['note' => 'minor']);
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(10, $user->age);
$this->assertEquals('minor', $user->note);
DB::table('users')->increment('age');
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(11, $user->age);
$user = DB::table('users')->where('name', 'Jane Doe')->first();
$this->assertEquals(21, $user->age);
$user = DB::table('users')->where('name', 'Robert Roe')->first();
$this->assertNull($user->age);
$user = DB::table('users')->where('name', 'Mark Moe')->first();
$this->assertEquals(1, $user->age);
}
public function testMultiplyAndDivide()
{
DB::table('users')->insert([
['name' => 'John Doe', 'salary' => 88000, 'note' => 'senior'],
['name' => 'Jane Doe', 'salary' => 64000, 'note' => 'junior'],
['name' => 'Robert Roe', 'salary' => null],
['name' => 'Mark Moe'],
]);
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(88000, $user->salary);
DB::table('users')->where('name', 'John Doe')->multiply('salary', 1);
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(88000, $user->salary);
DB::table('users')->where('name', 'John Doe')->divide('salary', 1);
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(88000, $user->salary);
DB::table('users')->where('name', 'John Doe')->multiply('salary', 2);
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(176000, $user->salary);
DB::table('users')->where('name', 'John Doe')->divide('salary', 2);
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(88000, $user->salary);
DB::table('users')->where('name', 'Jane Doe')->multiply('salary', 10, ['note' => 'senior']);
$user = DB::table('users')->where('name', 'Jane Doe')->first();
$this->assertEquals(640000, $user->salary);
$this->assertEquals('senior', $user->note);
DB::table('users')->where('name', 'John Doe')->divide('salary', 2, ['note' => 'junior']);
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(44000, $user->salary);
$this->assertEquals('junior', $user->note);
DB::table('users')->multiply('salary', 1);
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(44000, $user->salary);
$user = DB::table('users')->where('name', 'Jane Doe')->first();
$this->assertEquals(640000, $user->salary);
$user = DB::table('users')->where('name', 'Robert Roe')->first();
$this->assertNull($user->salary);
$user = DB::table('users')->where('name', 'Mark Moe')->first();
$this->assertFalse(isset($user->salary));
}
public function testProjections()
{
DB::table('items')->insert([
['name' => 'fork', 'tags' => ['sharp', 'pointy']],
['name' => 'spork', 'tags' => ['sharp', 'pointy', 'round', 'bowl']],
['name' => 'spoon', 'tags' => ['round', 'bowl']],
]);
$results = DB::table('items')->project(['tags' => ['$slice' => 1]])->get();
foreach ($results as $result) {
$this->assertEquals(1, count($result->tags));
}
}
public function testValue()
{
DB::table('books')->insert([
['title' => 'Moby-Dick', 'author' => ['first_name' => 'Herman', 'last_name' => 'Melville']],
]);
$this->assertEquals('Moby-Dick', DB::table('books')->value('title'));
$this->assertEquals(['first_name' => 'Herman', 'last_name' => 'Melville'], DB::table('books')
->value('author'));
$this->assertEquals('Herman', DB::table('books')->value('author.first_name'));
$this->assertEquals('Melville', DB::table('books')->value('author.last_name'));
}
public function testHintOptions()
{
DB::table('items')->insert([
['name' => 'fork', 'tags' => ['sharp', 'pointy']],
['name' => 'spork', 'tags' => ['sharp', 'pointy', 'round', 'bowl']],
['name' => 'spoon', 'tags' => ['round', 'bowl']],
]);
$results = DB::table('items')->hint(['$natural' => -1])->get();
$this->assertEquals('spoon', $results[0]->name);
$this->assertEquals('spork', $results[1]->name);
$this->assertEquals('fork', $results[2]->name);
$results = DB::table('items')->hint(['$natural' => 1])->get();
$this->assertEquals('spoon', $results[2]->name);
$this->assertEquals('spork', $results[1]->name);
$this->assertEquals('fork', $results[0]->name);
}
public function testCursor()
{
$data = [
['name' => 'fork', 'tags' => ['sharp', 'pointy']],
['name' => 'spork', 'tags' => ['sharp', 'pointy', 'round', 'bowl']],
['name' => 'spoon', 'tags' => ['round', 'bowl']],
];
DB::table('items')->insert($data);
$results = DB::table('items')->orderBy('id', 'asc')->cursor();
$this->assertInstanceOf(LazyCollection::class, $results);
foreach ($results as $i => $result) {
$this->assertEquals($data[$i]['name'], $result->name);
}
}
public function testStringableColumn()
{
DB::table('users')->insert([
['name' => 'Jane Doe', 'age' => 36, 'birthday' => new UTCDateTime(new DateTime('1987-01-01 00:00:00'))],
['name' => 'John Doe', 'age' => 28, 'birthday' => new UTCDateTime(new DateTime('1995-01-01 00:00:00'))],
]);
$nameColumn = Str::of('name');
$this->assertInstanceOf(Stringable::class, $nameColumn, 'Ensure we are testing the feature with a Stringable instance');
$user = DB::table('users')->where($nameColumn, 'John Doe')->first();
$this->assertEquals('John Doe', $user->name);
// Test this other document to be sure this is not a random success to data order
$user = DB::table('users')->where($nameColumn, 'Jane Doe')->orderBy('natural')->first();
$this->assertEquals('Jane Doe', $user->name);
// With an operator
$user = DB::table('users')->where($nameColumn, '!=', 'Jane Doe')->first();
$this->assertEquals('John Doe', $user->name);
// whereIn and whereNotIn
$user = DB::table('users')->whereIn($nameColumn, ['John Doe'])->first();
$this->assertEquals('John Doe', $user->name);
$user = DB::table('users')->whereNotIn($nameColumn, ['John Doe'])->first();
$this->assertEquals('Jane Doe', $user->name);
$ageColumn = Str::of('age');
// whereBetween and whereNotBetween
$user = DB::table('users')->whereBetween($ageColumn, [30, 40])->first();
$this->assertEquals('Jane Doe', $user->name);
// whereBetween and whereNotBetween
$user = DB::table('users')->whereNotBetween($ageColumn, [30, 40])->first();
$this->assertEquals('John Doe', $user->name);
$birthdayColumn = Str::of('birthday');
// whereDate
$user = DB::table('users')->whereDate($birthdayColumn, '1995-01-01')->first();
$this->assertEquals('John Doe', $user->name);
$user = DB::table('users')->whereDate($birthdayColumn, '<', '1990-01-01')
->orderBy($birthdayColumn, 'desc')->first();
$this->assertEquals('Jane Doe', $user->name);
$user = DB::table('users')->whereDate($birthdayColumn, '>', '1990-01-01')
->orderBy($birthdayColumn, 'asc')->first();
$this->assertEquals('John Doe', $user->name);
$user = DB::table('users')->whereDate($birthdayColumn, '!=', '1987-01-01')->first();
$this->assertEquals('John Doe', $user->name);
// increment
DB::table('users')->where($ageColumn, 28)->increment($ageColumn, 1);
$user = DB::table('users')->where($ageColumn, 29)->first();
$this->assertEquals('John Doe', $user->name);
}
public function testIncrementEach()
{
DB::table('users')->insert([
['name' => 'John Doe', 'age' => 30, 'note' => 5],
['name' => 'Jane Doe', 'age' => 10, 'note' => 6],
['name' => 'Robert Roe', 'age' => null],
]);
DB::table('users')->incrementEach([
'age' => 1,
'note' => 2,
]);
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(31, $user->age);
$this->assertEquals(7, $user->note);
$user = DB::table('users')->where('name', 'Jane Doe')->first();
$this->assertEquals(11, $user->age);
$this->assertEquals(8, $user->note);
$user = DB::table('users')->where('name', 'Robert Roe')->first();
$this->assertSame(1, $user->age);
$this->assertSame(2, $user->note);
DB::table('users')->where('name', 'Jane Doe')->incrementEach([
'age' => 1,
'note' => 2,
], ['extra' => 'foo']);
$user = DB::table('users')->where('name', 'Jane Doe')->first();
$this->assertEquals(12, $user->age);
$this->assertEquals(10, $user->note);
$this->assertEquals('foo', $user->extra);
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(31, $user->age);
$this->assertEquals(7, $user->note);
$this->assertObjectNotHasProperty('extra', $user);
DB::table('users')->decrementEach([
'age' => 1,
'note' => 2,
], ['extra' => 'foo']);
$user = DB::table('users')->where('name', 'John Doe')->first();
$this->assertEquals(30, $user->age);
$this->assertEquals(5, $user->note);
$this->assertEquals('foo', $user->extra);
}
#[TestWith(['id', 'id'])]
#[TestWith(['id', '_id'])]
#[TestWith(['_id', 'id'])]
#[TestWith(['_id', '_id'])]
public function testIdAlias($insertId, $queryId): void
{
DB::table('items')->insert([$insertId => 'abc', 'name' => 'Karting']);
$item = DB::table('items')->where($queryId, '=', 'abc')->first();
$this->assertNotNull($item);
$this->assertSame('abc', $item->id);
$this->assertSame('Karting', $item->name);
DB::table('items')->where($insertId, '=', 'abc')->update(['name' => 'Bike']);
$item = DB::table('items')->where($queryId, '=', 'abc')->first();
$this->assertSame('Bike', $item->name);
$result = DB::table('items')->where($queryId, '=', 'abc')->delete();
$this->assertSame(1, $result);
}
}
================================================
FILE: tests/QueryTest.php
================================================
'John Doe', 'age' => 35, 'title' => 'admin']);
User::create(['name' => 'Jane Doe', 'age' => 33, 'title' => 'admin']);
User::create(['name' => 'Harry Hoe', 'age' => 13, 'title' => 'user']);
User::create(['name' => 'Robert Roe', 'age' => 37, 'title' => 'user']);
User::create(['name' => 'Mark Moe', 'age' => 23, 'title' => 'user']);
User::create(['name' => 'Brett Boe', 'age' => 35, 'title' => 'user']);
User::create(['name' => 'Tommy Toe', 'age' => 33, 'title' => 'user']);
User::create(['name' => 'Yvonne Yoe', 'age' => 35, 'title' => 'admin']);
User::create(['name' => 'Error', 'age' => null, 'title' => null]);
Birthday::create(['name' => 'Mark Moe', 'birthday' => new DateTimeImmutable('2020-04-10 10:53:11')]);
Birthday::create(['name' => 'Jane Doe', 'birthday' => new DateTimeImmutable('2021-05-12 10:53:12')]);
Birthday::create(['name' => 'Harry Hoe', 'birthday' => new DateTimeImmutable('2021-05-11 10:53:13')]);
Birthday::create(['name' => 'Robert Doe', 'birthday' => new DateTimeImmutable('2021-05-12 10:53:14')]);
Birthday::create(['name' => 'Mark Moe', 'birthday' => new DateTimeImmutable('2021-05-12 10:53:15')]);
Birthday::create(['name' => 'Mark Moe', 'birthday' => new DateTimeImmutable('2022-05-12 10:53:16')]);
Birthday::create(['name' => 'Boo']);
}
public function tearDown(): void
{
User::truncate();
Scoped::truncate();
Birthday::truncate();
parent::tearDown();
}
public function testWhere(): void
{
$users = User::where('age', 35)->get();
$this->assertCount(3, $users);
$users = User::where('age', '=', 35)->get();
$this->assertCount(3, $users);
$users = User::where('age', '>=', 35)->get();
$this->assertCount(4, $users);
$users = User::where('age', '<=', 18)->get();
$this->assertCount(1, $users);
$users = User::where('age', '!=', 35)->get();
$this->assertCount(6, $users);
$users = User::where('age', '<>', 35)->get();
$this->assertCount(6, $users);
}
public function testAndWhere(): void
{
$users = User::where('age', 35)->where('title', 'admin')->get();
$this->assertCount(2, $users);
$users = User::where('age', '>=', 35)->where('title', 'user')->get();
$this->assertCount(2, $users);
}
public function testRegexp(): void
{
User::create(['name' => 'Simple', 'company' => 'acme']);
User::create(['name' => 'With slash', 'company' => 'oth/er']);
$users = User::where('company', 'regexp', '/^acme$/')->get();
$this->assertCount(1, $users);
$users = User::where('company', 'regexp', '/^ACME$/i')->get();
$this->assertCount(1, $users);
$users = User::where('company', 'regexp', '/^oth\/er$/')->get();
$this->assertCount(1, $users);
}
public function testLike(): void
{
$users = User::where('name', 'like', '%doe')->get();
$this->assertCount(2, $users);
$users = User::where('name', 'like', '%y%')->get();
$this->assertCount(3, $users);
$users = User::where('name', 'LIKE', '%y%')->get();
$this->assertCount(3, $users);
$users = User::where('name', 'like', 't%')->get();
$this->assertCount(1, $users);
$users = User::where('name', 'like', 'j___ doe')->get();
$this->assertCount(2, $users);
$users = User::where('name', 'like', '_oh_ _o_')->get();
$this->assertCount(1, $users);
}
public function testNotLike(): void
{
$users = User::where('name', 'not like', '%doe')->get();
$this->assertCount(7, $users);
$users = User::where('name', 'not like', '%y%')->get();
$this->assertCount(6, $users);
$users = User::where('name', 'not LIKE', '%y%')->get();
$this->assertCount(6, $users);
$users = User::where('name', 'not like', 't%')->get();
$this->assertCount(8, $users);
}
public function testSelect(): void
{
$user = User::where('name', 'John Doe')->select('name')->first();
$this->assertEquals('John Doe', $user->name);
$this->assertNull($user->age);
$this->assertNull($user->title);
$user = User::where('name', 'John Doe')->select('name', 'title')->first();
$this->assertEquals('John Doe', $user->name);
$this->assertEquals('admin', $user->title);
$this->assertNull($user->age);
$user = User::where('name', 'John Doe')->select(['name', 'title'])->get()->first();
$this->assertEquals('John Doe', $user->name);
$this->assertEquals('admin', $user->title);
$this->assertNull($user->age);
$user = User::where('name', 'John Doe')->get(['name'])->first();
$this->assertEquals('John Doe', $user->name);
$this->assertNull($user->age);
}
public function testWhereNot(): void
{
// implicit equality operator
$users = User::whereNot('title', 'admin')->get();
$this->assertCount(6, $users);
// nested query
$users = User::whereNot(fn (Builder $builder) => $builder->where('title', 'admin'))->get();
$this->assertCount(6, $users);
// double negation
$users = User::whereNot('title', '!=', 'admin')->get();
$this->assertCount(3, $users);
// nested negation
$users = User::whereNot(fn (Builder $builder) => $builder
->whereNot('title', 'admin'))->get();
$this->assertCount(3, $users);
// explicit equality operator
$users = User::whereNot('title', '=', 'admin')->get();
$this->assertCount(6, $users);
// custom query operator
$users = User::whereNot('title', ['$in' => ['admin']])->get();
$this->assertCount(6, $users);
// regex
$users = User::whereNot('title', new Regex('^admin$'))->get();
$this->assertCount(6, $users);
// equals null
$users = User::whereNot('title', null)->get();
$this->assertCount(8, $users);
// nested $or
$users = User::whereNot(fn (Builder $builder) => $builder
->where('title', 'admin')
->orWhere('age', 35))->get();
$this->assertCount(5, $users);
}
public function testOrWhere(): void
{
$users = User::where('age', 13)->orWhere('title', 'admin')->get();
$this->assertCount(4, $users);
$users = User::where('age', 13)->orWhere('age', 23)->get();
$this->assertCount(2, $users);
}
public function testBetween(): void
{
$users = User::whereBetween('age', [0, 25])->get();
$this->assertCount(2, $users);
$users = User::whereBetween('age', [13, 23])->get();
$this->assertCount(2, $users);
// testing whereNotBetween for version 4.1
$users = User::whereBetween('age', [0, 25], 'and', true)->get();
$this->assertCount(6, $users);
}
public function testIn(): void
{
$users = User::whereIn('age', [13, 23])->get();
$this->assertCount(2, $users);
$users = User::whereIn('age', [33, 35, 13])->get();
$this->assertCount(6, $users);
$users = User::whereNotIn('age', [33, 35])->get();
$this->assertCount(4, $users);
$users = User::whereNotNull('age')
->whereNotIn('age', [33, 35])->get();
$this->assertCount(3, $users);
}
public function testWhereNull(): void
{
$users = User::whereNull('age')->get();
$this->assertCount(1, $users);
}
public function testWhereNotNull(): void
{
$users = User::whereNotNull('age')->get();
$this->assertCount(8, $users);
}
public function testWhereDate(): void
{
$birthdayCount = Birthday::whereDate('birthday', '2021-05-12')->get();
$this->assertCount(3, $birthdayCount);
$birthdayCount = Birthday::whereDate('birthday', '2021-05-11')->get();
$this->assertCount(1, $birthdayCount);
$birthdayCount = Birthday::whereDate('birthday', '>', '2021-05-11')->get();
$this->assertCount(4, $birthdayCount);
$birthdayCount = Birthday::whereDate('birthday', '>=', '2021-05-11')->get();
$this->assertCount(5, $birthdayCount);
$birthdayCount = Birthday::whereDate('birthday', '<', '2021-05-11')->get();
$this->assertCount(1, $birthdayCount);
$birthdayCount = Birthday::whereDate('birthday', '<=', '2021-05-11')->get();
$this->assertCount(2, $birthdayCount);
$birthdayCount = Birthday::whereDate('birthday', '<>', '2021-05-11')->get();
$this->assertCount(6, $birthdayCount);
}
public function testWhereDay(): void
{
$day = Birthday::whereDay('birthday', '12')->get();
$this->assertCount(4, $day);
$day = Birthday::whereDay('birthday', '11')->get();
$this->assertCount(1, $day);
}
public function testWhereMonth(): void
{
$month = Birthday::whereMonth('birthday', '04')->get();
$this->assertCount(1, $month);
$month = Birthday::whereMonth('birthday', '05')->get();
$this->assertCount(5, $month);
$month = Birthday::whereMonth('birthday', '>=', '5')->get();
$this->assertCount(5, $month);
$month = Birthday::whereMonth('birthday', '<', '10')->get();
$this->assertCount(7, $month);
$month = Birthday::whereMonth('birthday', '<>', '5')->get();
$this->assertCount(2, $month);
}
public function testWhereYear(): void
{
$year = Birthday::whereYear('birthday', '2021')->get();
$this->assertCount(4, $year);
$year = Birthday::whereYear('birthday', '2022')->get();
$this->assertCount(1, $year);
$year = Birthday::whereYear('birthday', '<', '2021')->get();
$this->assertCount(2, $year);
$year = Birthday::whereYear('birthday', '<>', '2021')->get();
$this->assertCount(3, $year);
}
public function testWhereTime(): void
{
$time = Birthday::whereTime('birthday', '10:53:11')->get();
$this->assertCount(1, $time);
$time = Birthday::whereTime('birthday', '10:53')->get();
$this->assertCount(6, $time);
$time = Birthday::whereTime('birthday', '10')->get();
$this->assertCount(6, $time);
$time = Birthday::whereTime('birthday', '>=', '10:53:14')->get();
$this->assertCount(3, $time);
$time = Birthday::whereTime('birthday', '!=', '10:53:14')->get();
$this->assertCount(6, $time);
$time = Birthday::whereTime('birthday', '<', '10:53:12')->get();
$this->assertCount(2, $time);
}
public function testOrder(): void
{
$user = User::whereNotNull('age')->orderBy('age', 'asc')->first();
$this->assertEquals(13, $user->age);
$user = User::whereNotNull('age')->orderBy('age', 'ASC')->first();
$this->assertEquals(13, $user->age);
$user = User::whereNotNull('age')->orderBy('age', 'desc')->first();
$this->assertEquals(37, $user->age);
$user = User::whereNotNull('age')->orderBy('natural', 'asc')->first();
$this->assertEquals(35, $user->age);
$user = User::whereNotNull('age')->orderBy('natural', 'ASC')->first();
$this->assertEquals(35, $user->age);
$user = User::whereNotNull('age')->orderBy('natural', 'desc')->first();
$this->assertEquals(35, $user->age);
}
public function testStringableOrder(): void
{
$age = str('age');
$user = User::whereNotNull('age')->orderBy($age, 'asc')->first();
$this->assertEquals(13, $user->age);
$user = User::whereNotNull('age')->orderBy($age, 'desc')->first();
$this->assertEquals(37, $user->age);
}
public function testGroupBy(): void
{
$users = User::groupBy('title')->get();
$this->assertCount(3, $users);
$users = User::groupBy('age')->get();
$this->assertCount(6, $users);
$users = User::groupBy('age')->skip(1)->get();
$this->assertCount(5, $users);
$users = User::groupBy('age')->take(2)->get();
$this->assertCount(2, $users);
$users = User::groupBy('age')->orderBy('age', 'desc')->get();
$this->assertEquals(37, $users[0]->age);
$this->assertEquals(35, $users[1]->age);
$this->assertEquals(33, $users[2]->age);
$users = User::groupBy('age')->skip(1)->take(2)->orderBy('age', 'desc')->get();
$this->assertCount(2, $users);
$this->assertEquals(35, $users[0]->age);
$this->assertEquals(33, $users[1]->age);
$this->assertNull($users[0]->name);
$users = User::select('name')->groupBy('age')->skip(1)->take(2)->orderBy('age', 'desc')->get();
$this->assertCount(2, $users);
$this->assertNotNull($users[0]->name);
}
public function testCount(): void
{
$count = User::where('age', '<>', 35)->count();
$this->assertEquals(6, $count);
// Test for issue #165
$count = User::select('id', 'age', 'title')->where('age', '<>', 35)->count();
$this->assertEquals(6, $count);
}
public function testExists(): void
{
$this->assertFalse(User::where('age', '>', 37)->exists());
$this->assertTrue(User::where('age', '<', 37)->exists());
$this->assertTrue(User::where('age', '>', 37)->doesntExist());
$this->assertFalse(User::where('age', '<', 37)->doesntExist());
}
public function testSubQuery(): void
{
$users = User::where('title', 'admin')->orWhere(function ($query) {
$query->where('name', 'Tommy Toe')
->orWhere('name', 'Error');
})
->get();
$this->assertCount(5, $users);
$users = User::where('title', 'user')->where(function ($query) {
$query->where('age', 35)
->orWhere('name', 'like', '%harry%');
})
->get();
$this->assertCount(2, $users);
$users = User::where('age', 35)->orWhere(function ($query) {
$query->where('title', 'admin')
->orWhere('name', 'Error');
})
->get();
$this->assertCount(5, $users);
$users = User::whereNull('deleted_at')
->where('title', 'admin')
->where(function ($query) {
$query->where('age', '>', 15)
->orWhere('name', 'Harry Hoe');
})
->get();
$this->assertEquals(3, $users->count());
$users = User::whereNull('deleted_at')
->where(function ($query) {
$query->where('name', 'Harry Hoe')
->orWhere(function ($query) {
$query->where('age', '>', 15)
->where('title', '<>', 'admin');
});
})
->get();
$this->assertEquals(5, $users->count());
}
public function testWhereRaw(): void
{
$where = ['age' => ['$gt' => 30, '$lt' => 40]];
$users = User::whereRaw($where)->get();
$this->assertCount(6, $users);
$where1 = ['age' => ['$gt' => 30, '$lte' => 35]];
$where2 = ['age' => ['$gt' => 35, '$lt' => 40]];
$users = User::whereRaw($where1)->orWhereRaw($where2)->get();
$this->assertCount(6, $users);
}
public function testMultipleOr(): void
{
$users = User::where(function ($query) {
$query->where('age', 35)->orWhere('age', 33);
})
->where(function ($query) {
$query->where('name', 'John Doe')->orWhere('name', 'Jane Doe');
})->get();
$this->assertCount(2, $users);
$users = User::where(function ($query) {
$query->orWhere('age', 35)->orWhere('age', 33);
})
->where(function ($query) {
$query->orWhere('name', 'John Doe')->orWhere('name', 'Jane Doe');
})->get();
$this->assertCount(2, $users);
}
public function testPaginate(): void
{
$results = User::paginate(2);
$this->assertEquals(2, $results->count());
$this->assertNotNull($results->first()->title);
$this->assertEquals(9, $results->total());
$results = User::paginate(2, ['name', 'age']);
$this->assertEquals(2, $results->count());
$this->assertNull($results->first()->title);
$this->assertEquals(9, $results->total());
$this->assertEquals(1, $results->currentPage());
}
public function testCursorPaginate(): void
{
$results = User::cursorPaginate(2);
$this->assertEquals(2, $results->count());
$this->assertNotNull($results->first()->title);
$this->assertNotNull($results->nextCursor());
$this->assertTrue($results->onFirstPage());
$results = User::cursorPaginate(2, ['name', 'age']);
$this->assertEquals(2, $results->count());
$this->assertNull($results->first()->title);
$results = User::orderBy('age', 'desc')->cursorPaginate(2, ['name', 'age']);
$this->assertEquals(2, $results->count());
$this->assertEquals(37, $results->first()->age);
$this->assertNull($results->first()->title);
$results = User::whereNotNull('age')->orderBy('age', 'asc')->cursorPaginate(2, ['name', 'age']);
$this->assertEquals(2, $results->count());
$this->assertEquals(13, $results->first()->age);
$this->assertNull($results->first()->title);
}
public function testPaginateGroup(): void
{
// First page
$results = User::groupBy('age')->paginate(2);
$this->assertEquals(2, $results->count());
$this->assertEquals(6, $results->total());
$this->assertEquals(3, $results->lastPage());
$this->assertEquals(1, $results->currentPage());
$this->assertCount(2, $results->items());
$this->assertArrayHasKey('age', $results->first()->getAttributes());
// Last page has fewer results
$results = User::groupBy('age')->paginate(4, page: 2);
$this->assertEquals(2, $results->count());
$this->assertEquals(6, $results->total());
$this->assertEquals(2, $results->lastPage());
$this->assertEquals(2, $results->currentPage());
$this->assertCount(2, $results->items());
$this->assertArrayHasKey('age', $results->first()->getAttributes());
// Using a filter
$results = User::where('title', 'admin')->groupBy('age')->paginate(4);
$this->assertEquals(2, $results->count());
$this->assertEquals(2, $results->total());
$this->assertEquals(1, $results->lastPage());
$this->assertEquals(1, $results->currentPage());
$this->assertCount(2, $results->items());
$this->assertArrayHasKey('age', $results->last()->getAttributes());
}
public function testPaginateDistinct(): void
{
$this->expectException(BadMethodCallException::class);
$this->expectExceptionMessage('Distinct queries cannot be used for pagination. Use GroupBy instead');
User::distinct('age')->paginate(2);
}
public function testUpdate(): void
{
$this->assertEquals(1, User::where(['name' => 'John Doe'])->update(['name' => 'Jim Morrison']));
$this->assertEquals(1, User::where(['name' => 'Jim Morrison'])->count());
Scoped::create(['favorite' => true]);
Scoped::create(['favorite' => false]);
$this->assertCount(1, Scoped::get());
$this->assertEquals(1, Scoped::query()->update(['name' => 'Johnny']));
$this->assertCount(1, Scoped::withoutGlobalScopes()->where(['name' => 'Johnny'])->get());
$this->assertCount(2, Scoped::withoutGlobalScopes()->get());
$this->assertEquals(2, Scoped::withoutGlobalScopes()->update(['name' => 'Jimmy']));
$this->assertCount(2, Scoped::withoutGlobalScopes()->where(['name' => 'Jimmy'])->get());
}
public function testUnsorted(): void
{
$unsortedResults = User::get();
$unsortedSubset = $unsortedResults->where('age', 35)->values();
$this->assertEquals('John Doe', $unsortedSubset[0]->name);
$this->assertEquals('Brett Boe', $unsortedSubset[1]->name);
$this->assertEquals('Yvonne Yoe', $unsortedSubset[2]->name);
}
public function testSort(): void
{
$results = User::orderBy('age')->get();
$this->assertEquals($results->sortBy('age')->pluck('age')->all(), $results->pluck('age')->all());
}
public function testSortOrder(): void
{
$results = User::orderBy('age', 'desc')->get();
$this->assertEquals($results->sortByDesc('age')->pluck('age')->all(), $results->pluck('age')->all());
}
public function testMultipleSort(): void
{
$results = User::orderBy('age')->orderBy('name')->get();
$subset = $results->where('age', 35)->values();
$this->assertEquals('Brett Boe', $subset[0]->name);
$this->assertEquals('John Doe', $subset[1]->name);
$this->assertEquals('Yvonne Yoe', $subset[2]->name);
}
public function testMultipleSortOrder(): void
{
$results = User::orderBy('age')->orderBy('name', 'desc')->get();
$subset = $results->where('age', 35)->values();
$this->assertEquals('Yvonne Yoe', $subset[0]->name);
$this->assertEquals('John Doe', $subset[1]->name);
$this->assertEquals('Brett Boe', $subset[2]->name);
}
public function testDelete(): void
{
// Check fixtures
$this->assertEquals(3, User::where('title', 'admin')->count());
// Delete a single document with filter
User::where('title', 'admin')->limit(1)->delete();
$this->assertEquals(2, User::where('title', 'admin')->count());
// Delete all with filter
User::where('title', 'admin')->delete();
$this->assertEquals(0, User::where('title', 'admin')->count());
// Check remaining fixtures
$this->assertEquals(6, User::count());
// Delete a single document
User::limit(1)->delete();
$this->assertEquals(5, User::count());
// Delete all
User::limit(null)->delete();
$this->assertEquals(0, User::count());
}
public function testLimitCount(): void
{
$count = User::where('age', '>=', 20)->count();
$this->assertEquals(7, $count);
$count = User::where('age', '>=', 20)->options(['limit' => 3])->count();
$this->assertEquals(3, $count);
}
}
================================================
FILE: tests/Queue/Failed/DatabaseFailedJobProviderTest.php
================================================
table('failed_jobs')
->raw()
->insertMany(array_map(static fn ($i) => [
'_id' => new ObjectId(sprintf('%024d', $i)),
'connection' => 'mongodb',
'queue' => $i % 2 ? 'default' : 'other',
'failed_at' => new UTCDateTime(Date::now()->subHours($i)),
], range(1, 5)));
}
public function tearDown(): void
{
DB::connection('mongodb')
->table('failed_jobs')
->raw()
->drop();
parent::tearDown();
}
public function testLog(): void
{
$provider = $this->getProvider();
$provider->log('mongodb', 'default', '{"foo":"bar"}', new OutOfBoundsException('This is the error'));
$ids = $provider->ids();
$this->assertCount(6, $ids);
$inserted = $provider->find($ids[0]);
$this->assertSame('mongodb', $inserted->connection);
$this->assertSame('default', $inserted->queue);
$this->assertSame('{"foo":"bar"}', $inserted->payload);
$this->assertStringContainsString('OutOfBoundsException: This is the error', $inserted->exception);
$this->assertInstanceOf(ObjectId::class, $inserted->id);
}
public function testCount(): void
{
$provider = $this->getProvider();
$this->assertEquals(5, $provider->count());
$this->assertEquals(3, $provider->count('mongodb', 'default'));
$this->assertEquals(2, $provider->count('mongodb', 'other'));
}
public function testAll(): void
{
$all = $this->getProvider()->all();
$this->assertCount(5, $all);
$this->assertInstanceOf(ObjectId::class, $all[0]->id);
$this->assertEquals(new ObjectId(sprintf('%024d', 5)), $all[0]->id);
$this->assertEquals(sprintf('%024d', 5), (string) $all[0]->id, 'id field is added for compatibility with DatabaseFailedJobProvider');
}
public function testFindAndForget(): void
{
$provider = $this->getProvider();
$id = sprintf('%024d', 2);
$found = $provider->find($id);
$this->assertIsObject($found, 'The job is found');
$this->assertInstanceOf(ObjectId::class, $found->id);
$this->assertEquals(new ObjectId($id), $found->id);
$this->assertObjectHasProperty('failed_at', $found);
// Delete the job
$result = $provider->forget($id);
$this->assertTrue($result, 'forget return true when the job have been deleted');
$this->assertNull($provider->find($id), 'the job have been deleted');
// Delete the same job again
$result = $provider->forget($id);
$this->assertFalse($result, 'forget return false when the job does not exist');
$this->assertCount(4, $provider->ids(), 'Other jobs are kept');
}
public function testIds(): void
{
$ids = $this->getProvider()->ids();
$this->assertCount(5, $ids);
$this->assertEquals(new ObjectId(sprintf('%024d', 5)), $ids[0]);
}
public function testIdsFilteredByQuery(): void
{
$ids = $this->getProvider()->ids('other');
$this->assertCount(2, $ids);
$this->assertEquals(new ObjectId(sprintf('%024d', 4)), $ids[0]);
}
public function testFlush(): void
{
$provider = $this->getProvider();
$this->assertEquals(5, $provider->count());
$provider->flush(4);
$this->assertEquals(3, $provider->count());
}
public function testPrune(): void
{
$provider = $this->getProvider();
$this->assertEquals(5, $provider->count());
$result = $provider->prune(Date::now()->subHours(4));
$this->assertEquals(2, $result);
$this->assertEquals(3, $provider->count());
}
private function getProvider(): DatabaseFailedJobProvider
{
return new DatabaseFailedJobProvider(DB::getFacadeRoot(), '', 'failed_jobs');
}
}
================================================
FILE: tests/QueueTest.php
================================================
table(Config::get('queue.connections.database.table'))->truncate();
Queue::getDatabase()->table(Config::get('queue.failed.table'))->truncate();
Carbon::setTestNow(Carbon::now());
}
public function testQueueJobLifeCycle(): void
{
$uuid = Str::uuid();
Str::createUuidsUsing(fn () => $uuid);
$id = Queue::push('test', ['action' => 'QueueJobLifeCycle'], 'test');
$this->assertNotNull($id);
// Get and reserve the test job (next available)
$job = Queue::pop('test');
$this->assertInstanceOf(MongoJob::class, $job);
$this->assertEquals(1, $job->isReserved());
$payload = json_decode($job->getRawBody(), true);
$this->assertEquals($uuid, $payload['uuid']);
$this->assertEquals('test', $payload['displayName']);
$this->assertEquals('test', $payload['job']);
$this->assertNull($payload['maxTries']);
$this->assertNull($payload['maxExceptions']);
$this->assertFalse($payload['failOnTimeout']);
$this->assertNull($payload['backoff']);
$this->assertNull($payload['timeout']);
$this->assertEquals(['action' => 'QueueJobLifeCycle'], $payload['data']);
// Remove reserved job
$job->delete();
$this->assertEquals(0, Queue::getDatabase()->table(Config::get('queue.connections.database.table'))->count());
Str::createUuidsNormally();
}
public function testQueueJobExpired(): void
{
$id = Queue::push('test', ['action' => 'QueueJobExpired'], 'test');
$this->assertNotNull($id);
// Expire the test job
$expiry = Carbon::now()->subSeconds(Config::get('queue.connections.database.expire'))->getTimestamp();
Queue::getDatabase()
->table(Config::get('queue.connections.database.table'))
->where('id', $id)
->update(['reserved' => 1, 'reserved_at' => $expiry]);
// Expect an attempted older job in the queue
$job = Queue::pop('test');
$this->assertEquals(1, $job->attempts());
$this->assertGreaterThan($expiry, $job->reservedAt());
$job->delete();
$this->assertEquals(0, Queue::getDatabase()->table(Config::get('queue.connections.database.table'))->count());
}
public function testFailQueueJob(): void
{
$provider = app('queue.failer');
$this->assertInstanceOf(DatabaseFailedJobProvider::class, $provider);
}
public function testFindFailJobNull(): void
{
Config::set('queue.failed.database', 'mongodb');
$provider = app('queue.failer');
$this->assertNull($provider->find(1));
}
public function testIncrementAttempts(): void
{
$jobId = Queue::push('test1', ['action' => 'QueueJobExpired'], 'test');
$this->assertNotNull($jobId);
$jobId = Queue::push('test2', ['action' => 'QueueJobExpired'], 'test');
$this->assertNotNull($jobId);
$job = Queue::pop('test');
$this->assertEquals(1, $job->attempts());
$job->delete();
$othersJobs = Queue::getDatabase()
->table(Config::get('queue.connections.database.table'))
->get();
$this->assertCount(1, $othersJobs);
$this->assertEquals(0, $othersJobs[0]->attempts);
}
public function testJobRelease(): void
{
$queue = 'test';
$jobId = Queue::push($queue, ['action' => 'QueueJobRelease'], 'test');
$this->assertNotNull($jobId);
$job = Queue::pop($queue);
$job->release();
$jobs = Queue::getDatabase()
->table(Config::get('queue.connections.database.table'))
->get();
$this->assertCount(1, $jobs);
$this->assertEquals(1, $jobs[0]->attempts);
}
public function testQueueDeleteReserved(): void
{
$queue = 'test';
$jobId = Queue::push($queue, ['action' => 'QueueDeleteReserved'], 'test');
Queue::deleteReserved($queue, $jobId, 0);
$jobs = Queue::getDatabase()
->table(Config::get('queue.connections.database.table'))
->get();
$this->assertCount(0, $jobs);
}
public function testQueueRelease(): void
{
$queue = 'test';
$delay = 123;
Queue::push($queue, ['action' => 'QueueRelease'], 'test');
$job = Queue::pop($queue);
$releasedJobId = Queue::release($queue, $job->getJobRecord(), $delay);
$releasedJob = Queue::getDatabase()
->table(Config::get('queue.connections.database.table'))
->where('id', $releasedJobId)
->first();
$this->assertEquals($queue, $releasedJob->queue);
$this->assertEquals(1, $releasedJob->attempts);
$this->assertNull($releasedJob->reserved_at);
$this->assertEquals(
Carbon::now()->addRealSeconds($delay)->getTimestamp(),
$releasedJob->available_at,
);
$this->assertEquals(Carbon::now()->getTimestamp(), $releasedJob->created_at);
$this->assertEquals($job->getRawBody(), $releasedJob->payload);
}
public function testQueueDeleteAndRelease(): void
{
$queue = 'test';
$delay = 123;
Queue::push($queue, ['action' => 'QueueDeleteAndRelease'], 'test');
$job = Queue::pop($queue);
$mock = Mockery::mock(MongoQueue::class)->makePartial();
$mock->expects('deleteReserved')->once()->with($queue, $job->getJobId());
$mock->expects('release')->once()->with($queue, $job->getJobRecord(), $delay);
$mock->deleteAndRelease($queue, $job, $delay);
}
public function testFailedJobLogging()
{
Carbon::setTestNow('2019-01-01 00:00:00');
$provider = app('queue.failer');
$provider->log('test_connection', 'test_queue', 'test_payload', new Exception('test_exception'));
$failedJob = Queue::getDatabase()->table(Config::get('queue.failed.table'))->first();
$this->assertSame('test_connection', $failedJob->connection);
$this->assertSame('test_queue', $failedJob->queue);
$this->assertSame('test_payload', $failedJob->payload);
$this->assertEquals(Carbon::now(), $failedJob->failed_at);
$this->assertStringStartsWith('Exception: test_exception in ', $failedJob->exception);
}
}
================================================
FILE: tests/RelationsTest.php
================================================
'George R. R. Martin']);
Book::create(['title' => 'A Game of Thrones', 'author_id' => $author->id]);
Book::create(['title' => 'A Clash of Kings', 'author_id' => $author->id]);
$books = $author->books;
$this->assertCount(2, $books);
$user = User::create(['name' => 'John Doe']);
Item::create(['type' => 'knife', 'user_id' => $user->id]);
Item::create(['type' => 'shield', 'user_id' => $user->id]);
Item::create(['type' => 'sword', 'user_id' => $user->id]);
Item::create(['type' => 'bag', 'user_id' => null]);
$items = $user->items;
$this->assertCount(3, $items);
}
public function testHasManyWithTrashed(): void
{
$user = User::create(['name' => 'George R. R. Martin']);
$first = Soft::create(['title' => 'A Game of Thrones', 'user_id' => $user->id]);
$second = Soft::create(['title' => 'The Witcher', 'user_id' => $user->id]);
self::assertNull($first->deleted_at);
self::assertEquals($user->id, $first->user->id);
self::assertEquals([$first->id, $second->id], $user->softs->pluck('id')->toArray());
$first->delete();
$user->refresh();
self::assertNotNull($first->deleted_at);
self::assertEquals([$second->id], $user->softs->pluck('id')->toArray());
self::assertEquals([$first->id, $second->id], $user->softsWithTrashed->pluck('id')->toArray());
}
public function testBelongsTo(): void
{
$user = User::create(['name' => 'George R. R. Martin']);
Book::create(['title' => 'A Game of Thrones', 'author_id' => $user->id]);
$book = Book::create(['title' => 'A Clash of Kings', 'author_id' => $user->id]);
$author = $book->author;
$this->assertEquals('George R. R. Martin', $author->name);
$user = User::create(['name' => 'John Doe']);
$item = Item::create(['type' => 'sword', 'user_id' => $user->id]);
$owner = $item->user;
$this->assertEquals('John Doe', $owner->name);
$book = Book::create(['title' => 'A Clash of Kings']);
$this->assertNull($book->author);
}
public function testHasOne(): void
{
$user = User::create(['name' => 'John Doe']);
Role::create(['type' => 'admin', 'user_id' => $user->id]);
$role = $user->role;
$this->assertEquals('admin', $role->type);
$this->assertEquals($user->id, $role->user_id);
$user = User::create(['name' => 'Jane Doe']);
$role = new Role(['type' => 'user']);
$user->role()->save($role);
$role = $user->role;
$this->assertEquals('user', $role->type);
$this->assertEquals($user->id, $role->user_id);
$user = User::where('name', 'Jane Doe')->first();
$role = $user->role;
$this->assertEquals('user', $role->type);
$this->assertEquals($user->id, $role->user_id);
}
public function testWithBelongsTo(): void
{
$user = User::create(['name' => 'John Doe']);
Item::create(['type' => 'knife', 'user_id' => $user->id]);
Item::create(['type' => 'shield', 'user_id' => $user->id]);
Item::create(['type' => 'sword', 'user_id' => $user->id]);
Item::create(['type' => 'bag', 'user_id' => null]);
$items = Item::with('user')->orderBy('user_id', 'desc')->get();
$user = $items[0]->getRelation('user');
$this->assertInstanceOf(User::class, $user);
$this->assertEquals('John Doe', $user->name);
$this->assertCount(1, $items[0]->getRelations());
$this->assertNull($items[3]->getRelation('user'));
}
public function testWithHashMany(): void
{
$user = User::create(['name' => 'John Doe']);
Item::create(['type' => 'knife', 'user_id' => $user->id]);
Item::create(['type' => 'shield', 'user_id' => $user->id]);
Item::create(['type' => 'sword', 'user_id' => $user->id]);
Item::create(['type' => 'bag', 'user_id' => null]);
$user = User::with('items')->find($user->id);
$items = $user->getRelation('items');
$this->assertCount(3, $items);
$this->assertInstanceOf(Item::class, $items[0]);
}
public function testWithHasOne(): void
{
$user = User::create(['name' => 'John Doe']);
Role::create(['type' => 'admin', 'user_id' => $user->id]);
Role::create(['type' => 'guest', 'user_id' => $user->id]);
$user = User::with('role')->find($user->id);
$role = $user->getRelation('role');
$this->assertInstanceOf(Role::class, $role);
$this->assertEquals('admin', $role->type);
}
public function testEasyRelation(): void
{
// Has Many
$user = User::create(['name' => 'John Doe']);
$item = Item::create(['type' => 'knife']);
$user->items()->save($item);
$user = User::find($user->id);
$items = $user->items;
$this->assertCount(1, $items);
$this->assertInstanceOf(Item::class, $items[0]);
$this->assertEquals($user->id, $items[0]->user_id);
// Has one
$user = User::create(['name' => 'John Doe']);
$role = Role::create(['type' => 'admin']);
$user->role()->save($role);
$user = User::find($user->id);
$role = $user->role;
$this->assertInstanceOf(Role::class, $role);
$this->assertEquals('admin', $role->type);
$this->assertEquals($user->id, $role->user_id);
}
public function testBelongsToMany(): void
{
$user = User::create(['name' => 'John Doe']);
// Add 2 clients
$user->clients()->save(new Client(['name' => 'Pork Pies Ltd.']));
$user->clients()->create(['name' => 'Buffet Bar Inc.']);
// Refetch
$user = User::with('clients')->find($user->id);
$client = Client::with('users')->first();
// Check for relation attributes
$this->assertArrayHasKey('user_ids', $client->getAttributes());
$this->assertArrayHasKey('client_ids', $user->getAttributes());
$clients = $user->getRelation('clients');
$users = $client->getRelation('users');
$this->assertInstanceOf(Collection::class, $users);
$this->assertInstanceOf(Collection::class, $clients);
$this->assertInstanceOf(Client::class, $clients[0]);
$this->assertInstanceOf(User::class, $users[0]);
$this->assertCount(2, $user->clients);
$this->assertCount(1, $client->users);
// Now create a new user to an existing client
$user = $client->users()->create(['name' => 'Jane Doe']);
$this->assertInstanceOf(Collection::class, $user->clients);
$this->assertInstanceOf(Client::class, $user->clients->first());
$this->assertCount(1, $user->clients);
// Get user and unattached client
$user = User::where('name', '=', 'Jane Doe')->first();
$client = Client::Where('name', '=', 'Buffet Bar Inc.')->first();
// Check the models are what they should be
$this->assertInstanceOf(Client::class, $client);
$this->assertInstanceOf(User::class, $user);
// Assert they are not attached
$this->assertNotContains($client->id, $user->client_ids);
$this->assertNotContains($user->id, $client->user_ids);
$this->assertCount(1, $user->clients);
$this->assertCount(1, $client->users);
// Attach the client to the user
$user->clients()->attach($client);
// Get the new user model
$user = User::where('name', '=', 'Jane Doe')->first();
$client = Client::Where('name', '=', 'Buffet Bar Inc.')->first();
// Assert they are attached
$this->assertContains($client->id, $user->client_ids);
$this->assertContains($user->id, $client->user_ids);
$this->assertCount(2, $user->clients);
$this->assertCount(2, $client->users);
// Detach clients from user
$user->clients()->sync([]);
// Get the new user model
$user = User::where('name', '=', 'Jane Doe')->first();
$client = Client::Where('name', '=', 'Buffet Bar Inc.')->first();
// Assert they are not attached
$this->assertNotContains($client->id, $user->client_ids);
$this->assertNotContains($user->id, $client->user_ids);
$this->assertCount(0, $user->clients);
$this->assertCount(1, $client->users);
}
public function testBelongsToManyAttachesExistingModels(): void
{
$user = User::create(['name' => 'John Doe', 'client_ids' => ['1234523']]);
$clients = [
Client::create(['name' => 'Pork Pies Ltd.'])->id,
Client::create(['name' => 'Buffet Bar Inc.'])->id,
];
$moreClients = [
Client::create(['name' => 'synced Boloni Ltd.'])->id,
Client::create(['name' => 'synced Meatballs Inc.'])->id,
];
// Sync multiple records
$user->clients()->sync($clients);
$user = User::with('clients')->find($user->id);
// Assert non attached ID's are detached successfully
$this->assertNotContains('1234523', $user->client_ids);
// Assert there are two client objects in the relationship
$this->assertCount(2, $user->clients);
// Add more clients
$user->clients()->sync($moreClients);
// Refetch
$user = User::with('clients')->find($user->id);
// Assert there are now still 2 client objects in the relationship
$this->assertCount(2, $user->clients);
// Assert that the new relationships name start with synced
$this->assertStringStartsWith('synced', $user->clients[0]->name);
$this->assertStringStartsWith('synced', $user->clients[1]->name);
}
public function testBelongsToManySync(): void
{
// create test instances
$user = User::create(['name' => 'Hans Thomas']);
$client1 = Client::create(['name' => 'Pork Pies Ltd.']);
$client2 = Client::create(['name' => 'Buffet Bar Inc.']);
// Sync multiple
$user->clients()->sync([$client1->id, $client2->id]);
$this->assertCount(2, $user->clients);
// Sync single wrapped by an array
$user->clients()->sync([$client1->id]);
$user->load('clients');
$this->assertCount(1, $user->clients);
self::assertTrue($user->clients->first()->is($client1));
// Sync single model
$user->clients()->sync($client2);
$user->load('clients');
$this->assertCount(1, $user->clients);
self::assertTrue($user->clients->first()->is($client2));
}
public function testBelongsToManyAttachArray(): void
{
$user = User::create(['name' => 'John Doe']);
$client1 = Client::create(['name' => 'Test 1'])->id;
$client2 = Client::create(['name' => 'Test 2'])->id;
$user->clients()->attach([$client1, $client2]);
$this->assertCount(2, $user->clients);
}
public function testBelongsToManyAttachEloquentCollection(): void
{
User::create(['name' => 'John Doe']);
$client1 = Client::create(['name' => 'Test 1']);
$client2 = Client::create(['name' => 'Test 2']);
$collection = new Collection([$client1, $client2]);
$user = User::where('name', '=', 'John Doe')->first();
$user->clients()->attach($collection);
$this->assertCount(2, $user->clients);
}
public function testBelongsToManySyncWithCustomKeys(): void
{
$client = Client::create(['cclient_id' => (string) (new ObjectId()), 'years' => '5']);
$skill1 = Skill::create(['cskill_id' => (string) (new ObjectId()), 'name' => 'PHP']);
$skill2 = Skill::create(['cskill_id' => (string) (new ObjectId()), 'name' => 'Laravel']);
$client = Client::query()->find($client->id);
$client->skillsWithCustomKeys()->sync([$skill1->cskill_id, $skill2->cskill_id]);
$this->assertCount(2, $client->skillsWithCustomKeys);
self::assertIsString($skill1->cskill_id);
self::assertContains($skill1->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($skill1->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertIsString($skill2->cskill_id);
self::assertContains($skill2->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($skill2->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
$check = Skill::query()->find($skill1->id);
self::assertIsString($check->cskill_id);
self::assertContains($check->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($check->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
$check = Skill::query()->find($skill2->id);
self::assertIsString($check->cskill_id);
self::assertContains($check->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($check->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
}
public function testBelongsToManySyncModelWithCustomKeys(): void
{
$client = Client::create(['cclient_id' => (string) (new ObjectId()), 'years' => '5']);
$skill1 = Skill::create(['cskill_id' => (string) (new ObjectId()), 'name' => 'PHP']);
$client = Client::query()->find($client->id);
$client->skillsWithCustomKeys()->sync($skill1);
$this->assertCount(1, $client->skillsWithCustomKeys);
self::assertIsString($skill1->cskill_id);
self::assertContains($skill1->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($skill1->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
$check = Skill::query()->find($skill1->id);
self::assertIsString($check->id);
self::assertContains($check->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($check->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
}
public function testBelongsToManySyncEloquentCollectionWithCustomKeys(): void
{
$client = Client::create(['cclient_id' => (string) (new ObjectId()), 'years' => '5']);
$skill1 = Skill::create(['cskill_id' => (string) (new ObjectId()), 'name' => 'PHP']);
$skill2 = Skill::create(['cskill_id' => (string) (new ObjectId()), 'name' => 'Laravel']);
$collection = new Collection([$skill1, $skill2]);
$client = Client::query()->find($client->id);
$client->skillsWithCustomKeys()->sync($collection);
$this->assertCount(2, $client->skillsWithCustomKeys);
self::assertIsString($skill1->cskill_id);
self::assertContains($skill1->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($skill1->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertIsString($skill2->cskill_id);
self::assertContains($skill2->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($skill2->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
$check = Skill::query()->find($skill1->id);
self::assertIsString($check->cskill_id);
self::assertContains($check->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($check->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
$check = Skill::query()->find($skill2->id);
self::assertIsString($check->cskill_id);
self::assertContains($check->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($check->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
}
public function testBelongsToManyAttachWithCustomKeys(): void
{
$client = Client::create(['cclient_id' => (string) (new ObjectId()), 'years' => '5']);
$skill1 = Skill::create(['cskill_id' => (string) (new ObjectId()), 'name' => 'PHP']);
$skill2 = Skill::create(['cskill_id' => (string) (new ObjectId()), 'name' => 'Laravel']);
$client = Client::query()->find($client->id);
$client->skillsWithCustomKeys()->attach([$skill1->cskill_id, $skill2->cskill_id]);
$this->assertCount(2, $client->skillsWithCustomKeys);
self::assertIsString($skill1->cskill_id);
self::assertContains($skill1->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($skill1->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertIsString($skill2->cskill_id);
self::assertContains($skill2->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($skill2->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
$check = Skill::query()->find($skill1->id);
self::assertIsString($check->cskill_id);
self::assertContains($check->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($check->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
$check = Skill::query()->find($skill2->id);
self::assertIsString($check->cskill_id);
self::assertContains($check->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($check->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
}
public function testBelongsToManyAttachModelWithCustomKeys(): void
{
$client = Client::create(['cclient_id' => (string) (new ObjectId()), 'years' => '5']);
$skill1 = Skill::create(['cskill_id' => (string) (new ObjectId()), 'name' => 'PHP']);
$client = Client::query()->find($client->id);
$client->skillsWithCustomKeys()->attach($skill1);
$this->assertCount(1, $client->skillsWithCustomKeys);
self::assertIsString($skill1->cskill_id);
self::assertContains($skill1->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($skill1->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
$check = Skill::query()->find($skill1->id);
self::assertIsString($check->id);
self::assertContains($check->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($check->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
}
public function testBelongsToManyAttachEloquentCollectionWithCustomKeys(): void
{
$client = Client::create(['cclient_id' => (string) (new ObjectId()), 'years' => '5']);
$skill1 = Skill::create(['cskill_id' => (string) (new ObjectId()), 'name' => 'PHP']);
$skill2 = Skill::create(['cskill_id' => (string) (new ObjectId()), 'name' => 'Laravel']);
$collection = new Collection([$skill1, $skill2]);
$client = Client::query()->find($client->id);
$client->skillsWithCustomKeys()->attach($collection);
$this->assertCount(2, $client->skillsWithCustomKeys);
self::assertIsString($skill1->cskill_id);
self::assertContains($skill1->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($skill1->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertIsString($skill2->cskill_id);
self::assertContains($skill2->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($skill2->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
$check = Skill::query()->find($skill1->id);
self::assertIsString($check->cskill_id);
self::assertContains($check->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($check->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
$check = Skill::query()->find($skill2->id);
self::assertIsString($check->cskill_id);
self::assertContains($check->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($check->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
}
public function testBelongsToManyDetachWithCustomKeys(): void
{
$client = Client::create(['cclient_id' => (string) (new ObjectId()), 'years' => '5']);
$skill1 = Skill::create(['cskill_id' => (string) (new ObjectId()), 'name' => 'PHP']);
$skill2 = Skill::create(['cskill_id' => (string) (new ObjectId()), 'name' => 'Laravel']);
$client = Client::query()->find($client->id);
$client->skillsWithCustomKeys()->sync([$skill1->cskill_id, $skill2->cskill_id]);
$this->assertCount(2, $client->skillsWithCustomKeys);
$client->skillsWithCustomKeys()->detach($skill1->cskill_id);
$client->load('skillsWithCustomKeys'); // Reload the relationship based on the latest pivot column's data
$this->assertCount(1, $client->skillsWithCustomKeys);
self::assertIsString($skill1->cskill_id);
self::assertNotContains($skill1->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($skill1->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertIsString($skill2->cskill_id);
self::assertContains($skill2->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($skill2->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
$check = Skill::query()->find($skill1->id);
self::assertIsString($check->cskill_id);
self::assertNotContains($check->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($check->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
$check = Skill::query()->find($skill2->id);
self::assertIsString($check->cskill_id);
self::assertContains($check->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($check->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
}
public function testBelongsToManyDetachModelWithCustomKeys(): void
{
$client = Client::create(['cclient_id' => (string) (new ObjectId()), 'years' => '5']);
$skill1 = Skill::create(['cskill_id' => (string) (new ObjectId()), 'name' => 'PHP']);
$skill2 = Skill::create(['cskill_id' => (string) (new ObjectId()), 'name' => 'Laravel']);
$client = Client::query()->find($client->id);
$client->skillsWithCustomKeys()->sync([$skill1->cskill_id, $skill2->cskill_id]);
$this->assertCount(2, $client->skillsWithCustomKeys);
$client->skillsWithCustomKeys()->detach($skill1);
$client->load('skillsWithCustomKeys'); // Reload the relationship based on the latest pivot column's data
$this->assertCount(1, $client->skillsWithCustomKeys);
self::assertIsString($skill1->cskill_id);
self::assertNotContains($skill1->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($skill1->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertIsString($skill2->cskill_id);
self::assertContains($skill2->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($skill2->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
$check = Skill::query()->find($skill1->id);
self::assertIsString($check->cskill_id);
self::assertNotContains($check->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($check->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
$check = Skill::query()->find($skill2->id);
self::assertIsString($check->cskill_id);
self::assertContains($check->cskill_id, $client->skillsWithCustomKeys->pluck('cskill_id'));
self::assertNotContains($check->id, $client->skillsWithCustomKeys->pluck('cskill_id'));
}
public function testBelongsToManySyncAlreadyPresent(): void
{
$user = User::create(['name' => 'John Doe']);
$client1 = Client::create(['name' => 'Test 1'])->id;
$client2 = Client::create(['name' => 'Test 2'])->id;
$user->clients()->sync([$client1, $client2]);
$this->assertCount(2, $user->clients);
$user = User::where('name', '=', 'John Doe')->first();
$user->clients()->sync([$client1]);
$this->assertCount(1, $user->clients);
$user = User::where('name', '=', 'John Doe')->first()->toArray();
$this->assertCount(1, $user['client_ids']);
}
public function testBelongsToManyCustom(): void
{
$user = User::create(['name' => 'John Doe']);
$group = $user->groups()->create(['name' => 'Admins']);
// Refetch
$user = User::find($user->id);
$group = Group::find($group->id);
// Check for custom relation attributes
$this->assertArrayHasKey('users', $group->getAttributes());
$this->assertArrayHasKey('groups', $user->getAttributes());
// Assert they are attached
$this->assertContains($group->id, $user->groups->pluck('id')->toArray());
$this->assertContains($user->id, $group->users->pluck('id')->toArray());
$this->assertEquals($group->id, $user->groups()->first()->id);
$this->assertEquals($user->id, $group->users()->first()->id);
}
public function testMorph(): void
{
$user = User::create(['name' => 'John Doe']);
$client = Client::create(['name' => 'Jane Doe']);
$photo = Photo::create(['url' => 'http://graph.facebook.com/john.doe/picture']);
$photo = $user->photos()->save($photo);
$this->assertEquals(1, $user->photos->count());
$this->assertEquals($photo->id, $user->photos->first()->id);
$user = User::find($user->id);
$this->assertEquals(1, $user->photos->count());
$this->assertEquals($photo->id, $user->photos->first()->id);
$photo = Photo::create(['url' => 'http://graph.facebook.com/jane.doe/picture']);
$client->photo()->save($photo);
$this->assertNotNull($client->photo);
$this->assertEquals($photo->id, $client->photo->id);
$client = Client::find($client->id);
$this->assertNotNull($client->photo);
$this->assertEquals($photo->id, $client->photo->id);
$photo = Photo::first();
$this->assertEquals($photo->hasImage->name, $user->name);
// eager load
$user = User::with('photos')->find($user->id);
$relations = $user->getRelations();
$this->assertArrayHasKey('photos', $relations);
$this->assertEquals(1, $relations['photos']->count());
// inverse eager load
$photos = Photo::with('hasImage')->get();
$relations = $photos[0]->getRelations();
$this->assertArrayHasKey('hasImage', $relations);
$this->assertInstanceOf(User::class, $photos[0]->hasImage);
$relations = $photos[1]->getRelations();
$this->assertArrayHasKey('hasImage', $relations);
$this->assertInstanceOf(Client::class, $photos[1]->hasImage);
// inverse relationship
$photo = Photo::query()->create(['url' => 'https://graph.facebook.com/hans.thomas/picture']);
$client = Client::create(['name' => 'Hans Thomas']);
$photo->hasImage()->associate($client)->save();
$this->assertCount(1, $photo->hasImage()->get());
$this->assertInstanceOf(Client::class, $photo->hasImage);
$this->assertEquals($client->id, $photo->hasImage->id);
// inverse with custom ownerKey
$photo = Photo::query()->create(['url' => 'https://graph.facebook.com/young.gerald/picture']);
$client = Client::create(['cclient_id' => (string) (new ObjectId()), 'name' => 'Young Gerald']);
$photo->hasImageWithCustomOwnerKey()->associate($client)->save();
$this->assertCount(1, $photo->hasImageWithCustomOwnerKey()->get());
$this->assertInstanceOf(Client::class, $photo->hasImageWithCustomOwnerKey);
$this->assertEquals($client->cclient_id, $photo->has_image_with_custom_owner_key_id);
$this->assertEquals($client->id, $photo->hasImageWithCustomOwnerKey->id);
// inverse eager load with custom ownerKey
$photos = Photo::with('hasImageWithCustomOwnerKey')->get();
$check = $photos->last();
$relations = $check->getRelations();
$this->assertArrayHasKey('hasImageWithCustomOwnerKey', $relations);
$this->assertInstanceOf(Client::class, $check->hasImageWithCustomOwnerKey);
}
public function testMorphToMany(): void
{
$user = User::query()->create(['name' => 'Young Gerald']);
$client = Client::query()->create(['name' => 'Hans Thomas']);
$label = Label::query()->create(['name' => 'Had the world in my palms, I gave it to you']);
$user->labels()->attach($label);
$client->labels()->attach($label);
$this->assertEquals(1, $user->labels->count());
$this->assertContains($label->id, $user->labels->pluck('id'));
$this->assertEquals(1, $client->labels->count());
$this->assertContains($label->id, $user->labels->pluck('id'));
}
public function testMorphToManyAttachEloquentCollection(): void
{
$client = Client::query()->create(['name' => 'Young Gerald']);
$label1 = Label::query()->create(['name' => "Make no mistake, it's the life that I was chosen for"]);
$label2 = Label::query()->create(['name' => 'All I prayed for was an open door']);
$client->labels()->attach(new Collection([$label1, $label2]));
$this->assertEquals(2, $client->labels->count());
$this->assertContains($label1->id, $client->labels->pluck('id'));
$this->assertContains($label2->id, $client->labels->pluck('id'));
}
public function testMorphToManyAttachMultipleIds(): void
{
$client = Client::query()->create(['name' => 'Young Gerald']);
$label1 = Label::query()->create(['name' => 'stayed solid i never fled']);
$label2 = Label::query()->create(['name' => "I've got a lane and I'm in gear"]);
$client->labels()->attach([$label1->id, $label2->id]);
$this->assertEquals(2, $client->labels->count());
$this->assertContains($label1->id, $client->labels->pluck('id'));
$this->assertContains($label2->id, $client->labels->pluck('id'));
}
public function testMorphToManyDetaching(): void
{
$client = Client::query()->create(['name' => 'Marshall Mathers']);
$label1 = Label::query()->create(['name' => "I'll never love again"]);
$label2 = Label::query()->create(['name' => 'The way I loved you']);
$client->labels()->attach([$label1->id, $label2->id]);
$this->assertEquals(2, $client->labels->count());
$client->labels()->detach($label1);
$check = $client->withoutRelations();
$this->assertEquals(1, $check->labels->count());
$this->assertContains($label2->id, $client->labels->pluck('id'));
}
public function testMorphToManyDetachingMultipleIds(): void
{
$client = Client::query()->create(['name' => 'Young Gerald']);
$label1 = Label::query()->create(['name' => "I make what I wanna make, but I won't make everyone happy"]);
$label2 = Label::query()->create(['name' => "My skin's thick, but I'm not bulletproof"]);
$label3 = Label::query()->create(['name' => 'All I can be is myself, go, and tell the truth']);
$client->labels()->attach([$label1->id, $label2->id, $label3->id]);
$this->assertEquals(3, $client->labels->count());
$client->labels()->detach([$label1->id, $label2->id]);
$client->refresh();
$this->assertEquals(1, $client->labels->count());
$this->assertContains($label3->id, $client->labels->pluck('id'));
}
public function testMorphToManySyncing(): void
{
$user = User::query()->create(['name' => 'Young Gerald']);
$client = Client::query()->create(['name' => 'Hans Thomas']);
$label = Label::query()->create(['name' => "Lesson learned, we weren't the perfect match"]);
$label2 = Label::query()->create(['name' => 'Future ref, not keeping personal and work attached']);
$user->labels()->sync($label);
$client->labels()->sync($label);
$client->labels()->sync($label2, false);
$this->assertEquals(1, $user->labels->count());
$this->assertContains($label->id, $user->labels->pluck('id'));
$this->assertNotContains($label2->id, $user->labels->pluck('id'));
$this->assertEquals(2, $client->labels->count());
$this->assertContains($label->id, $client->labels->pluck('id'));
$this->assertContains($label2->id, $client->labels->pluck('id'));
}
public function testMorphToManySyncingEloquentCollection(): void
{
$client = Client::query()->create(['name' => 'Young Gerald']);
$label = Label::query()->create(['name' => 'Why the ones who love me most, the people I push away?']);
$label2 = Label::query()->create(['name' => 'Look in a mirror, this is you']);
$client->labels()->sync(new Collection([$label, $label2]));
$this->assertEquals(2, $client->labels->count());
$this->assertContains($label->id, $client->labels->pluck('id'));
$this->assertContains($label2->id, $client->labels->pluck('id'));
}
public function testMorphToManySyncingMultipleIds(): void
{
$client = Client::query()->create(['name' => 'Young Gerald']);
$label = Label::query()->create(['name' => 'They all talk about karma, how it slowly comes']);
$label2 = Label::query()->create(['name' => "But life is short, enjoy it while you're young"]);
$client->labels()->sync([$label->id, $label2->id]);
$this->assertEquals(2, $client->labels->count());
$this->assertContains($label->id, $client->labels->pluck('id'));
$this->assertContains($label2->id, $client->labels->pluck('id'));
}
public function testMorphToManySyncingWithCustomKeys(): void
{
$client = Client::query()->create(['cclient_id' => (string) (new ObjectId()), 'name' => 'Young Gerald']);
$label = Label::query()->create(['clabel_id' => (string) (new ObjectId()), 'name' => "Why do people do things that be bad for 'em?"]);
$label2 = Label::query()->create(['clabel_id' => (string) (new ObjectId()), 'name' => "Say we done with these things, then we ask for 'em"]);
$client->labelsWithCustomKeys()->sync([$label->clabel_id, $label2->clabel_id]);
$this->assertEquals(2, $client->labelsWithCustomKeys->count());
$this->assertContains($label->id, $client->labelsWithCustomKeys->pluck('id'));
$this->assertContains($label2->id, $client->labelsWithCustomKeys->pluck('id'));
$client->labelsWithCustomKeys()->sync($label);
$client->load('labelsWithCustomKeys');
$this->assertEquals(1, $client->labelsWithCustomKeys->count());
$this->assertContains($label->id, $client->labelsWithCustomKeys->pluck('id'));
$this->assertNotContains($label2->id, $client->labelsWithCustomKeys->pluck('id'));
}
public function testMorphToManyLoadAndRefreshing(): void
{
$user = User::query()->create(['name' => 'The Pretty Reckless']);
$client = Client::query()->create(['name' => 'Young Gerald']);
$label = Label::query()->create(['name' => 'The greatest gift is knowledge itself']);
$label2 = Label::query()->create(['name' => "I made it here all by my lonely, no askin' for help"]);
$client->labels()->sync([$label->id, $label2->id]);
$client->users()->sync($user);
$this->assertEquals(2, $client->labels->count());
$client->load('labels');
$this->assertEquals(2, $client->labels->count());
$client->refresh();
$this->assertEquals(2, $client->labels->count());
$check = Client::query()->find($client->id);
$this->assertEquals(2, $check->labels->count());
$check = Client::query()->with('labels')->find($client->id);
$this->assertEquals(2, $check->labels->count());
}
public function testMorphToManyHasQuery(): void
{
$client = Client::query()->create(['name' => 'Ashley']);
$client2 = Client::query()->create(['name' => 'Halsey']);
$client3 = Client::query()->create(['name' => 'John Doe 2']);
$label = Label::query()->create(['name' => "I've been digging myself down deeper"]);
$label2 = Label::query()->create(['name' => "I won't stop 'til I get where you are"]);
$client->labels()->sync([$label->id, $label2->id]);
$client2->labels()->sync($label);
$this->assertEquals(2, $client->labels->count());
$this->assertEquals(1, $client2->labels->count());
$check = Client::query()->has('labels')->get();
$this->assertCount(2, $check);
$check = Client::query()->has('labels', '>', 1)->get();
$this->assertCount(1, $check);
$this->assertContains($client->id, $check->pluck('id'));
$check = Client::query()->has('labels', '<', 2)->get();
$this->assertCount(2, $check);
$this->assertContains($client2->id, $check->pluck('id'));
$this->assertContains($client3->id, $check->pluck('id'));
}
public function testMorphedByMany(): void
{
$user = User::query()->create(['name' => 'Young Gerald']);
$client = Client::query()->create(['name' => 'Hans Thomas']);
$extra = Client::query()->create(['name' => 'John Doe']);
$label = Label::query()->create(['name' => 'Never finished, tryna search for more']);
$label->users()->attach($user);
$label->clients()->attach($client);
$this->assertEquals(1, $label->users->count());
$this->assertContains($user->id, $label->users->pluck('id'));
$this->assertEquals(1, $label->clients->count());
$this->assertContains($client->id, $label->clients->pluck('id'));
}
public function testMorphedByManyAttachEloquentCollection(): void
{
$client1 = Client::query()->create(['name' => 'Young Gerald']);
$client2 = Client::query()->create(['name' => 'Hans Thomas']);
$extra = Client::query()->create(['name' => 'John Doe']);
$label = Label::query()->create(['name' => 'They want me to architect Rome, in a day']);
$label->clients()->attach(new Collection([$client1, $client2]));
$this->assertEquals(2, $label->clients->count());
$this->assertContains($client1->id, $label->clients->pluck('id'));
$this->assertContains($client2->id, $label->clients->pluck('id'));
$client1->refresh();
$this->assertEquals(1, $client1->labels->count());
}
public function testMorphedByManyAttachMultipleIds(): void
{
$client1 = Client::query()->create(['name' => 'Austin Richard Post']);
$client2 = Client::query()->create(['name' => 'Hans Thomas']);
$extra = Client::query()->create(['name' => 'John Doe']);
$label = Label::query()->create(['name' => 'Always in the game and never played by the rules']);
$label->clients()->attach([$client1->id, $client2->id]);
$this->assertEquals(2, $label->clients->count());
$this->assertContains($client1->id, $label->clients->pluck('id'));
$this->assertContains($client2->id, $label->clients->pluck('id'));
$client1->refresh();
$this->assertEquals(1, $client1->labels->count());
}
public function testMorphedByManyDetaching(): void
{
$client1 = Client::query()->create(['name' => 'Austin Richard Post']);
$client2 = Client::query()->create(['name' => 'Hans Thomas']);
$extra = Client::query()->create(['name' => 'John Doe']);
$label = Label::query()->create(['name' => 'Seasons change and our love went cold']);
$label->clients()->attach([$client1->id, $client2->id]);
$this->assertEquals(2, $label->clients->count());
$label->clients()->detach($client1->id);
$check = $label->withoutRelations();
$this->assertEquals(1, $check->clients->count());
$this->assertContains($client2->id, $check->clients->pluck('id'));
}
public function testMorphedByManyDetachingMultipleIds(): void
{
$client1 = Client::query()->create(['name' => 'Austin Richard Post']);
$client2 = Client::query()->create(['name' => 'Hans Thomas']);
$client3 = Client::query()->create(['name' => 'John Doe']);
$label = Label::query()->create(['name' => "Run away, but we're running in circles"]);
$label->clients()->attach([$client1->id, $client2->id, $client3->id]);
$this->assertEquals(3, $label->clients->count());
$label->clients()->detach([$client1->id, $client2->id]);
$label->load('clients');
$this->assertEquals(1, $label->clients->count());
$this->assertContains($client3->id, $label->clients->pluck('id'));
}
public function testMorphedByManySyncing(): void
{
$client1 = Client::query()->create(['name' => 'Austin Richard Post']);
$client2 = Client::query()->create(['name' => 'Hans Thomas']);
$client3 = Client::query()->create(['name' => 'John Doe']);
$label = Label::query()->create(['name' => "Was scared of losin' somethin' that we never found"]);
$label->clients()->sync($client1);
$label->clients()->sync($client2, false);
$label->clients()->sync($client3, false);
$this->assertEquals(3, $label->clients->count());
$this->assertContains($client1->id, $label->clients->pluck('id'));
$this->assertContains($client2->id, $label->clients->pluck('id'));
$this->assertContains($client3->id, $label->clients->pluck('id'));
}
public function testMorphedByManySyncingEloquentCollection(): void
{
$client1 = Client::query()->create(['name' => 'Austin Richard Post']);
$client2 = Client::query()->create(['name' => 'Hans Thomas']);
$extra = Client::query()->create(['name' => 'John Doe']);
$label = Label::query()->create(['name' => "I'm goin' hard 'til I'm gone. Can you feel it?"]);
$label->clients()->sync(new Collection([$client1, $client2]));
$this->assertEquals(2, $label->clients->count());
$this->assertContains($client1->id, $label->clients->pluck('id'));
$this->assertContains($client2->id, $label->clients->pluck('id'));
$this->assertNotContains($extra->id, $label->clients->pluck('id'));
}
public function testMorphedByManySyncingMultipleIds(): void
{
$client1 = Client::query()->create(['name' => 'Dorothy']);
$client2 = Client::query()->create(['name' => 'Hans Thomas']);
$extra = Client::query()->create(['name' => 'John Doe']);
$label = Label::query()->create(['name' => "Love ain't patient, it's not kind. true love waits to rob you blind"]);
$label->clients()->sync([$client1->id, $client2->id]);
$this->assertEquals(2, $label->clients->count());
$this->assertContains($client1->id, $label->clients->pluck('id'));
$this->assertContains($client2->id, $label->clients->pluck('id'));
$this->assertNotContains($extra->id, $label->clients->pluck('id'));
}
public function testMorphedByManySyncingWithCustomKeys(): void
{
$client1 = Client::query()->create(['cclient_id' => (string) (new ObjectId()), 'name' => 'Young Gerald']);
$client2 = Client::query()->create(['cclient_id' => (string) (new ObjectId()), 'name' => 'Hans Thomas']);
$client3 = Client::query()->create(['cclient_id' => (string) (new ObjectId()), 'name' => 'John Doe']);
$label = Label::query()->create(['clabel_id' => (string) (new ObjectId()), 'name' => "I'm in my own lane, so what do I have to hurry for?"]);
$label->clientsWithCustomKeys()->sync([$client1->cclient_id, $client2->cclient_id]);
$this->assertEquals(2, $label->clientsWithCustomKeys->count());
$this->assertContains($client1->id, $label->clientsWithCustomKeys->pluck('id'));
$this->assertContains($client2->id, $label->clientsWithCustomKeys->pluck('id'));
$this->assertNotContains($client3->id, $label->clientsWithCustomKeys->pluck('id'));
$label->clientsWithCustomKeys()->sync($client3);
$label->load('clientsWithCustomKeys');
$this->assertEquals(1, $label->clientsWithCustomKeys->count());
$this->assertNotContains($client1->id, $label->clientsWithCustomKeys->pluck('id'));
$this->assertNotContains($client2->id, $label->clientsWithCustomKeys->pluck('id'));
$this->assertContains($client3->id, $label->clientsWithCustomKeys->pluck('id'));
}
public function testMorphedByManyLoadAndRefreshing(): void
{
$user = User::query()->create(['name' => 'Abel Tesfaye']);
$client1 = Client::query()->create(['name' => 'Young Gerald']);
$client2 = Client::query()->create(['name' => 'Hans Thomas']);
$client3 = Client::query()->create(['name' => 'John Doe']);
$label = Label::query()->create(['name' => "but don't think I don't think about you just cause I ain't spoken about you"]);
$label->clients()->sync(new Collection([$client1, $client2, $client3]));
$label->users()->sync($user);
$this->assertEquals(3, $label->clients->count());
$label->load('clients');
$this->assertEquals(3, $label->clients->count());
$label->refresh();
$this->assertEquals(3, $label->clients->count());
$check = Label::query()->find($label->id);
$this->assertEquals(3, $check->clients->count());
$check = Label::query()->with('clients')->find($label->id);
$this->assertEquals(3, $check->clients->count());
}
public function testMorphedByManyHasQuery(): void
{
$user = User::query()->create(['name' => 'Austin Richard Post']);
$client1 = Client::query()->create(['name' => 'Young Gerald']);
$client2 = Client::query()->create(['name' => 'John Doe']);
$label = Label::query()->create(['name' => "My star's back shining bright, I just polished it"]);
$label2 = Label::query()->create(['name' => "Somethin' in my spirit woke back up like I just sat up"]);
$label3 = Label::query()->create(['name' => 'How can I beam when you blocking my light?']);
$label->clients()->sync(new Collection([$client1, $client2]));
$label2->clients()->sync($client1);
$label3->users()->sync($user);
$this->assertEquals(2, $label->clients->count());
$check = Label::query()->has('clients')->get();
$this->assertCount(2, $check);
$this->assertContains($label->id, $check->pluck('id'));
$this->assertContains($label2->id, $check->pluck('id'));
$check = Label::query()->has('users')->get();
$this->assertCount(1, $check);
$this->assertContains($label3->id, $check->pluck('id'));
$check = Label::query()->has('clients', '>', 1)->get();
$this->assertCount(1, $check);
$this->assertContains($label->id, $check->pluck('id'));
}
public function testHasManyHas(): void
{
$author1 = User::create(['name' => 'George R. R. Martin']);
$author1->books()->create(['title' => 'A Game of Thrones', 'rating' => 5]);
$author1->books()->create(['title' => 'A Clash of Kings', 'rating' => 5]);
$author2 = User::create(['name' => 'John Doe']);
$author2->books()->create(['title' => 'My book', 'rating' => 2]);
User::create(['name' => 'Anonymous author']);
Book::create(['title' => 'Anonymous book', 'rating' => 1]);
$authors = User::has('books')->get();
$this->assertCount(2, $authors);
$this->assertEquals('George R. R. Martin', $authors[0]->name);
$this->assertEquals('John Doe', $authors[1]->name);
$authors = User::has('books', '>', 1)->get();
$this->assertCount(1, $authors);
$authors = User::has('books', '<', 5)->get();
$this->assertCount(3, $authors);
$authors = User::has('books', '>=', 2)->get();
$this->assertCount(1, $authors);
$authors = User::has('books', '<=', 1)->get();
$this->assertCount(2, $authors);
$authors = User::has('books', '=', 2)->get();
$this->assertCount(1, $authors);
$authors = User::has('books', '!=', 2)->get();
$this->assertCount(2, $authors);
$authors = User::has('books', '=', 0)->get();
$this->assertCount(1, $authors);
$authors = User::has('books', '!=', 0)->get();
$this->assertCount(2, $authors);
$authors = User::whereHas('books', function ($query) {
$query->where('rating', 5);
})->get();
$this->assertCount(1, $authors);
$authors = User::whereHas('books', function ($query) {
$query->where('rating', '<', 5);
})->get();
$this->assertCount(1, $authors);
}
public function testHasOneHas(): void
{
$user1 = User::create(['name' => 'John Doe']);
$user1->role()->create(['title' => 'admin']);
$user2 = User::create(['name' => 'Jane Doe']);
$user2->role()->create(['title' => 'reseller']);
User::create(['name' => 'Mark Moe']);
Role::create(['title' => 'Customer']);
$users = User::has('role')->get();
$this->assertCount(2, $users);
$this->assertEquals('John Doe', $users[0]->name);
$this->assertEquals('Jane Doe', $users[1]->name);
$users = User::has('role', '=', 0)->get();
$this->assertCount(1, $users);
$users = User::has('role', '!=', 0)->get();
$this->assertCount(2, $users);
}
public function testNestedKeys(): void
{
$client = Client::create([
'data' => [
'client_id' => 35298,
'name' => 'John Doe',
],
]);
$client->addresses()->create([
'data' => [
'address_id' => 1432,
'city' => 'Paris',
],
]);
$client = Client::where('data.client_id', 35298)->first();
$this->assertEquals(1, $client->addresses->count());
$address = $client->addresses->first();
$this->assertEquals('Paris', $address->data['city']);
$client = Client::with('addresses')->first();
$this->assertEquals('Paris', $client->addresses->first()->data['city']);
}
public function testDoubleSaveOneToMany(): void
{
$author = User::create(['name' => 'George R. R. Martin']);
$book = Book::create(['title' => 'A Game of Thrones']);
$author->books()->save($book);
$author->books()->save($book);
$author->save();
$this->assertEquals(1, $author->books()->count());
$this->assertEquals($author->id, $book->author_id);
$author = User::where('name', 'George R. R. Martin')->first();
$book = Book::where('title', 'A Game of Thrones')->first();
$this->assertEquals(1, $author->books()->count());
$this->assertEquals($author->id, $book->author_id);
$author->books()->save($book);
$author->books()->save($book);
$author->save();
$this->assertEquals(1, $author->books()->count());
$this->assertEquals($author->id, $book->author_id);
}
public function testDoubleSaveManyToMany(): void
{
$user = User::create(['name' => 'John Doe']);
$client = Client::create(['name' => 'Admins']);
$user->clients()->save($client);
$user->clients()->save($client);
$user->save();
$this->assertEquals(1, $user->clients()->count());
$this->assertEquals([$user->id], $client->user_ids);
$this->assertEquals([$client->id], $user->client_ids);
$user = User::where('name', 'John Doe')->first();
$client = Client::where('name', 'Admins')->first();
$this->assertEquals(1, $user->clients()->count());
$this->assertEquals([$user->id], $client->user_ids);
$this->assertEquals([$client->id], $user->client_ids);
$user->clients()->save($client);
$user->clients()->save($client);
$user->save();
$this->assertEquals(1, $user->clients()->count());
$this->assertEquals([$user->id], $client->user_ids);
$this->assertEquals([$client->id], $user->client_ids);
}
public function testWhereBelongsTo()
{
$user = User::create(['name' => 'John Doe']);
Item::create(['user_id' => $user->id]);
Item::create(['user_id' => $user->id]);
Item::create(['user_id' => $user->id]);
Item::create(['user_id' => null]);
$items = Item::whereBelongsTo($user)->get();
$this->assertCount(3, $items);
}
}
================================================
FILE: tests/SchemaTest.php
================================================
getConnection('mongodb')->getDatabase();
assert($database instanceof Database);
$database->dropCollection(self::COLL_1);
$database->dropCollection(self::COLL_2);
$database->dropCollection(self::COLL_WITH_COLLATION);
$database->dropCollection('test_view');
parent::tearDown();
}
public function testCreate(): void
{
Schema::create(self::COLL_1);
$this->assertTrue(Schema::hasCollection(self::COLL_1));
$this->assertTrue(Schema::hasTable(self::COLL_1));
}
public function testCreateWithCallback(): void
{
Schema::create(self::COLL_1, static function ($collection) {
self::assertInstanceOf(Blueprint::class, $collection);
});
$this->assertTrue(Schema::hasCollection(self::COLL_1));
}
public function testCreateWithOptions(): void
{
Schema::create(self::COLL_2, null, ['capped' => true, 'size' => 1024]);
$this->assertTrue(Schema::hasCollection(self::COLL_2));
$this->assertTrue(Schema::hasTable(self::COLL_2));
$collection = Schema::getCollection(self::COLL_2);
$this->assertTrue($collection['options']['capped']);
$this->assertEquals(1024, $collection['options']['size']);
}
public function testCreateWithSchemaValidator(): void
{
$schema = [
'bsonType' => 'object',
'required' => [ 'username' ],
'properties' => [
'username' => [
'bsonType' => 'string',
'description' => 'must be a string and is required',
],
],
];
Schema::create(self::COLL_2, function (Blueprint $collection) use ($schema) {
$collection->string('username');
$collection->jsonSchema(schema: $schema, validationAction: 'warn');
});
$this->assertTrue(Schema::hasCollection(self::COLL_2));
$this->assertTrue(Schema::hasTable(self::COLL_2));
$collection = Schema::getCollection(self::COLL_2);
$this->assertEquals(
['$jsonSchema' => $schema],
$collection['options']['validator'],
);
$this->assertEquals(
'warn',
$collection['options']['validationAction'],
);
}
public function testDrop(): void
{
Schema::create(self::COLL_1);
Schema::drop(self::COLL_1);
$this->assertFalse(Schema::hasCollection(self::COLL_1));
}
public function testBluePrint(): void
{
Schema::table(self::COLL_1, static function ($collection) {
self::assertInstanceOf(Blueprint::class, $collection);
});
Schema::table(self::COLL_1, static function ($collection) {
self::assertInstanceOf(Blueprint::class, $collection);
});
}
public function testIndex(): void
{
Schema::table(self::COLL_1, function ($collection) {
$collection->index('mykey1');
});
$index = $this->assertIndexExists(self::COLL_1, 'mykey1_1');
$this->assertEquals(1, $index['key']['mykey1']);
Schema::table(self::COLL_1, function ($collection) {
$collection->index(['mykey2']);
});
$index = $this->assertIndexExists(self::COLL_1, 'mykey2_1');
$this->assertEquals(1, $index['key']['mykey2']);
Schema::table(self::COLL_1, function ($collection) {
$collection->string('mykey3')->index();
});
$index = $this->assertIndexExists(self::COLL_1, 'mykey3_1');
$this->assertEquals(1, $index['key']['mykey3']);
}
public function testPrimary(): void
{
Schema::table(self::COLL_1, function ($collection) {
$collection->string('mykey', 100)->primary();
});
$index = $this->assertIndexExists(self::COLL_1, 'mykey_1');
$this->assertEquals(1, $index['unique']);
}
public function testUnique(): void
{
Schema::table(self::COLL_1, function ($collection) {
$collection->unique('uniquekey');
});
$index = $this->assertIndexExists(self::COLL_1, 'uniquekey_1');
$this->assertEquals(1, $index['unique']);
}
public function testDropIndex(): void
{
Schema::table(self::COLL_1, function ($collection) {
$collection->unique('uniquekey');
$collection->dropIndex('uniquekey_1');
});
$this->assertIndexNotExists(self::COLL_1, 'uniquekey_1');
Schema::table(self::COLL_1, function ($collection) {
$collection->unique('uniquekey');
$collection->dropIndex(['uniquekey']);
});
$this->assertIndexNotExists(self::COLL_1, 'uniquekey_1');
Schema::table(self::COLL_1, function ($collection) {
$collection->index(['field_a', 'field_b']);
});
$this->assertIndexExists(self::COLL_1, 'field_a_1_field_b_1');
Schema::table(self::COLL_1, function ($collection) {
$collection->dropIndex(['field_a', 'field_b']);
});
$this->assertIndexNotExists(self::COLL_1, 'field_a_1_field_b_1');
$indexName = 'field_a_-1_field_b_1';
Schema::table(self::COLL_1, function ($collection) {
$collection->index(['field_a' => -1, 'field_b' => 1]);
});
$this->assertIndexExists(self::COLL_1, $indexName);
Schema::table(self::COLL_1, function ($collection) {
$collection->dropIndex(['field_a' => -1, 'field_b' => 1]);
});
$this->assertIndexNotExists(self::COLL_1, $indexName);
$indexName = 'custom_index_name';
Schema::table(self::COLL_1, function ($collection) use ($indexName) {
$collection->index(['field_a', 'field_b'], $indexName);
});
$this->assertIndexExists(self::COLL_1, $indexName);
Schema::table(self::COLL_1, function ($collection) use ($indexName) {
$collection->dropIndex($indexName);
});
$this->assertIndexNotExists(self::COLL_1, $indexName);
}
public function testDropIndexIfExists(): void
{
Schema::table(self::COLL_1, function (Blueprint $collection) {
$collection->unique('uniquekey');
$collection->dropIndexIfExists('uniquekey_1');
});
$this->assertIndexNotExists(self::COLL_1, 'uniquekey');
Schema::table(self::COLL_1, function (Blueprint $collection) {
$collection->unique('uniquekey');
$collection->dropIndexIfExists(['uniquekey']);
});
$this->assertIndexNotExists(self::COLL_1, 'uniquekey');
Schema::table(self::COLL_1, function (Blueprint $collection) {
$collection->index(['field_a', 'field_b']);
});
$this->assertIndexExists(self::COLL_1, 'field_a_1_field_b_1');
Schema::table(self::COLL_1, function (Blueprint $collection) {
$collection->dropIndexIfExists(['field_a', 'field_b']);
});
$this->assertIndexNotExists(self::COLL_1, 'field_a_1_field_b_1');
Schema::table(self::COLL_1, function (Blueprint $collection) {
$collection->index(['field_a', 'field_b'], 'custom_index_name');
});
$this->assertIndexExists(self::COLL_1, 'custom_index_name');
Schema::table(self::COLL_1, function (Blueprint $collection) {
$collection->dropIndexIfExists('custom_index_name');
});
$this->assertIndexNotExists(self::COLL_1, 'custom_index_name');
}
public function testHasIndex(): void
{
Schema::table(self::COLL_1, function (Blueprint $collection) {
$collection->index('myhaskey1');
$this->assertTrue($collection->hasIndex('myhaskey1_1'));
$this->assertFalse($collection->hasIndex('myhaskey1'));
});
Schema::table(self::COLL_1, function (Blueprint $collection) {
$collection->index('myhaskey2');
$this->assertTrue($collection->hasIndex(['myhaskey2']));
$this->assertFalse($collection->hasIndex(['myhaskey2_1']));
});
Schema::table(self::COLL_1, function (Blueprint $collection) {
$collection->index(['field_a', 'field_b']);
$this->assertTrue($collection->hasIndex(['field_a_1_field_b']));
$this->assertFalse($collection->hasIndex(['field_a_1_field_b_1']));
});
}
public function testSparse(): void
{
Schema::table(self::COLL_1, function ($collection) {
$collection->sparse('sparsekey');
});
$index = $this->assertIndexExists(self::COLL_1, 'sparsekey_1');
$this->assertEquals(1, $index['sparse']);
}
public function testExpire(): void
{
Schema::table(self::COLL_1, function ($collection) {
$collection->expire('expirekey', 60);
});
$index = $this->assertIndexExists(self::COLL_1, 'expirekey_1');
$this->assertEquals(60, $index['expireAfterSeconds']);
}
public function testSoftDeletes(): void
{
Schema::table(self::COLL_1, function ($collection) {
$collection->softDeletes();
});
Schema::table(self::COLL_1, function ($collection) {
$collection->string('email')->nullable()->index();
});
$index = $this->assertIndexExists(self::COLL_1, 'email_1');
$this->assertEquals(1, $index['key']['email']);
}
public function testFluent(): void
{
Schema::table(self::COLL_1, function ($collection) {
$collection->string('email')->index();
$collection->string('token')->index();
$collection->timestamp('created_at');
});
$index = $this->assertIndexExists(self::COLL_1, 'email_1');
$this->assertEquals(1, $index['key']['email']);
$index = $this->assertIndexExists(self::COLL_1, 'token_1');
$this->assertEquals(1, $index['key']['token']);
}
public function testGeospatial(): void
{
Schema::table(self::COLL_1, function ($collection) {
$collection->geospatial('point');
$collection->geospatial('area', '2d');
$collection->geospatial('continent', '2dsphere');
});
$index = $this->assertIndexExists(self::COLL_1, 'point_2d');
$this->assertEquals('2d', $index['key']['point']);
$index = $this->assertIndexExists(self::COLL_1, 'area_2d');
$this->assertEquals('2d', $index['key']['area']);
$index = $this->assertIndexExists(self::COLL_1, 'continent_2dsphere');
$this->assertEquals('2dsphere', $index['key']['continent']);
}
public function testDummies(): void
{
Schema::table(self::COLL_1, function ($collection) {
$collection->boolean('activated')->default(0);
$collection->integer('user_id')->unsigned();
});
$this->expectNotToPerformAssertions();
}
public function testSparseUnique(): void
{
Schema::table(self::COLL_1, function ($collection) {
$collection->sparse_and_unique('sparseuniquekey');
});
$index = $this->assertIndexExists(self::COLL_1, 'sparseuniquekey_1');
$this->assertEquals(1, $index['sparse']);
$this->assertEquals(1, $index['unique']);
}
public function testRenameColumn(): void
{
DB::connection()->table(self::COLL_1)->insert(['test' => 'value']);
DB::connection()->table(self::COLL_1)->insert(['test' => 'value 2']);
DB::connection()->table(self::COLL_1)->insert(['column' => 'column value']);
$check = DB::connection()->table(self::COLL_1)->get();
$this->assertCount(3, $check);
$this->assertObjectHasProperty('test', $check[0]);
$this->assertObjectNotHasProperty('newtest', $check[0]);
$this->assertObjectHasProperty('test', $check[1]);
$this->assertObjectNotHasProperty('newtest', $check[1]);
$this->assertObjectHasProperty('column', $check[2]);
$this->assertObjectNotHasProperty('test', $check[2]);
$this->assertObjectNotHasProperty('newtest', $check[2]);
Schema::table(self::COLL_1, function (Blueprint $collection) {
$collection->renameColumn('test', 'newtest');
});
$check2 = DB::connection()->table(self::COLL_1)->get();
$this->assertCount(3, $check2);
$this->assertObjectHasProperty('newtest', $check2[0]);
$this->assertObjectNotHasProperty('test', $check2[0]);
$this->assertSame($check[0]->test, $check2[0]->newtest);
$this->assertObjectHasProperty('newtest', $check2[1]);
$this->assertObjectNotHasProperty('test', $check2[1]);
$this->assertSame($check[1]->test, $check2[1]->newtest);
$this->assertObjectHasProperty('column', $check2[2]);
$this->assertObjectNotHasProperty('test', $check2[2]);
$this->assertObjectNotHasProperty('newtest', $check2[2]);
$this->assertSame($check[2]->column, $check2[2]->column);
}
public function testHasColumn(): void
{
$this->assertTrue(Schema::hasColumn(self::COLL_1, '_id'));
$this->assertTrue(Schema::hasColumn(self::COLL_1, 'id'));
DB::connection()->table(self::COLL_1)->insert(['column1' => 'value', 'embed' => ['_id' => 1]]);
$this->assertTrue(Schema::hasColumn(self::COLL_1, 'column1'));
$this->assertFalse(Schema::hasColumn(self::COLL_1, 'column2'));
$this->assertTrue(Schema::hasColumn(self::COLL_1, 'embed._id'));
$this->assertTrue(Schema::hasColumn(self::COLL_1, 'embed.id'));
}
public function testHasColumns(): void
{
$this->assertTrue(Schema::hasColumns(self::COLL_1, ['_id']));
$this->assertTrue(Schema::hasColumns(self::COLL_1, ['id']));
// Insert documents with both column1 and column2
DB::connection()->table(self::COLL_1)->insert([
['column1' => 'value1', 'column2' => 'value2'],
['column1' => 'value3'],
]);
$this->assertTrue(Schema::hasColumns(self::COLL_1, ['column1', 'column2']));
$this->assertFalse(Schema::hasColumns(self::COLL_1, ['column1', 'column3']));
}
public function testGetTables()
{
$db = DB::connection('mongodb')->getDatabase();
$db->createCollection(self::COLL_WITH_COLLATION, [
'collation' => [
'locale' => 'fr',
'strength' => 2,
],
]);
DB::connection('mongodb')->table(self::COLL_1)->insert(['test' => 'value']);
DB::connection('mongodb')->table(self::COLL_2)->insert(['test' => 'value']);
$db->createCollection('test_view', ['viewOn' => self::COLL_1]);
$dbName = DB::connection('mongodb')->getDatabaseName();
$tables = Schema::getTables();
$this->assertIsArray($tables);
$this->assertGreaterThanOrEqual(2, count($tables));
$found = false;
foreach ($tables as $table) {
$this->assertArrayHasKey('name', $table);
$this->assertArrayHasKey('size', $table);
$this->assertArrayHasKey('schema', $table);
$this->assertArrayHasKey('collation', $table);
$this->assertArrayHasKey('schema_qualified_name', $table);
$this->assertNotEquals('test_view', $table['name'], 'Standard views should not be included in the result of getTables.');
if ($table['name'] === self::COLL_1) {
$this->assertGreaterThanOrEqual(8192, $table['size']);
$this->assertEquals($dbName, $table['schema']);
$this->assertEquals($dbName . '.' . self::COLL_1, $table['schema_qualified_name']);
$found = true;
}
if ($table['name'] === self::COLL_WITH_COLLATION) {
$this->assertEquals('l=fr;cl=0;cf=off;s=2;no=0;a=non-ignorable;mv=punct;n=0;b=0', $table['collation']);
}
}
if (! $found) {
$this->fail('Collection "' . self::COLL_1 . '" not found');
}
}
public function testGetViews()
{
DB::connection('mongodb')->table(self::COLL_1)->insert(['test' => 'value']);
DB::connection('mongodb')->table(self::COLL_2)->insert(['test' => 'value']);
$dbName = DB::connection('mongodb')->getDatabaseName();
DB::connection('mongodb')->getDatabase()->createCollection('test_view', ['viewOn' => self::COLL_1]);
$tables = Schema::getViews();
$this->assertIsArray($tables);
$this->assertGreaterThanOrEqual(1, count($tables));
$found = false;
foreach ($tables as $table) {
$this->assertArrayHasKey('name', $table);
$this->assertArrayHasKey('size', $table);
$this->assertArrayHasKey('schema', $table);
$this->assertArrayHasKey('schema_qualified_name', $table);
// Ensure "normal collections" are not in the views list
$this->assertNotEquals(self::COLL_1, $table['name'], 'Normal collections should not be included in the result of getViews.');
if ($table['name'] === 'test_view') {
$this->assertEquals($dbName, $table['schema']);
$this->assertEquals($dbName . '.test_view', $table['schema_qualified_name']);
$found = true;
}
}
if (! $found) {
$this->fail('Collection "test_view" not found');
}
}
public function testGetTableListing()
{
DB::connection('mongodb')->table(self::COLL_1)->insert(['test' => 'value']);
DB::connection('mongodb')->table(self::COLL_2)->insert(['test' => 'value']);
$tables = Schema::getTableListing();
$this->assertIsArray($tables);
$this->assertGreaterThanOrEqual(2, count($tables));
$this->assertContains(self::COLL_1, $tables);
$this->assertContains(self::COLL_2, $tables);
}
public function testGetTableListingBySchema()
{
DB::connection('mongodb')->table(self::COLL_1)->insert(['test' => 'value']);
DB::connection('mongodb')->table(self::COLL_2)->insert(['test' => 'value']);
$dbName = DB::connection('mongodb')->getDatabaseName();
$tables = Schema::getTableListing([$dbName, 'database__that_does_not_exists'], schemaQualified: true);
$this->assertIsArray($tables);
$this->assertGreaterThanOrEqual(2, count($tables));
$this->assertContains($dbName . '.' . self::COLL_1, $tables);
$this->assertContains($dbName . '.' . self::COLL_2, $tables);
$tables = Schema::getTableListing([$dbName, 'database__that_does_not_exists'], schemaQualified: false);
$this->assertIsArray($tables);
$this->assertGreaterThanOrEqual(2, count($tables));
$this->assertContains(self::COLL_1, $tables);
$this->assertContains(self::COLL_2, $tables);
}
public function testGetColumns()
{
$collection = DB::connection('mongodb')->table(self::COLL_1);
$collection->insert(['text' => 'value', 'mixed' => ['key' => 'value']]);
$collection->insert(['date' => new UTCDateTime(), 'binary' => new Binary('binary'), 'mixed' => true]);
$columns = Schema::getColumns(self::COLL_1);
$this->assertIsArray($columns);
$this->assertCount(5, $columns);
$columns = collect($columns)->keyBy('name');
$columns->each(function ($column) {
$this->assertIsString($column['name']);
$this->assertEquals($column['type'], $column['type_name']);
$this->assertNull($column['collation']);
$this->assertIsBool($column['nullable']);
$this->assertNull($column['default']);
$this->assertFalse($column['auto_increment']);
$this->assertIsString($column['comment']);
});
$this->assertNull($columns->get('_id'), '_id is renamed to id');
$this->assertEquals('objectId', $columns->get('id')['type']);
$this->assertEquals('objectId', $columns->get('id')['generation']['type']);
$this->assertNull($columns->get('text')['generation']);
$this->assertEquals('string', $columns->get('text')['type']);
$this->assertEquals('date', $columns->get('date')['type']);
$this->assertEquals('binData', $columns->get('binary')['type']);
$this->assertEquals('bool, object', $columns->get('mixed')['type']);
$this->assertEquals('2 occurrences', $columns->get('mixed')['comment']);
// Non-existent collection
$columns = Schema::getColumns('missing');
$this->assertSame([], $columns);
// Qualified table name
$columns = Schema::getColumns(DB::getDatabaseName() . '.' . self::COLL_1);
$this->assertIsArray($columns);
$this->assertCount(5, $columns);
}
/** @see AtlasSearchTest::testGetIndexes() */
public function testGetIndexes()
{
Schema::create(self::COLL_1, function (Blueprint $collection) {
$collection->index('mykey1');
$collection->string('mykey2')->unique('unique_index');
$collection->string('mykey3')->index();
});
$indexes = Schema::getIndexes(self::COLL_1);
self::assertIsArray($indexes);
self::assertCount(4, $indexes);
$expected = [
[
'name' => '_id_',
'columns' => ['_id'],
'primary' => true,
'type' => null,
'unique' => false,
],
[
'name' => 'mykey1_1',
'columns' => ['mykey1'],
'primary' => false,
'type' => null,
'unique' => false,
],
[
'name' => 'unique_index_1',
'columns' => ['unique_index'],
'primary' => false,
'type' => null,
'unique' => true,
],
[
'name' => 'mykey3_1',
'columns' => ['mykey3'],
'primary' => false,
'type' => null,
'unique' => false,
],
];
self::assertSame($expected, $indexes);
// Non-existent collection
$indexes = Schema::getIndexes('missing');
$this->assertSame([], $indexes);
}
public function testSearchIndex(): void
{
$this->skipIfSearchIndexManagementIsNotSupported();
Schema::create(self::COLL_1, function (Blueprint $collection) {
$collection->searchIndex([
'mappings' => [
'dynamic' => false,
'fields' => [
'foo' => ['type' => 'string', 'analyzer' => 'lucene.whitespace'],
],
],
]);
});
$index = $this->getSearchIndex(self::COLL_1, 'default');
self::assertNotNull($index);
self::assertSame('default', $index['name']);
self::assertSame('search', $index['type']);
self::assertFalse($index['latestDefinition']['mappings']['dynamic']);
self::assertSame('lucene.whitespace', $index['latestDefinition']['mappings']['fields']['foo']['analyzer']);
Schema::table(self::COLL_1, function (Blueprint $collection) {
$collection->dropSearchIndex('default');
});
$index = $this->getSearchIndex(self::COLL_1, 'default');
self::assertNull($index);
}
public function testVectorSearchIndex()
{
$this->skipIfSearchIndexManagementIsNotSupported();
Schema::create(self::COLL_1, function (Blueprint $collection) {
$collection->vectorSearchIndex([
'fields' => [
['type' => 'vector', 'path' => 'foo', 'numDimensions' => 128, 'similarity' => 'euclidean', 'quantization' => 'none'],
],
], 'vector');
});
$index = $this->getSearchIndex(self::COLL_1, 'vector');
self::assertNotNull($index);
self::assertSame('vector', $index['name']);
self::assertSame('vectorSearch', $index['type']);
self::assertSame('vector', $index['latestDefinition']['fields'][0]['type']);
// Drop the index
Schema::table(self::COLL_1, function (Blueprint $collection) {
$collection->dropSearchIndex('vector');
});
$index = $this->getSearchIndex(self::COLL_1, 'vector');
self::assertNull($index);
}
protected function assertIndexExists(string $collection, string $name): IndexInfo
{
$index = $this->getIndex($collection, $name);
self::assertNotNull($index, sprintf('Index "%s.%s" does not exist.', $collection, $name));
return $index;
}
protected function assertIndexNotExists(string $collection, string $name): void
{
$index = $this->getIndex($collection, $name);
self::assertNull($index, sprintf('Index "%s.%s" exists.', $collection, $name));
}
protected function getIndex(string $collection, string $name): ?IndexInfo
{
$collection = $this->getConnection('mongodb')->getCollection($collection);
assert($collection instanceof Collection);
foreach ($collection->listIndexes() as $index) {
if ($index->getName() === $name) {
return $index;
}
}
return null;
}
protected function getSearchIndex(string $collection, string $name): ?array
{
$collection = $this->getConnection('mongodb')->getCollection($collection);
assert($collection instanceof Collection);
foreach ($collection->listSearchIndexes(['name' => $name, 'typeMap' => ['root' => 'array', 'array' => 'array', 'document' => 'array']]) as $index) {
return $index;
}
return null;
}
}
================================================
FILE: tests/SchemaVersionTest.php
================================================
'Luc']);
$this->assertEmpty($document->getSchemaVersion());
$document->save();
// The current schema version of the model is stored by default
$this->assertEquals(2, $document->getSchemaVersion());
// Test automatic migration
SchemaVersion::insert([
['name' => 'Vador', 'schema_version' => 1],
]);
$document = SchemaVersion::where('name', 'Vador')->first();
$this->assertEquals(2, $document->getSchemaVersion());
$this->assertEquals(35, $document->age);
$document->save();
// The migrated version is saved
$data = DB::connection('mongodb')
->table('documentVersion')
->where('name', 'Vador')
->get();
$this->assertEquals(2, $data[0]->schema_version);
}
public function testIncompleteImplementation(): void
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('::SCHEMA_VERSION is required when using HasSchemaVersion');
$document = new class extends Model {
use HasSchemaVersion;
};
$document->save();
}
}
================================================
FILE: tests/Scout/Models/ScoutUser.php
================================================
dropIfExists('scout_users');
$schema->create('scout_users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->nullable();
$table->date('email_verified_at')->nullable();
$table->timestamps();
$table->softDeletes();
});
}
}
================================================
FILE: tests/Scout/Models/SearchableInSameNamespace.php
================================================
getTable();
}
}
================================================
FILE: tests/Scout/Models/SearchableModel.php
================================================
getAttribute($this->getScoutKeyName()) ?: 'key_' . $this->getKey();
}
/**
* This method must be overridden when the `getScoutKey` method is also overridden,
* to support model serialization for async indexing jobs.
*
* @see Searchable::getScoutKeyName()
*/
public function getScoutKeyName(): string
{
return 'scout_key';
}
}
================================================
FILE: tests/Scout/ScoutEngineTest.php
================================================
'object', 'document' => 'bson', 'array' => 'bson'];
public function testCreateIndexInvalidDefinition(): void
{
$database = $this->createMock(Database::class);
$engine = new ScoutEngine($database, false, ['collection_invalid' => ['foo' => 'bar']]);
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Invalid search index definition for collection "collection_invalid", the "mappings" key is required.');
$engine->createIndex('collection_invalid');
}
public function testCreateIndex(): void
{
$collectionName = 'collection_custom';
$expectedDefinition = [
'mappings' => ['dynamic' => true],
];
$database = $this->createMock(Database::class);
$collection = $this->createMock(Collection::class);
$database->expects($this->once())
->method('createCollection')
->with($collectionName);
$database->expects($this->once())
->method('selectCollection')
->with($collectionName)
->willReturn($collection);
$collection->expects($this->once())
->method('createSearchIndex')
->with($expectedDefinition, ['name' => 'scout']);
$collection->expects($this->once())
->method('listSearchIndexes')
->with(['name' => 'scout', 'typeMap' => ['root' => 'bson']])
->willReturn(new ArrayIterator([Document::fromPHP(['name' => 'scout', 'status' => 'READY'])]));
$engine = new ScoutEngine($database, false, []);
$engine->createIndex($collectionName);
}
public function testCreateIndexCustomDefinition(): void
{
$collectionName = 'collection_custom';
$expectedDefinition = [
'mappings' => [
[
'analyzer' => 'lucene.standard',
'fields' => [
[
'name' => 'wildcard',
'type' => 'string',
],
],
],
],
];
$database = $this->createMock(Database::class);
$collection = $this->createMock(Collection::class);
$database->expects($this->once())
->method('createCollection')
->with($collectionName);
$database->expects($this->once())
->method('selectCollection')
->with($collectionName)
->willReturn($collection);
$collection->expects($this->once())
->method('createSearchIndex')
->with($expectedDefinition, ['name' => 'scout']);
$collection->expects($this->once())
->method('listSearchIndexes')
->with(['name' => 'scout', 'typeMap' => ['root' => 'bson']])
->willReturn(new ArrayIterator([Document::fromPHP(['name' => 'scout', 'status' => 'READY'])]));
$engine = new ScoutEngine($database, false, [$collectionName => $expectedDefinition]);
$engine->createIndex($collectionName);
}
/** @param callable(): Builder $builder */
#[DataProvider('provideSearchPipelines')]
public function testSearch(Closure $builder, array $expectedPipeline): void
{
$data = [['_id' => 'key_1', '__count' => 15], ['_id' => 'key_2', '__count' => 15]];
$database = $this->createMock(Database::class);
$collection = $this->createMock(Collection::class);
$database->expects($this->once())
->method('selectCollection')
->with('collection_searchable')
->willReturn($collection);
$cursor = $this->createMock(CursorInterface::class);
$cursor->expects($this->once())
->method('setTypeMap')
->with(self::EXPECTED_TYPEMAP);
$cursor->expects($this->once())
->method('toArray')
->with()
->willReturn($data);
$collection->expects($this->any())
->method('getCollectionName')
->willReturn('collection_searchable');
$collection->expects($this->once())
->method('aggregate')
->with($expectedPipeline)
->willReturn($cursor);
$engine = new ScoutEngine($database, softDelete: false);
$result = $engine->search($builder());
$this->assertEquals($data, $result);
}
public static function provideSearchPipelines(): iterable
{
$defaultPipeline = [
[
'$search' => [
'index' => 'scout',
'compound' => [
'should' => [
[
'text' => [
'path' => ['wildcard' => '*'],
'query' => 'lar',
'fuzzy' => ['maxEdits' => 2],
'score' => ['boost' => ['value' => 5]],
],
],
[
'wildcard' => [
'query' => 'lar*',
'path' => ['wildcard' => '*'],
'allowAnalyzedField' => true,
],
],
],
'minimumShouldMatch' => 1,
],
'count' => ['type' => 'lowerBound'],
],
],
[
'$addFields' => ['__count' => '$$SEARCH_META.count.lowerBound'],
],
];
yield 'simple string' => [
function () {
return new Builder(new SearchableModel(), 'lar');
},
$defaultPipeline,
];
yield 'where conditions' => [
function () {
$builder = new Builder(new SearchableModel(), 'lar');
$builder->where('foo', 'bar');
$builder->where('key', 'value');
return $builder;
},
array_replace_recursive($defaultPipeline, [
[
'$search' => [
'compound' => [
'filter' => [
['equals' => ['path' => 'foo', 'value' => 'bar']],
['equals' => ['path' => 'key', 'value' => 'value']],
],
],
],
],
]),
];
yield 'where in conditions' => [
function () {
$builder = new Builder(new SearchableModel(), 'lar');
$builder->where('foo', 'bar');
$builder->where('bar', 'baz');
$builder->whereIn('qux', [1, 2]);
$builder->whereIn('quux', [1, 2]);
return $builder;
},
array_replace_recursive($defaultPipeline, [
[
'$search' => [
'compound' => [
'filter' => [
['equals' => ['path' => 'foo', 'value' => 'bar']],
['equals' => ['path' => 'bar', 'value' => 'baz']],
['in' => ['path' => 'qux', 'value' => [1, 2]]],
['in' => ['path' => 'quux', 'value' => [1, 2]]],
],
],
],
],
]),
];
yield 'where not in conditions' => [
function () {
$builder = new Builder(new SearchableModel(), 'lar');
$builder->where('foo', 'bar');
$builder->where('bar', 'baz');
$builder->whereIn('qux', [1, 2]);
$builder->whereIn('quux', [1, 2]);
$builder->whereNotIn('eaea', [3]);
return $builder;
},
array_replace_recursive($defaultPipeline, [
[
'$search' => [
'compound' => [
'filter' => [
['equals' => ['path' => 'foo', 'value' => 'bar']],
['equals' => ['path' => 'bar', 'value' => 'baz']],
['in' => ['path' => 'qux', 'value' => [1, 2]]],
['in' => ['path' => 'quux', 'value' => [1, 2]]],
],
'mustNot' => [
['in' => ['path' => 'eaea', 'value' => [3]]],
],
],
],
],
]),
];
yield 'where in conditions without other conditions' => [
function () {
$builder = new Builder(new SearchableModel(), 'lar');
$builder->whereIn('qux', [1, 2]);
$builder->whereIn('quux', [1, 2]);
return $builder;
},
array_replace_recursive($defaultPipeline, [
[
'$search' => [
'compound' => [
'filter' => [
['in' => ['path' => 'qux', 'value' => [1, 2]]],
['in' => ['path' => 'quux', 'value' => [1, 2]]],
],
],
],
],
]),
];
yield 'where not in conditions without other conditions' => [
function () {
$builder = new Builder(new SearchableModel(), 'lar');
$builder->whereIn('qux', [1, 2]);
$builder->whereIn('quux', [1, 2]);
$builder->whereNotIn('eaea', [3]);
return $builder;
},
array_replace_recursive($defaultPipeline, [
[
'$search' => [
'compound' => [
'filter' => [
['in' => ['path' => 'qux', 'value' => [1, 2]]],
['in' => ['path' => 'quux', 'value' => [1, 2]]],
],
'mustNot' => [
['in' => ['path' => 'eaea', 'value' => [3]]],
],
],
],
],
]),
];
yield 'empty where in conditions' => [
function () {
$builder = new Builder(new SearchableModel(), 'lar');
$builder->whereIn('qux', [1, 2]);
$builder->whereIn('quux', [1, 2]);
$builder->whereNotIn('eaea', [3]);
return $builder;
},
array_replace_recursive($defaultPipeline, [
[
'$search' => [
'compound' => [
'filter' => [
['in' => ['path' => 'qux', 'value' => [1, 2]]],
['in' => ['path' => 'quux', 'value' => [1, 2]]],
],
'mustNot' => [
['in' => ['path' => 'eaea', 'value' => [3]]],
],
],
],
],
]),
];
yield 'exclude soft-deleted' => [
function () {
return new Builder(new SearchableModel(), 'lar', softDelete: true);
},
array_replace_recursive($defaultPipeline, [
[
'$search' => [
'compound' => [
'filter' => [
['equals' => ['path' => '__soft_deleted', 'value' => false]],
],
],
],
],
]),
];
yield 'only trashed' => [
function () {
$builder = new Builder(new SearchableModel(), 'lar', softDelete: true);
$builder->onlyTrashed();
return $builder;
},
array_replace_recursive($defaultPipeline, [
[
'$search' => [
'compound' => [
'filter' => [
['equals' => ['path' => '__soft_deleted', 'value' => true]],
],
],
],
],
]),
];
yield 'with callback' => [
fn () => new Builder(new SearchableModel(), 'query', callback: function (...$args) {
self::assertCount(3, $args);
self::assertInstanceOf(Collection::class, $args[0]);
self::assertSame('collection_searchable', $args[0]->getCollectionName());
self::assertSame('query', $args[1]);
self::assertNull($args[2]);
return $args[0]->aggregate(['pipeline']);
}),
['pipeline'],
];
yield 'ordered' => [
function () {
$builder = new Builder(new SearchableModel(), 'lar');
$builder->orderBy('name', 'desc');
$builder->orderBy('age', 'asc');
return $builder;
},
array_replace_recursive($defaultPipeline, [
[
'$search' => [
'sort' => [
'name' => -1,
'age' => 1,
],
],
],
]),
];
}
public function testPaginate()
{
$perPage = 5;
$page = 3;
$database = $this->createMock(Database::class);
$collection = $this->createMock(Collection::class);
$cursor = $this->createMock(CursorInterface::class);
$database->method('selectCollection')
->with('collection_searchable')
->willReturn($collection);
$collection->expects($this->once())
->method('aggregate')
->willReturnCallback(function (...$args) use ($cursor) {
self::assertSame([
[
'$search' => [
'index' => 'scout',
'compound' => [
'should' => [
[
'text' => [
'query' => 'mustang',
'path' => ['wildcard' => '*'],
'fuzzy' => ['maxEdits' => 2],
'score' => ['boost' => ['value' => 5]],
],
],
[
'wildcard' => [
'query' => 'mustang*',
'path' => ['wildcard' => '*'],
'allowAnalyzedField' => true,
],
],
],
'minimumShouldMatch' => 1,
],
'count' => ['type' => 'lowerBound'],
'sort' => [
'name' => -1,
],
],
],
[
'$addFields' => ['__count' => '$$SEARCH_META.count.lowerBound'],
],
['$skip' => 10],
['$limit' => 5],
], $args[0]);
return $cursor;
});
$cursor->expects($this->once())->method('setTypeMap')->with(self::EXPECTED_TYPEMAP);
$cursor->expects($this->once())->method('toArray')->with()
->willReturn([['_id' => 'key_1', '__count' => 17], ['_id' => 'key_2', '__count' => 17]]);
$engine = new ScoutEngine($database, softDelete: false);
$builder = new Builder(new SearchableModel(), 'mustang');
$builder->orderBy('name', 'desc');
$engine->paginate($builder, $perPage, $page);
}
public function testMapMethodRespectsOrder()
{
$database = $this->createMock(Database::class);
$query = $this->createMock(Builder::class);
$engine = new ScoutEngine($database, false);
$model = $this->createMock(SearchableModel::class);
$model->expects($this->any())
->method('getScoutKeyName')
->willReturn('id');
$model->expects($this->once())
->method('queryScoutModelsByIds')
->willReturn($query);
$query->expects($this->once())
->method('get')
->willReturn(LaravelCollection::make([
new ScoutUser(['id' => 1]),
new ScoutUser(['id' => 2]),
new ScoutUser(['id' => 3]),
new ScoutUser(['id' => 4]),
]));
$builder = $this->createMock(Builder::class);
$results = $engine->map($builder, [
['_id' => 1, '__count' => 4],
['_id' => 2, '__count' => 4],
['_id' => 4, '__count' => 4],
['_id' => 3, '__count' => 4],
], $model);
$this->assertEquals(4, count($results));
$this->assertEquals([
0 => ['id' => 1],
1 => ['id' => 2],
2 => ['id' => 4],
3 => ['id' => 3],
], $results->toArray());
}
public function testLazyMapMethodRespectsOrder()
{
$database = $this->createMock(Database::class);
$query = $this->createMock(Builder::class);
$engine = new ScoutEngine($database, false);
$model = $this->createMock(SearchableModel::class);
$model->expects($this->any())
->method('getScoutKeyName')
->willReturn('id');
$model->expects($this->once())
->method('queryScoutModelsByIds')
->willReturn($query);
$query->expects($this->once())
->method('cursor')
->willReturn(LazyCollection::make([
new ScoutUser(['id' => 1]),
new ScoutUser(['id' => 2]),
new ScoutUser(['id' => 3]),
new ScoutUser(['id' => 4]),
]));
$builder = $this->createMock(Builder::class);
$results = $engine->lazyMap($builder, [
['_id' => 1, '__count' => 4],
['_id' => 2, '__count' => 4],
['_id' => 4, '__count' => 4],
['_id' => 3, '__count' => 4],
], $model);
$this->assertEquals(4, count($results));
$this->assertEquals([
0 => ['id' => 1],
1 => ['id' => 2],
2 => ['id' => 4],
3 => ['id' => 3],
], $results->toArray());
}
public function testUpdate(): void
{
$date = new DateTimeImmutable('2000-01-02 03:04:05');
$database = $this->createMock(Database::class);
$collection = $this->createMock(Collection::class);
$database->expects($this->once())
->method('selectCollection')
->with('collection_indexable')
->willReturn($collection);
$collection->expects($this->once())
->method('bulkWrite')
->with([
[
'updateOne' => [
['_id' => 'key_1'],
['$set' => ['id' => 1, 'date' => new UTCDateTime($date)]],
['upsert' => true],
],
],
[
'updateOne' => [
['_id' => 'key_2'],
['$set' => ['id' => 2]],
['upsert' => true],
],
],
]);
$engine = new ScoutEngine($database, softDelete: false);
$engine->update(EloquentCollection::make([
new SearchableModel([
'id' => 1,
'date' => $date,
]),
new SearchableModel(['id' => 2]),
]));
}
public function testUpdateWithSoftDelete(): void
{
$date = new DateTimeImmutable('2000-01-02 03:04:05');
$database = $this->createMock(Database::class);
$collection = $this->createMock(Collection::class);
$database->expects($this->once())
->method('selectCollection')
->with('collection_indexable')
->willReturn($collection);
$collection->expects($this->once())
->method('bulkWrite')
->with([
[
'updateOne' => [
['_id' => 'key_1'],
['$set' => ['id' => 1, '__soft_deleted' => false]],
['upsert' => true],
],
],
]);
$model = new SearchableModel(['id' => 1]);
$model->delete();
$engine = new ScoutEngine($database, softDelete: true);
$engine->update(EloquentCollection::make([$model]));
}
public function testDelete(): void
{
$database = $this->createMock(Database::class);
$collection = $this->createMock(Collection::class);
$database->expects($this->once())
->method('selectCollection')
->with('collection_indexable')
->willReturn($collection);
$collection->expects($this->once())
->method('deleteMany')
->with(['_id' => ['$in' => ['key_1', 'key_2']]]);
$engine = new ScoutEngine($database, softDelete: false);
$engine->delete(EloquentCollection::make([
new SearchableModel(['id' => 1]),
new SearchableModel(['id' => 2]),
]));
}
public function testDeleteWithRemoveableScoutCollection(): void
{
$job = new RemoveFromSearch(EloquentCollection::make([
new SearchableModel(['id' => 5, 'scout_key' => 'key_5']),
]));
$job = unserialize(serialize($job));
$database = $this->createMock(Database::class);
$collection = $this->createMock(Collection::class);
$database->expects($this->once())
->method('selectCollection')
->with('collection_indexable')
->willReturn($collection);
$collection->expects($this->once())
->method('deleteMany')
->with(['_id' => ['$in' => ['key_5']]]);
$engine = new ScoutEngine($database, softDelete: false);
$engine->delete($job->models);
}
public function testDeleteRejectsNonEloquentCollection(): void
{
$database = $this->createMock(Database::class);
$engine = new ScoutEngine($database, softDelete: false);
$this->expectException(TypeError::class);
$this->expectExceptionMessage(
'Argument #1 ($models) must be of type Illuminate\Database\Eloquent\Collection',
);
$engine->delete(LaravelCollection::make([1, 2, 3]));
}
}
================================================
FILE: tests/Scout/ScoutIntegrationTest.php
================================================
set('scout.driver', 'mongodb');
$app['config']->set('scout.prefix', 'prefix_');
$app['config']->set('scout.mongodb.index-definitions', [
'prefix_scout_users' => ['mappings' => ['dynamic' => true, 'fields' => ['bool_field' => ['type' => 'boolean']]]],
]);
}
public function setUp(): void
{
parent::setUp();
$this->skipIfSearchIndexManagementIsNotSupported();
// Init the SQL database with some objects that will be indexed
// Test data copied from Laravel Scout tests
// https://github.com/laravel/scout/blob/10.x/tests/Integration/SearchableTests.php
ScoutUser::executeSchema();
$collect = LazyCollection::make(function () {
yield ['name' => 'Laravel Framework'];
foreach (range(2, 10) as $key) {
yield ['name' => 'Example ' . $key];
}
yield ['name' => 'Larry Casper', 'email_verified_at' => null];
yield ['name' => 'Reta Larkin'];
foreach (range(13, 19) as $key) {
yield ['name' => 'Example ' . $key];
}
yield ['name' => 'Prof. Larry Prosacco DVM', 'email_verified_at' => null];
foreach (range(21, 38) as $key) {
yield ['name' => 'Example ' . $key, 'email_verified_at' => null];
}
yield ['name' => 'Linkwood Larkin', 'email_verified_at' => null];
yield ['name' => 'Otis Larson MD'];
yield ['name' => 'Gudrun Larkin'];
yield ['name' => 'Dax Larkin'];
yield ['name' => 'Dana Larson Sr.'];
yield ['name' => 'Amos Larson Sr.'];
});
$id = 0;
$date = new DateTimeImmutable('2021-01-01 00:00:00');
foreach ($collect as $data) {
$data = array_merge(['id' => ++$id, 'email_verified_at' => $date], $data);
ScoutUser::create($data)->save();
}
self::assertSame(44, ScoutUser::count());
}
/** This test create the search index for tests performing search */
public function testItCanCreateTheCollection()
{
$this->skipIfSearchIndexManagementIsNotSupported();
$collection = DB::connection('mongodb')->getCollection('prefix_scout_users');
$collection->drop();
$this->waitForSearchIndexesDropped($collection);
// Recreate the indexes using the artisan commands
// Ensure they return a success exit code (0)
self::assertSame(0, artisan($this, 'scout:delete-index', ['name' => ScoutUser::class]));
self::assertSame(0, artisan($this, 'scout:import', ['model' => ScoutUser::class]));
self::assertSame(0, artisan($this, 'scout:index', ['name' => ScoutUser::class]));
self::assertSame(44, $collection->countDocuments());
$searchIndexes = $collection->listSearchIndexes(['name' => 'scout', 'typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array']]);
self::assertCount(1, $searchIndexes);
self::assertSame(['mappings' => ['dynamic' => true, 'fields' => ['bool_field' => ['type' => 'boolean']]]], iterator_to_array($searchIndexes)[0]['latestDefinition']);
$this->waitForSearchIndexesReady($collection);
// Wait for all documents to be indexed asynchronously
$timeout = hrtime()[0] + 30;
while (true) {
$indexedDocuments = $collection->aggregate([
['$search' => ['index' => 'scout', 'exists' => ['path' => 'name']]],
])->toArray();
if (count($indexedDocuments) >= 44) {
break;
}
if (hrtime()[0] > $timeout) {
self::fail('Timed out waiting for documents to be indexed');
}
usleep(1000);
}
self::assertCount(44, $indexedDocuments);
}
#[Depends('testItCanCreateTheCollection')]
public function testItCanUseBasicSearch()
{
// All the search queries use the "sort" option to ensure the results are deterministic
$results = ScoutUser::search('lar')->take(10)->orderBy('id')->get();
self::assertSame([
1 => 'Laravel Framework',
11 => 'Larry Casper',
12 => 'Reta Larkin',
20 => 'Prof. Larry Prosacco DVM',
39 => 'Linkwood Larkin',
40 => 'Otis Larson MD',
41 => 'Gudrun Larkin',
42 => 'Dax Larkin',
43 => 'Dana Larson Sr.',
44 => 'Amos Larson Sr.',
], $results->pluck('name', 'id')->all());
}
#[Depends('testItCanCreateTheCollection')]
public function testItCanUseBasicSearchCursor()
{
// All the search queries use "sort" option to ensure the results are deterministic
$results = ScoutUser::search('lar')->take(10)->orderBy('id')->cursor();
self::assertSame([
1 => 'Laravel Framework',
11 => 'Larry Casper',
12 => 'Reta Larkin',
20 => 'Prof. Larry Prosacco DVM',
39 => 'Linkwood Larkin',
40 => 'Otis Larson MD',
41 => 'Gudrun Larkin',
42 => 'Dax Larkin',
43 => 'Dana Larson Sr.',
44 => 'Amos Larson Sr.',
], $results->pluck('name', 'id')->all());
}
#[Depends('testItCanCreateTheCollection')]
public function testItCanUseBasicSearchWithQueryCallback()
{
$results = ScoutUser::search('lar')->take(10)->orderBy('id')->query(function ($query) {
return $query->whereNotNull('email_verified_at');
})->get();
self::assertSame([
1 => 'Laravel Framework',
12 => 'Reta Larkin',
40 => 'Otis Larson MD',
41 => 'Gudrun Larkin',
42 => 'Dax Larkin',
43 => 'Dana Larson Sr.',
44 => 'Amos Larson Sr.',
], $results->pluck('name', 'id')->all());
}
#[Depends('testItCanCreateTheCollection')]
public function testItCanUseBasicSearchToFetchKeys()
{
$results = ScoutUser::search('lar')->orderBy('id')->take(10)->keys();
self::assertSame([1, 11, 12, 20, 39, 40, 41, 42, 43, 44], $results->all());
}
#[Depends('testItCanCreateTheCollection')]
public function testItCanUseBasicSearchWithQueryCallbackToFetchKeys()
{
$results = ScoutUser::search('lar')->take(10)->orderBy('id', 'desc')->query(function ($query) {
return $query->whereNotNull('email_verified_at');
})->keys();
self::assertSame([44, 43, 42, 41, 40, 39, 20, 12, 11, 1], $results->all());
}
#[Depends('testItCanCreateTheCollection')]
public function testItCanUsePaginatedSearch()
{
$page1 = ScoutUser::search('lar')->take(10)->orderBy('id')->paginate(5, 'page', 1);
$page2 = ScoutUser::search('lar')->take(10)->orderBy('id')->paginate(5, 'page', 2);
self::assertSame([
1 => 'Laravel Framework',
11 => 'Larry Casper',
12 => 'Reta Larkin',
20 => 'Prof. Larry Prosacco DVM',
39 => 'Linkwood Larkin',
], $page1->pluck('name', 'id')->all());
self::assertSame([
40 => 'Otis Larson MD',
41 => 'Gudrun Larkin',
42 => 'Dax Larkin',
43 => 'Dana Larson Sr.',
44 => 'Amos Larson Sr.',
], $page2->pluck('name', 'id')->all());
}
#[Depends('testItCanCreateTheCollection')]
public function testItCanUsePaginatedSearchWithQueryCallback()
{
$queryCallback = function ($query) {
return $query->whereNotNull('email_verified_at');
};
$page1 = ScoutUser::search('lar')->take(10)->orderBy('id')->query($queryCallback)->paginate(5, 'page', 1);
$page2 = ScoutUser::search('lar')->take(10)->orderBy('id')->query($queryCallback)->paginate(5, 'page', 2);
self::assertSame([
1 => 'Laravel Framework',
12 => 'Reta Larkin',
], $page1->pluck('name', 'id')->all());
self::assertSame([
40 => 'Otis Larson MD',
41 => 'Gudrun Larkin',
42 => 'Dax Larkin',
43 => 'Dana Larson Sr.',
44 => 'Amos Larson Sr.',
], $page2->pluck('name', 'id')->all());
}
public function testItCannotIndexInTheSameNamespace()
{
self::expectException(LogicException::class);
self::expectExceptionMessage(sprintf(
'The MongoDB Scout collection "%s.searchable_in_same_namespaces" must use a different collection from the collection name of the model "%s". Set the "scout.prefix" configuration or use a distinct MongoDB database',
env('MONGODB_DATABASE', 'unittest'),
SearchableInSameNamespace::class,
),);
SearchableInSameNamespace::create(['name' => 'test']);
}
}
================================================
FILE: tests/Seeder/DatabaseSeeder.php
================================================
call(UserTableSeeder::class);
}
}
================================================
FILE: tests/Seeder/UserTableSeeder.php
================================================
delete();
DB::table('users')->insert(['name' => 'John Doe', 'seed' => true]);
}
}
================================================
FILE: tests/SeederTest.php
================================================
run();
$user = User::where('name', 'John Doe')->first();
$this->assertTrue($user->seed);
}
public function testArtisan(): void
{
Artisan::call('db:seed', ['class' => DatabaseSeeder::class]);
$user = User::where('name', 'John Doe')->first();
$this->assertTrue($user->seed);
}
}
================================================
FILE: tests/SessionTest.php
================================================
getCollection('sessions')->drop();
parent::tearDown();
}
/** @param class-string $class */
#[TestWith([DatabaseSessionHandler::class])]
#[TestWith([MongoDbSessionHandler::class])]
public function testSessionHandlerFunctionality(string $class)
{
$handler = new $class(
$this->app['db']->connection('mongodb'),
'sessions',
10,
);
$sessionId = '123';
$handler->write($sessionId, 'foo');
$this->assertEquals('foo', $handler->read($sessionId));
$handler->write($sessionId, 'bar');
$this->assertEquals('bar', $handler->read($sessionId));
$handler->destroy($sessionId);
$this->assertEmpty($handler->read($sessionId));
$handler->write($sessionId, 'bar');
$handler->gc(-1);
$this->assertEmpty($handler->read($sessionId));
}
public function testDatabaseSessionHandlerRegistration()
{
$this->app['config']->set('session.driver', 'database');
$this->app['config']->set('session.connection', 'mongodb');
$session = $this->app['session'];
$this->assertInstanceOf(SessionManager::class, $session);
$this->assertInstanceOf(DatabaseSessionHandler::class, $session->getHandler());
$this->assertSessionCanStoreInMongoDB($session);
}
public function testMongoDBSessionHandlerRegistration()
{
$this->app['config']->set('session.driver', 'mongodb');
$this->app['config']->set('session.connection', 'mongodb');
$session = $this->app['session'];
$this->assertInstanceOf(SessionManager::class, $session);
$this->assertInstanceOf(MongoDbSessionHandler::class, $session->getHandler());
$this->assertSessionCanStoreInMongoDB($session);
}
private function assertSessionCanStoreInMongoDB(SessionManager $session): void
{
$session->put('foo', 'bar');
$session->save();
$this->assertNotNull($session->getId());
$data = DB::connection('mongodb')
->getCollection('sessions')
->findOne(['_id' => $session->getId()]);
self::assertIsObject($data);
self::assertSame($session->getId(), $data->_id);
$session->remove('foo');
$data = DB::connection('mongodb')
->getCollection('sessions')
->findOne(['_id' => $session->getId()]);
self::assertIsObject($data);
self::assertSame($session->getId(), $data->_id);
}
}
================================================
FILE: tests/TestCase.php
================================================
set('app.key', 'ZsZewWyUJ5FsKp9lMwv4tYbNlegQilM7');
$app['config']->set('database.default', 'mongodb');
$app['config']->set('database.connections.sqlite', $config['connections']['sqlite']);
$app['config']->set('database.connections.mongodb', $config['connections']['mongodb']);
$app['config']->set('database.connections.mongodb2', $config['connections']['mongodb']);
$app['config']->set('auth.model', User::class);
$app['config']->set('auth.providers.users.model', User::class);
$app['config']->set('cache.driver', 'array');
$app['config']->set('cache.stores.mongodb', [
'driver' => 'mongodb',
'connection' => 'mongodb',
'collection' => 'foo_cache',
]);
$app['config']->set('queue.default', 'database');
$app['config']->set('queue.connections.database', [
'driver' => 'mongodb',
'table' => 'jobs',
'queue' => 'default',
'expire' => 60,
]);
$app['config']->set('queue.failed.database', 'mongodb2');
$app['config']->set('queue.failed.driver', 'mongodb');
}
public function skipIfSearchIndexManagementIsNotSupported(): void
{
try {
$this->getConnection('mongodb')->getCollection('test')->listSearchIndexes(['name' => 'just_for_testing']);
} catch (ServerException $e) {
if (Builder::isAtlasSearchNotSupportedException($e)) {
self::markTestSkipped('Search index management is not supported on this server');
}
throw $e;
}
}
}
================================================
FILE: tests/Ticket/GH2489Test.php
================================================
'Location 1',
'images' => [
['_id' => 1, 'uri' => 'image1.jpg'],
['_id' => 2, 'uri' => 'image2.jpg'],
],
],
[
'name' => 'Location 2',
'images' => [
['_id' => 3, 'uri' => 'image3.jpg'],
['_id' => 4, 'uri' => 'image4.jpg'],
],
],
]);
// With _id
$results = Location::whereIn('images._id', [1])->get();
$this->assertCount(1, $results);
$this->assertSame('Location 1', $results->first()->name);
// With id
$results = Location::whereIn('images.id', [1])->get();
$this->assertCount(1, $results);
$this->assertSame('Location 1', $results->first()->name);
}
}
================================================
FILE: tests/Ticket/GH2783Test.php
================================================
'Lorem ipsum']);
$user = GH2783User::create(['username' => 'jsmith']);
$imageWithPost = GH2783Image::create(['uri' => 'http://example.com/post.png']);
$imageWithPost->imageable()->associate($post)->save();
$imageWithUser = GH2783Image::create(['uri' => 'http://example.com/user.png']);
$imageWithUser->imageable()->associate($user)->save();
$queriedImageWithPost = GH2783Image::with('imageable')->find($imageWithPost->getKey());
$this->assertInstanceOf(GH2783Post::class, $queriedImageWithPost->imageable);
$this->assertEquals($post->id, $queriedImageWithPost->imageable->getKey());
$queriedImageWithUser = GH2783Image::with('imageable')->find($imageWithUser->getKey());
$this->assertInstanceOf(GH2783User::class, $queriedImageWithUser->imageable);
$this->assertEquals($user->username, $queriedImageWithUser->imageable->getKey());
}
}
class GH2783Image extends Model
{
protected $connection = 'mongodb';
protected $fillable = ['uri'];
public function imageable(): MorphTo
{
return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}
}
class GH2783Post extends Model
{
protected $connection = 'mongodb';
protected $fillable = ['text'];
public function image(): MorphOne
{
return $this->morphOne(GH2783Image::class, 'imageable');
}
}
class GH2783User extends Model
{
protected $connection = 'mongodb';
protected $fillable = ['username'];
protected $primaryKey = 'username';
public function image(): MorphOne
{
return $this->morphOne(GH2783Image::class, 'imageable');
}
}
================================================
FILE: tests/Ticket/GH3326Test.php
================================================
foo = 'bar';
$model->save();
$fresh = $model->fresh();
$this->assertEquals('bar', $fresh->foo);
$this->assertEquals('written-in-created', $fresh->extra);
}
}
class GH3326Model extends Model
{
protected $connection = 'mongodb';
protected $collection = 'test_gh3326';
protected $guarded = [];
protected static function booted(): void
{
static::created(function ($model) {
$model->extra = 'written-in-created';
$model->saveQuietly();
});
}
}
================================================
FILE: tests/Ticket/GH3328Test.php
================================================
beforeStartingTransactionIsSupported()) {
Event::assertDispatchedTimes(BeforeTransactionEvent::class);
}
Event::assertDispatchedTimes(RegularEvent::class);
Event::assertDispatchedTimes(AfterCommitEvent::class);
Event::assertDispatched(TransactionBeginning::class);
Event::assertDispatched(TransactionCommitting::class);
Event::assertDispatched(TransactionCommitted::class);
};
$this->assertTransactionCallbackResult($callback, $assert);
}
public function testAfterCommitOnFailedTransaction(): void
{
$callback = static function (): void {
event(new RegularEvent());
event(new AfterCommitEvent());
// Transaction failed; after commit event should not be dispatched
throw new Fake();
};
$assert = function (): void {
if ($this->beforeStartingTransactionIsSupported()) {
Event::assertDispatchedTimes(BeforeTransactionEvent::class, 3);
}
Event::assertDispatchedTimes(RegularEvent::class, 3);
Event::assertDispatchedTimes(TransactionBeginning::class, 3);
Event::assertDispatched(TransactionRolledBack::class);
Event::assertNotDispatched(TransactionCommitting::class);
Event::assertNotDispatched(TransactionCommitted::class);
};
$this->assertCallbackResultForConnection(
DB::connection('mongodb'),
$callback,
$assert,
3,
);
if (! interface_exists(ConcurrencyErrorDetector::class)) {
// Earlier versions of Laravel use a trait instead of DI to detect concurrency errors
// That would increase the scope of this comparison dramatically and is probably not worth it.
return;
}
$this->app->bind(ConcurrencyErrorDetector::class, FakeConcurrencyErrorDetector::class);
$this->assertCallbackResultForConnection(
DB::connection('sqlite'),
$callback,
$assert,
3,
);
}
public function testAfterCommitOnSuccessfulManualTransaction(): void
{
$callback = function (): void {
event(new RegularEvent());
event(new AfterCommitEvent());
};
$assert = function (): void {
if ($this->beforeStartingTransactionIsSupported()) {
Event::assertDispatchedTimes(BeforeTransactionEvent::class);
}
Event::assertDispatchedTimes(RegularEvent::class);
Event::assertDispatchedTimes(AfterCommitEvent::class);
Event::assertDispatched(TransactionBeginning::class);
Event::assertNotDispatched(TransactionRolledBack::class);
Event::assertDispatched(TransactionCommitting::class);
Event::assertDispatched(TransactionCommitted::class);
};
$this->assertTransactionResult($callback, $assert);
}
public function testAfterCommitOnFailedManualTransaction(): void
{
$callback = function (): void {
event(new RegularEvent());
event(new AfterCommitEvent());
throw new Fake();
};
$assert = function (): void {
if ($this->beforeStartingTransactionIsSupported()) {
Event::assertDispatchedTimes(BeforeTransactionEvent::class);
}
Event::assertDispatchedTimes(RegularEvent::class);
Event::assertNotDispatched(AfterCommitEvent::class);
Event::assertDispatched(TransactionBeginning::class);
Event::assertDispatched(TransactionRolledBack::class);
Event::assertNotDispatched(TransactionCommitting::class);
Event::assertNotDispatched(TransactionCommitted::class);
};
$this->assertTransactionResult($callback, $assert);
}
private function assertTransactionCallbackResult(Closure $callback, Closure $assert, ?int $attempts = 1): void
{
$this->assertCallbackResultForConnection(
DB::connection('sqlite'),
$callback,
$assert,
$attempts,
);
$this->assertCallbackResultForConnection(
DB::connection('mongodb'),
$callback,
$assert,
$attempts,
);
}
/**
* Ensure equal transaction behavior between SQLite (handled by Laravel) and MongoDB
*/
private function assertCallbackResultForConnection(Connection $connection, Closure $callback, Closure $assertions, int $attempts): void
{
$fake = Event::fake();
$connection->setEventDispatcher($fake);
if ($this->beforeStartingTransactionIsSupported()) {
$connection->beforeStartingTransaction(function () {
event(new BeforeTransactionEvent());
});
}
try {
$connection->transaction($callback, $attempts);
} catch (Exception) {
}
$assertions();
}
private function assertTransactionResult(Closure $callback, Closure $assert): void
{
$this->assertManualResultForConnection(
DB::connection('sqlite'),
$callback,
$assert,
);
$this->assertManualResultForConnection(
DB::connection('mongodb'),
$callback,
$assert,
);
}
/**
* Ensure equal transaction behavior between SQLite (handled by Laravel) and MongoDB
*/
private function assertManualResultForConnection(Connection $connection, Closure $callback, Closure $assert): void
{
$fake = Event::fake();
$connection->setEventDispatcher($fake);
if ($this->beforeStartingTransactionIsSupported()) {
$connection->beforeStartingTransaction(function () {
event(new BeforeTransactionEvent());
});
}
$connection->beginTransaction();
try {
$callback();
$connection->commit();
} catch (Exception) {
$connection->rollBack();
}
$assert();
}
private function beforeStartingTransactionIsSupported(): bool
{
return property_exists(ManagesTransactions::class, 'beforeStartingTransaction');
}
}
class AfterCommitEvent implements ShouldDispatchAfterCommit
{
use Dispatchable;
}
class BeforeTransactionEvent
{
use Dispatchable;
}
class RegularEvent
{
use Dispatchable;
}
class Fake extends RuntimeException
{
public function __construct()
{
$this->errorLabels = ['TransientTransactionError'];
}
}
if (interface_exists(ConcurrencyErrorDetector::class)) {
class FakeConcurrencyErrorDetector implements ConcurrencyErrorDetector
{
public function causedByConcurrencyError(Throwable $e): bool
{
return true;
}
}
}
================================================
FILE: tests/Ticket/GH3335Test.php
================================================
id = 'foo';
$model->save();
$model = Location::find('foo');
$model->{'38'} = 'PHP';
$model->save();
$model = Location::find('foo');
self::assertSame('PHP', $model->{'38'});
}
}
================================================
FILE: tests/TransactionTest.php
================================================
getPrimaryServerType() === Server::TYPE_STANDALONE) {
$this->markTestSkipped('Transactions are not supported on standalone servers');
}
User::truncate();
}
public function tearDown(): void
{
User::truncate();
parent::tearDown();
}
public function testCreateWithCommit(): void
{
DB::beginTransaction();
$klinson = User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
$this->assertInstanceOf(User::class, $klinson);
DB::commit();
$this->assertTrue(Model::isDocumentModel($klinson));
$this->assertTrue($klinson->exists);
$this->assertEquals('klinson', $klinson->name);
$check = User::find($klinson->id);
$this->assertInstanceOf(User::class, $check);
$this->assertEquals($klinson->name, $check->name);
}
public function testCreateRollBack(): void
{
DB::beginTransaction();
$klinson = User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
$this->assertInstanceOf(User::class, $klinson);
DB::rollBack();
$this->assertTrue(Model::isDocumentModel($klinson));
$this->assertTrue($klinson->exists);
$this->assertEquals('klinson', $klinson->name);
$this->assertFalse(User::where('id', $klinson->id)->exists());
}
public function testInsertWithCommit(): void
{
DB::beginTransaction();
DB::table('users')->insert(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
DB::commit();
$this->assertTrue(DB::table('users')->where('name', 'klinson')->exists());
}
public function testInsertWithRollBack(): void
{
DB::beginTransaction();
DB::table('users')->insert(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
DB::rollBack();
$this->assertFalse(DB::table('users')->where('name', 'klinson')->exists());
}
public function testEloquentCreateWithCommit(): void
{
DB::beginTransaction();
$klinson = User::getModel();
$this->assertInstanceOf(User::class, $klinson);
$klinson->name = 'klinson';
$klinson->save();
DB::commit();
$this->assertTrue($klinson->exists);
$this->assertNotNull($klinson->getIdAttribute());
$check = User::find($klinson->id);
$this->assertInstanceOf(User::class, $check);
$this->assertEquals($check->name, $klinson->name);
}
public function testEloquentCreateWithRollBack(): void
{
DB::beginTransaction();
$klinson = User::getModel();
$this->assertInstanceOf(User::class, $klinson);
$klinson->name = 'klinson';
$klinson->save();
DB::rollBack();
$this->assertTrue($klinson->exists);
$this->assertNotNull($klinson->getIdAttribute());
$this->assertFalse(User::where('id', $klinson->id)->exists());
}
public function testInsertGetIdWithCommit(): void
{
DB::beginTransaction();
$userId = DB::table('users')->insertGetId(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
DB::commit();
$this->assertInstanceOf(ObjectId::class, $userId);
$user = DB::table('users')->find((string) $userId);
$this->assertEquals('klinson', $user->name);
}
public function testInsertGetIdWithRollBack(): void
{
DB::beginTransaction();
$userId = DB::table('users')->insertGetId(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
DB::rollBack();
$this->assertInstanceOf(ObjectId::class, $userId);
$this->assertFalse(DB::table('users')->where('id', (string) $userId)->exists());
}
public function testUpdateWithCommit(): void
{
User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
DB::beginTransaction();
$updated = DB::table('users')->where('name', 'klinson')->update(['age' => 21]);
DB::commit();
$this->assertEquals(1, $updated);
$this->assertTrue(DB::table('users')->where('name', 'klinson')->where('age', 21)->exists());
}
public function testUpdateWithRollback(): void
{
User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
DB::beginTransaction();
$updated = DB::table('users')->where('name', 'klinson')->update(['age' => 21]);
DB::rollBack();
$this->assertEquals(1, $updated);
$this->assertFalse(DB::table('users')->where('name', 'klinson')->where('age', 21)->exists());
}
public function testEloquentUpdateWithCommit(): void
{
$klinson = User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
$this->assertInstanceOf(User::class, $klinson);
$alcaeus = User::create(['name' => 'alcaeus', 'age' => 38, 'title' => 'admin']);
$this->assertInstanceOf(User::class, $alcaeus);
DB::beginTransaction();
$klinson->age = 21;
$klinson->update();
$alcaeus->update(['age' => 39]);
DB::commit();
$this->assertEquals(21, $klinson->age);
$this->assertEquals(39, $alcaeus->age);
$this->assertTrue(User::where('id', $klinson->id)->where('age', 21)->exists());
$this->assertTrue(User::where('id', $alcaeus->id)->where('age', 39)->exists());
}
public function testEloquentUpdateWithRollBack(): void
{
$klinson = User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
$this->assertInstanceOf(User::class, $klinson);
$alcaeus = User::create(['name' => 'klinson', 'age' => 38, 'title' => 'admin']);
$this->assertInstanceOf(User::class, $alcaeus);
DB::beginTransaction();
$klinson->age = 21;
$klinson->update();
$alcaeus->update(['age' => 39]);
DB::rollBack();
$this->assertEquals(21, $klinson->age);
$this->assertEquals(39, $alcaeus->age);
$this->assertFalse(User::where('id', $klinson->id)->where('age', 21)->exists());
$this->assertFalse(User::where('id', $alcaeus->id)->where('age', 39)->exists());
}
public function testDeleteWithCommit(): void
{
User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
DB::beginTransaction();
$deleted = User::where(['name' => 'klinson'])->delete();
DB::commit();
$this->assertEquals(1, $deleted);
$this->assertFalse(User::where(['name' => 'klinson'])->exists());
}
public function testDeleteWithRollBack(): void
{
User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
DB::beginTransaction();
$deleted = User::where(['name' => 'klinson'])->delete();
DB::rollBack();
$this->assertEquals(1, $deleted);
$this->assertTrue(User::where(['name' => 'klinson'])->exists());
}
public function testEloquentDeleteWithCommit(): void
{
$klinson = User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
$this->assertInstanceOf(User::class, $klinson);
DB::beginTransaction();
$klinson->delete();
DB::commit();
$this->assertFalse(User::where('id', $klinson->id)->exists());
}
public function testEloquentDeleteWithRollBack(): void
{
$klinson = User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
$this->assertInstanceOf(User::class, $klinson);
DB::beginTransaction();
$klinson->delete();
DB::rollBack();
$this->assertTrue(User::where('id', $klinson->id)->exists());
}
public function testIncrementWithCommit(): void
{
User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
DB::beginTransaction();
DB::table('users')->where('name', 'klinson')->increment('age');
DB::commit();
$this->assertTrue(DB::table('users')->where('name', 'klinson')->where('age', 21)->exists());
}
public function testIncrementWithRollBack(): void
{
User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
DB::beginTransaction();
DB::table('users')->where('name', 'klinson')->increment('age');
DB::rollBack();
$this->assertTrue(DB::table('users')->where('name', 'klinson')->where('age', 20)->exists());
}
public function testDecrementWithCommit(): void
{
User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
DB::beginTransaction();
DB::table('users')->where('name', 'klinson')->decrement('age');
DB::commit();
$this->assertTrue(DB::table('users')->where('name', 'klinson')->where('age', 19)->exists());
}
public function testDecrementWithRollBack(): void
{
User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
DB::beginTransaction();
DB::table('users')->where('name', 'klinson')->decrement('age');
DB::rollBack();
$this->assertTrue(DB::table('users')->where('name', 'klinson')->where('age', 20)->exists());
}
public function testQuery()
{
/** rollback test */
DB::beginTransaction();
$count = DB::table('users')->count();
$this->assertEquals(0, $count);
DB::table('users')->insert(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
$count = DB::table('users')->count();
$this->assertEquals(1, $count);
DB::rollBack();
$count = DB::table('users')->count();
$this->assertEquals(0, $count);
/** commit test */
DB::beginTransaction();
$count = DB::table('users')->count();
$this->assertEquals(0, $count);
DB::table('users')->insert(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
$count = DB::table('users')->count();
$this->assertEquals(1, $count);
DB::commit();
$count = DB::table('users')->count();
$this->assertEquals(1, $count);
}
public function testTransaction(): void
{
User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
// The $connection parameter may be unused, but is implicitly used to
// test that the closure is executed with the connection as an argument.
DB::transaction(function (Connection $connection): void {
User::create(['name' => 'alcaeus', 'age' => 38, 'title' => 'admin']);
User::where(['name' => 'klinson'])->update(['age' => 21]);
});
$count = User::count();
$this->assertEquals(2, $count);
$this->assertTrue(User::where('name', 'alcaeus')->exists());
$this->assertTrue(User::where(['name' => 'klinson'])->where('age', 21)->exists());
}
public function testTransactionRepeatsOnTransientFailure(): void
{
User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
$timesRun = 0;
DB::transaction(function () use (&$timesRun): void {
$timesRun++;
// Run a query to start the transaction on the server
User::create(['name' => 'alcaeus', 'age' => 38, 'title' => 'admin']);
// Update user outside of the session
if ($timesRun === 1) {
DB::getCollection('users')->updateOne(['name' => 'klinson'], ['$set' => ['age' => 22]]);
}
// This update will create a write conflict, aborting the transaction
User::where(['name' => 'klinson'])->update(['age' => 21]);
}, 2);
$this->assertSame(2, $timesRun);
$this->assertTrue(User::where(['name' => 'klinson'])->where('age', 21)->exists());
}
public function testTransactionRespectsRepetitionLimit(): void
{
$klinson = User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
$timesRun = 0;
try {
DB::transaction(function () use (&$timesRun): void {
$timesRun++;
// Run a query to start the transaction on the server
User::create(['name' => 'alcaeus', 'age' => 38, 'title' => 'admin']);
// Update user outside of the session
DB::getCollection('users')->updateOne(['name' => 'klinson'], ['$inc' => ['age' => 2]]);
// This update will create a write conflict, aborting the transaction
User::where(['name' => 'klinson'])->update(['age' => 21]);
}, 2);
$this->fail('Expected exception during transaction');
} catch (BulkWriteException $e) {
$this->assertInstanceOf(BulkWriteException::class, $e);
}
$this->assertSame(2, $timesRun);
$check = User::find($klinson->id);
$this->assertInstanceOf(User::class, $check);
// Age is expected to be 24: the callback is executed twice, incrementing age by 2 every time
$this->assertSame(24, $check->age);
}
public function testTransactionReturnsCallbackResult(): void
{
$result = DB::transaction(function (): User {
return User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
});
$this->assertInstanceOf(User::class, $result);
$this->assertEquals($result->title, 'admin');
$this->assertSame(1, User::count());
}
public function testNestedTransactionsCauseException(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Transaction already in progress');
DB::beginTransaction();
DB::beginTransaction();
DB::commit();
DB::rollBack();
}
public function testNestingTransactionInManualTransaction()
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Transaction already in progress');
DB::beginTransaction();
DB::transaction(function (): void {
});
DB::rollBack();
}
public function testCommitWithoutSession(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('There is no active session.');
DB::commit();
}
public function testRollBackWithoutSession(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('There is no active session.');
DB::rollback();
}
private function getPrimaryServerType(): int
{
return DB::getMongoClient()->getManager()->selectServer()->getType();
}
}
================================================
FILE: tests/ValidationTest.php
================================================
'John Doe'],
['name' => 'required|unique:users'],
);
$this->assertFalse($validator->fails());
User::create(['name' => 'John Doe']);
$validator = Validator::make(
['name' => 'John Doe'],
['name' => 'required|unique:users'],
);
$this->assertTrue($validator->fails());
$validator = Validator::make(
['name' => 'John doe'],
['name' => 'required|unique:users'],
);
$this->assertTrue($validator->fails());
$validator = Validator::make(
['name' => 'john doe'],
['name' => 'required|unique:users'],
);
$this->assertTrue($validator->fails());
$validator = Validator::make(
['name' => 'test doe'],
['name' => 'required|unique:users'],
);
$this->assertFalse($validator->fails());
$validator = Validator::make(
['name' => 'John'], // Part of an existing value
['name' => 'required|unique:users'],
);
$this->assertFalse($validator->fails());
User::create(['name' => 'Johnny Cash', 'email' => 'johnny.cash+200@gmail.com']);
$validator = Validator::make(
['email' => 'johnny.cash+200@gmail.com'],
['email' => 'required|unique:users'],
);
$this->assertTrue($validator->fails());
$validator = Validator::make(
['email' => 'johnny.cash+20@gmail.com'],
['email' => 'required|unique:users'],
);
$this->assertFalse($validator->fails());
$validator = Validator::make(
['email' => 'johnny.cash+1@gmail.com'],
['email' => 'required|unique:users'],
);
$this->assertFalse($validator->fails());
}
public function testExists(): void
{
$validator = Validator::make(
['name' => 'John Doe'],
['name' => 'required|exists:users'],
);
$this->assertTrue($validator->fails());
User::create(['name' => 'John Doe']);
User::create(['name' => 'Test Name']);
$validator = Validator::make(
['name' => 'John Doe'],
['name' => 'required|exists:users'],
);
$this->assertFalse($validator->fails());
$validator = Validator::make(
['name' => 'john Doe'],
['name' => 'required|exists:users'],
);
$this->assertFalse($validator->fails());
$validator = Validator::make(
['name' => ['test name', 'john doe']],
['name' => 'required|exists:users'],
);
$this->assertFalse($validator->fails());
$validator = Validator::make(
['name' => ['test name', 'john']], // Part of an existing value
['name' => 'required|exists:users'],
);
$this->assertTrue($validator->fails());
$validator = Validator::make(
['name' => '(invalid regex{'],
['name' => 'required|exists:users'],
);
$this->assertTrue($validator->fails());
$validator = Validator::make(
['name' => ['foo', '(invalid regex{']],
['name' => 'required|exists:users'],
);
$this->assertTrue($validator->fails());
User::create(['name' => '']);
$validator = Validator::make(
['name' => []],
['name' => 'exists:users'],
);
$this->assertFalse($validator->fails());
}
}
================================================
FILE: tests/config/database.php
================================================
[
'mongodb' => [
'name' => 'mongodb',
'driver' => 'mongodb',
'dsn' => env('MONGODB_URI', 'mongodb://127.0.0.1/'),
'database' => env('MONGODB_DATABASE', 'unittest'),
'options' => [
'connectTimeoutMS' => 1000,
'serverSelectionTimeoutMS' => 6000,
],
],
'sqlite' => [
'driver' => 'sqlite',
'database' => env('SQLITE_DATABASE', ':memory:'),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
],
],
];
================================================
FILE: tests/config/queue.php
================================================
env('QUEUE_CONNECTION'),
'connections' => [
'database' => [
'driver' => 'mongodb',
'table' => 'jobs',
'queue' => 'default',
'expire' => 60,
],
],
'failed' => [
'database' => env('MONGODB_DATABASE'),
'driver' => 'mongodb',
'table' => 'failed_jobs',
],
];