From 4aa09930345e901e4a10f9bd722612d668c742aa Mon Sep 17 00:00:00 2001
From: korridor <26689068+korridor@users.noreply.github.com>
Date: Mon, 15 Feb 2021 11:36:23 +0100
Subject: [PATCH] Init commit
---
.gitattributes | 14 +++
.gitignore | 18 +++
.php_cs | 14 +++
.styleci.yml | 1 +
.travis.yml | 44 +++++++
composer.json | 42 +++++++
docker/Dockerfile | 24 ++++
docker/docker-compose.yml | 9 ++
license.md | 25 ++++
phpcs.xml | 9 ++
phpunit.xml | 38 ++++++
readme.md | 97 +++++++++++++++
src/HasManyMerged.php | 225 ++++++++++++++++++++++++++++++++++
src/HasManyMergedRelation.php | 32 +++++
tests/HasManyMergedTest.php | 184 +++++++++++++++++++++++++++
tests/Models/Message.php | 40 ++++++
tests/Models/User.php | 55 +++++++++
tests/TestCase.php | 43 +++++++
18 files changed, 914 insertions(+)
create mode 100644 .gitattributes
create mode 100644 .gitignore
create mode 100644 .php_cs
create mode 100644 .styleci.yml
create mode 100644 .travis.yml
create mode 100644 composer.json
create mode 100644 docker/Dockerfile
create mode 100644 docker/docker-compose.yml
create mode 100644 license.md
create mode 100644 phpcs.xml
create mode 100644 phpunit.xml
create mode 100644 readme.md
create mode 100644 src/HasManyMerged.php
create mode 100644 src/HasManyMergedRelation.php
create mode 100644 tests/HasManyMergedTest.php
create mode 100644 tests/Models/Message.php
create mode 100644 tests/Models/User.php
create mode 100644 tests/TestCase.php
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..07c80d1
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,14 @@
+/tests export-ignore
+/phpunit.xml export-ignore
+/phpunit.xml.old export-ignore
+/phpcs.xml export-ignore
+/phpcs.xml export-ignore
+/docker export-ignore
+/.travis.yml export-ignore
+/.styleci.yml export-ignore
+/.php_cs export-ignore
+/.editorconfig export-ignore
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/vendor export-ignore
+/coverage export-ignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8192972
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+# IDE generated files #
+######################
+.idea
+
+# OS generated files #
+######################
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+vendor
+coverage
+composer.lock
+.php_cs.cache
+.phpunit.result.cache
diff --git a/.php_cs b/.php_cs
new file mode 100644
index 0000000..398a1a5
--- /dev/null
+++ b/.php_cs
@@ -0,0 +1,14 @@
+setRiskyAllowed(false)
+ ->setRules([
+ '@PSR2' => true,
+ ])
+ ->setUsingCache(true)
+ ->setFinder(
+ PhpCsFixer\Finder::create()
+ ->in(__DIR__.'/src')
+ ->in(__DIR__.'/tests')
+ )
+;
diff --git a/.styleci.yml b/.styleci.yml
new file mode 100644
index 0000000..0285f17
--- /dev/null
+++ b/.styleci.yml
@@ -0,0 +1 @@
+preset: laravel
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..0798e99
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,44 @@
+cache:
+ directories:
+ - $HOME/.composer/cache
+
+language: php
+
+matrix:
+ include:
+ # Laravel 6.*
+ - php: 7.2
+ env: LARAVEL='6.*' COMPOSER_FLAGS='--prefer-stable'
+ - php: 7.3
+ env: LARAVEL='6.*' COMPOSER_FLAGS='--prefer-stable'
+ # Laravel 7.*
+ - php: 7.2
+ env: LARAVEL='7.*' COMPOSER_FLAGS='--prefer-stable'
+ - php: 7.3
+ env: LARAVEL='7.*' COMPOSER_FLAGS='--prefer-stable'
+ - php: 7.4
+ env: LARAVEL='7.*' COMPOSER_FLAGS='--prefer-stable'
+ # Laravel 8.*
+ - php: 7.3
+ env: LARAVEL='8.*' COMPOSER_FLAGS='--prefer-stable'
+ - php: 7.4
+ env: LARAVEL='8.*' COMPOSER_FLAGS='--prefer-stable'
+ - php: 8.0
+ env: LARAVEL='8.*' COMPOSER_FLAGS='--prefer-stable'
+ fast_finish: true
+
+before_install:
+ - travis_retry composer self-update
+ - travis_retry composer require "illuminate/database:${LARAVEL}" --no-interaction --no-update
+
+install:
+ - travis_retry composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction --no-suggest
+
+before_script:
+ - composer config discard-changes true
+
+script:
+ - XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml
+
+after_success:
+ - bash <(curl -s https://codecov.io/bash)
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..a920db3
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,42 @@
+{
+ "name": "korridor/laravel-has-many-merged",
+ "description": "Custom relationship for Eloquent that merges/combines multiple one-to-may (hasMany) relationships",
+ "keywords": ["laravel", "eloquent", "relations", "has-many"],
+ "homepage": "https://github.com/korridor/laravel-has-many-merged",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "korridor",
+ "email": "26689068+korridor@users.noreply.github.com"
+ }
+ ],
+ "minimum-stability": "stable",
+ "require": {
+ "php": "^7.1|^8",
+ "illuminate/database": "^6|^7|^8"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.0|^8.0|^9.0",
+ "friendsofphp/php-cs-fixer": "^2.16",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "autoload": {
+ "psr-4": {
+ "Korridor\\LaravelHasManyMerged\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Korridor\\LaravelHasManyMerged\\Tests\\": "tests/"
+ }
+ },
+ "scripts": {
+ "test": "vendor/bin/phpunit",
+ "test-coverage": "vendor/bin/phpunit --coverage-html coverage",
+ "fix": "./vendor/bin/php-cs-fixer fix",
+ "lint": "./vendor/bin/phpcs --error-severity=1 --warning-severity=8 --extensions=php"
+ },
+ "config": {
+ "sort-packages": true
+ }
+}
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 0000000..7dc8b96
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,24 @@
+FROM php:7.4-cli
+
+RUN apt-get update && apt-get install -y \
+ zlib1g-dev \
+ libzip-dev
+
+RUN docker-php-ext-install zip
+
+RUN pecl install xdebug-2.8.1 \
+ && docker-php-ext-enable xdebug
+
+# Install composer and add its bin to the PATH.
+RUN curl -s http://getcomposer.org/installer | php && \
+ echo "export PATH=${PATH}:/var/www/vendor/bin" >> ~/.bashrc && \
+ mv composer.phar /usr/local/bin/composer
+
+# Add bash aliases
+RUN echo "alias ll='ls --color=auto -al'" >> ~/.bashrc
+
+
+# Source the bash
+RUN . ~/.bashrc
+
+WORKDIR /usr/src/app
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 0000000..dd54d6e
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,9 @@
+version: '3.7'
+services:
+ workspace:
+ build:
+ context: .
+ volumes:
+ - ..:/usr/src/app
+ tty: true
+ stdin_open: true
diff --git a/license.md b/license.md
new file mode 100644
index 0000000..8f1350d
--- /dev/null
+++ b/license.md
@@ -0,0 +1,25 @@
+The MIT License (MIT)
+=====================
+
+Copyright © `2021` `korridor`
+
+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.
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..4f5006d
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,9 @@
+
+
+ The ASH2 coding standard.
+
+
+
+ src/
+ tests/
+
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..c67e28c
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,38 @@
+
+
+
+
+ src/
+
+
+
+
+ tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..493b70a
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,97 @@
+# Laravel has many merged
+
+[![Latest Version on Packagist](https://img.shields.io/packagist/v/korridor/laravel-has-many-merged?style=flat-square)](https://packagist.org/packages/korridor/laravel-has-many-merged)
+[![License](https://img.shields.io/packagist/l/korridor/laravel-has-many-merged?style=flat-square)](license.md)
+[![Codecov](https://img.shields.io/codecov/c/github/korridor/laravel-has-many-merged?style=flat-square)](https://codecov.io/gh/korridor/laravel-has-many-merged)
+[![TravisCI](https://img.shields.io/travis/korridor/laravel-has-many-merged?style=flat-square)](https://travis-ci.org/korridor/laravel-has-many-merged)
+[![StyleCI](https://styleci.io/repos/339041939/shield)](https://styleci.io/repos/339041939)
+
+Custom relationship for Eloquent that merges/combines multiple one-to-may (hasMany) relationships.
+This relation fully supports lazy and eager loading.
+
+## Installation
+
+You can install the package via composer with following command:
+
+```bash
+composer require korridor/laravel-has-many-merged
+```
+
+### Requirements
+
+This package is tested for the following Laravel versions:
+
+- 8.* (PHP 7.3, 7.4, 8.0)
+- 7.* (PHP 7.2, 7.3, 7.4)
+- 6.* (PHP 7.2, 7.3)
+
+## Usage examples
+
+In the following example there are two models User and Message.
+Each message has a sender and a receiver.
+The User model has two hasMany relations - one for the sent messages and the other for the received ones.
+
+With this plugin you can add a relation that contains sent and received messages of a user.
+
+```php
+use Korridor\LaravelHasManyMerged\HasManyMerged;
+use Korridor\LaravelHasManyMerged\HasManyMergedRelation;
+
+class User extends Model
+{
+ use HasManyMergedRelation;
+
+ // ...
+
+ /**
+ * @return HasManyMerged|Message
+ */
+ public function messages()
+ {
+ return $this->hasManyMerged(Message::class, ['sender_user_id', 'receiver_user_id']);
+ }
+
+ /**
+ * @return HasMany|Message
+ */
+ public function sentMessages()
+ {
+ return $this->hasMany(Message::class, 'sender_user_id');
+ }
+
+ /**
+ * @return HasMany|Message
+ */
+ public function receivedMessages()
+ {
+ return $this->hasMany(Message::class, 'receiver_user_id');
+ }
+}
+```
+
+## Contributing
+
+I am open for suggestions and contributions. Just create an issue or a pull request.
+
+### Local docker environment
+
+The `docker` folder contains a local docker environment for development.
+The docker workspace has composer and xdebug installed.
+
+```bash
+docker-compose run workspace bash
+```
+
+### Testing
+
+The `composer test` command runs all tests with [phpunit](https://phpunit.de/).
+The `composer test-coverage` command runs all tests with phpunit and creates a coverage report into the `coverage` folder.
+
+### Codeformatting/Linting
+
+The `composer fix` command formats the code with [php-cs-fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer).
+The `composer lint` command checks the code with [phpcs](https://github.com/squizlabs/PHP_CodeSniffer).
+
+## License
+
+This package is licensed under the MIT License (MIT). Please see [license file](license.md) for more information.
diff --git a/src/HasManyMerged.php b/src/HasManyMerged.php
new file mode 100644
index 0000000..0f1f801
--- /dev/null
+++ b/src/HasManyMerged.php
@@ -0,0 +1,225 @@
+foreignKeys = $foreignKeys;
+ $this->localKey = $localKey;
+
+ parent::__construct($query, $parent);
+ }
+
+ /**
+ * Set the base constraints on the relation query.
+ * Note: Used to load relations of one model.
+ *
+ * @return void
+ */
+ public function addConstraints()
+ {
+ if (static::$constraints) {
+ $foreignKeys = $this->foreignKeys;
+
+ $this->query->where(function ($query) use ($foreignKeys) {
+ foreach ($foreignKeys as $foreignKey) {
+ $query->orWhere(function ($query) use ($foreignKey) {
+ $query->where($foreignKey, '=', $this->getParentKey())
+ ->whereNotNull($foreignKey);
+ });
+ }
+ });
+ }
+ }
+
+ /**
+ * Get the key value of the parent's local key.
+ * Info: From HasOneOrMany class.
+ *
+ * @return mixed
+ */
+ public function getParentKey()
+ {
+ return $this->parent->getAttribute($this->localKey);
+ }
+
+ /**
+ * Set the constraints for an eager load of the relation.
+ * Note: Used to load relations of multiple models at once.
+ *
+ * @param array $models
+ */
+ public function addEagerConstraints(array $models)
+ {
+ $foreignKeys = $this->foreignKeys;
+ $orWhereIn = $this->orWhereInMethod($this->parent, $this->localKey);
+
+ $this->query->where(function ($query) use ($foreignKeys, $models, $orWhereIn) {
+ foreach ($foreignKeys as $foreignKey) {
+ $query->{$orWhereIn}($foreignKey, $this->getKeys($models, $this->localKey));
+ }
+ });
+ }
+
+ /**
+ * Get the name of the "where in" method for eager loading.
+ * Note: Similar to whereInMethod of Relation class.
+ *
+ * @param Model $model
+ * @param string $key
+ * @return string
+ */
+ protected function orWhereInMethod(Model $model, string $key): string
+ {
+ return $model->getKeyName() === last(explode('.', $key))
+ && in_array($model->getKeyType(), ['int', 'integer'])
+ ? 'orWhereIntegerInRaw'
+ : 'orWhereIn';
+ }
+
+ /**
+ * Initialize the relation on a set of models.
+ *
+ * @param array $models
+ * @param string $relation
+ * @return array
+ */
+ public function initRelation(array $models, $relation)
+ {
+ // Info: From HasMany class
+ foreach ($models as $model) {
+ $model->setRelation($relation, $this->related->newCollection());
+ }
+
+ return $models;
+ }
+
+ /**
+ * Match the eagerly loaded results to their parents.
+ * Info: From HasMany class.
+ *
+ * @param array $models
+ * @param Collection $results
+ * @param string $relation
+ * @return array
+ */
+ public function match(array $models, Collection $results, $relation)
+ {
+ $dictionary = $this->buildDictionary($results);
+
+ // Once we have the dictionary we can simply spin through the parent models to
+ // link them up with their children using the keyed dictionary to make the
+ // matching very convenient and easy work. Then we'll just return them.
+ foreach ($models as $model) {
+ if (isset($dictionary[$key = $model->getAttribute($this->localKey)])) {
+ $model->setRelation(
+ $relation,
+ $this->related->newCollection($dictionary[$key])->unique($this->related->getKeyName())
+ );
+ }
+ }
+
+ return $models;
+ }
+
+ /**
+ * Build model dictionary keyed by the relation's foreign key.
+ * Note: Custom code.
+ *
+ * @param Collection $results
+ * @return array
+ */
+ protected function buildDictionary(Collection $results): array
+ {
+ $dictionary = [];
+ $foreignKeyNames = $this->getForeignKeyNames();
+
+ foreach ($results as $result) {
+ foreach ($foreignKeyNames as $foreignKeyName) {
+ $foreignKeyValue = $result->{$foreignKeyName};
+ if (! isset($dictionary[$foreignKeyValue])) {
+ $dictionary[$foreignKeyValue] = [];
+ }
+
+ $dictionary[$foreignKeyValue][] = $result;
+ }
+ }
+
+ return $dictionary;
+ }
+
+ /**
+ * Get the plain foreign key.
+ *
+ * @return string[]
+ */
+ public function getForeignKeyNames(): array
+ {
+ return array_map(function (string $qualifiedForeignKeyName) {
+ $segments = explode('.', $qualifiedForeignKeyName);
+
+ return end($segments);
+ }, $this->getQualifiedForeignKeyNames());
+ }
+
+ /**
+ * Get the foreign key for the relationship.
+ *
+ * @return string[]
+ */
+ public function getQualifiedForeignKeyNames(): array
+ {
+ return $this->foreignKeys;
+ }
+
+ /**
+ * Get the results of the relationship.
+ *
+ * @return mixed
+ */
+ public function getResults()
+ {
+ return $this->get();
+ }
+
+ /**
+ * Execute the query as a "select" statement.
+ *
+ * @param array $columns
+ * @return \Illuminate\Database\Eloquent\Collection
+ */
+ public function get($columns = ['*'])
+ {
+ return parent::get($columns);
+ }
+}
diff --git a/src/HasManyMergedRelation.php b/src/HasManyMergedRelation.php
new file mode 100644
index 0000000..a02b220
--- /dev/null
+++ b/src/HasManyMergedRelation.php
@@ -0,0 +1,32 @@
+getKeyName();
+
+ $foreignKeys = array_map(function ($foreignKey) use ($instance) {
+ return $instance->getTable().'.'.$foreignKey;
+ }, $foreignKeys);
+
+ return new HasManyMerged($instance->newQuery(), $this, $foreignKeys, $localKey);
+ }
+
+ /**
+ * Get the primary key for the model.
+ *
+ * @return string
+ */
+ abstract public function getKeyName();
+}
diff --git a/tests/HasManyMergedTest.php b/tests/HasManyMergedTest.php
new file mode 100644
index 0000000..8a2cb23
--- /dev/null
+++ b/tests/HasManyMergedTest.php
@@ -0,0 +1,184 @@
+ 11,
+ 'other_unique_id' => 1,
+ 'name' => 'Tester 1',
+ ]);
+ User::create([
+ 'id' => 12,
+ 'other_unique_id' => 2,
+ 'name' => 'Tester 2',
+ ]);
+
+ Message::create([
+ 'id' => 1,
+ 'content' => 'A - This is a message!',
+ 'sender_user_id' => 1,
+ 'receiver_user_id' => 1,
+ ]);
+ Message::create([
+ 'id' => 2,
+ 'content' => 'B - This is a message!',
+ 'sender_user_id' => 1,
+ 'receiver_user_id' => 2,
+ ]);
+ Message::create([
+ 'id' => 3,
+ 'content' => 'C - This is a message!',
+ 'sender_user_id' => 2,
+ 'receiver_user_id' => 1,
+ ]);
+ Message::create([
+ 'id' => 4,
+ 'content' => 'D - This is a message!',
+ 'sender_user_id' => 2,
+ 'receiver_user_id' => 2,
+ ]);
+ }
+
+ public function testHasManyMergedWithTwoUsersWereBothAreSenderOrReceiverOfTheSameFourMessagesWithLazyLoading()
+ {
+ // Arrange
+ $this->createTwoUsersWereBothAreSenderOrReceiverOfTheSameFourMessages();
+
+ // Act
+ $this->db::connection()->enableQueryLog();
+ $user1 = User::find(11);
+ $user2 = User::find(12);
+ $messagesOfUser1 = $user1->messages;
+ $messagesOfUser2 = $user2->messages;
+ $queries = $this->db::getQueryLog();
+ $this->db::connection()->disableQueryLog();
+
+ // Assert
+ $this->assertEquals(4, count($queries));
+ $this->assertEquals(3, $messagesOfUser1->count());
+ $this->assertEquals(3, $messagesOfUser2->count());
+ $this->assertEquals(2, $user1->receivedMessages()->count());
+ $this->assertEquals(2, $user1->sentMessages()->count());
+ $this->assertEquals(2, $user2->receivedMessages()->count());
+ $this->assertEquals(2, $user2->sentMessages()->count());
+ }
+
+ public function testHasManyMergedWithTwoUsersWereBothAreSenderOrReceiverOfTheSameFourMessagesWithEagerLoading()
+ {
+ // Arrange
+ $this->createTwoUsersWereBothAreSenderOrReceiverOfTheSameFourMessages();
+
+ // Act
+ $this->db::connection()->enableQueryLog();
+ $users = User::with(['messages'])->get();
+ $user1 = $users->firstWhere('id', 11);
+ $user2 = $users->firstWhere('id', 12);
+ $messagesOfUser1 = $user1->messages;
+ $messagesOfUser2 = $user2->messages;
+ $queries = $this->db::getQueryLog();
+ $this->db::connection()->disableQueryLog();
+
+ // Assert
+ $this->assertEquals(2, count($queries));
+ $this->assertEquals(3, $messagesOfUser1->count());
+ $this->assertEquals(3, $messagesOfUser2->count());
+ $this->assertEquals(2, $user1->receivedMessages()->count());
+ $this->assertEquals(2, $user1->sentMessages()->count());
+ $this->assertEquals(2, $user2->receivedMessages()->count());
+ $this->assertEquals(2, $user2->sentMessages()->count());
+ }
+
+ public function testHasManyMergedWithTwoUsersWereBothAreSenderOrReceiverOfTheSameFourMessagesWithLazyEagerLoading()
+ {
+ // Arrange
+ $this->createTwoUsersWereBothAreSenderOrReceiverOfTheSameFourMessages();
+
+ // Act
+ $this->db::connection()->enableQueryLog();
+ $users = User::all();
+ $users->load(['messages']);
+ $user1 = $users->firstWhere('id', 11);
+ $user2 = $users->firstWhere('id', 12);
+ $messagesOfUser1 = $user1->messages;
+ $messagesOfUser2 = $user2->messages;
+ $queries = $this->db::getQueryLog();
+ $this->db::connection()->disableQueryLog();
+
+ // Assert
+ $this->assertEquals(2, count($queries));
+ $this->assertEquals(3, $messagesOfUser1->count());
+ $this->assertEquals(3, $messagesOfUser2->count());
+ $this->assertEquals(2, $user1->receivedMessages()->count());
+ $this->assertEquals(2, $user1->sentMessages()->count());
+ $this->assertEquals(2, $user2->receivedMessages()->count());
+ $this->assertEquals(2, $user2->sentMessages()->count());
+ }
+
+ public function testHasManyMergedWithTwoUsersWereBothAreSenderOrReceiverOfTheSameFourMessagesWithConstrainedEagerLoading()
+ {
+ // Arrange
+ $this->createTwoUsersWereBothAreSenderOrReceiverOfTheSameFourMessages();
+
+ // Act
+ $this->db::connection()->enableQueryLog();
+ $users = User::with([
+ 'messages' => function (HasManyMerged $builder) {
+ $builder->where('content', 'like', 'A -%');
+ },
+ ])->get();
+ $user1 = $users->firstWhere('id', 11);
+ $user2 = $users->firstWhere('id', 12);
+ $messagesOfUser1 = $user1->messages;
+ $messagesOfUser2 = $user2->messages;
+ $queries = $this->db::getQueryLog();
+ $this->db::connection()->disableQueryLog();
+
+ // Assert
+ $this->assertEquals(2, count($queries));
+ $this->assertEquals(1, $messagesOfUser1->count());
+ $this->assertEquals(0, $messagesOfUser2->count());
+ $this->assertEquals(2, $user1->receivedMessages()->count());
+ $this->assertEquals(2, $user1->sentMessages()->count());
+ $this->assertEquals(2, $user2->receivedMessages()->count());
+ $this->assertEquals(2, $user2->sentMessages()->count());
+ }
+
+ public function testHasManyMergedWithTwoUsersWereBothAreSenderOrReceiverOfTheSameFourMessagesWithEagerLoadingAndOrderCheck()
+ {
+ // Arrange
+ $this->createTwoUsersWereBothAreSenderOrReceiverOfTheSameFourMessages();
+
+ // Act
+ $this->db::connection()->enableQueryLog();
+ $users = User::with([
+ 'messages' => function (HasManyMerged $builder) {
+ $builder->orderBy('id', 'desc');
+ },
+ ])->get();
+ $user1 = $users->firstWhere('id', 11);
+ $user2 = $users->firstWhere('id', 12);
+ $messagesOfUser1 = $user1->messages;
+ $messagesOfUser2 = $user2->messages;
+ $queries = $this->db::getQueryLog();
+ $this->db::connection()->disableQueryLog();
+
+ // Assert
+ $this->assertEquals(2, count($queries));
+ $this->assertEquals(3, $messagesOfUser1->count());
+ $this->assertEquals(3, $messagesOfUser2->count());
+ $this->assertEquals([3, 2, 1], $messagesOfUser1->pluck('id')->toArray());
+ $this->assertEquals([4, 3, 2], $messagesOfUser2->pluck('id')->toArray());
+ $this->assertEquals(2, $user1->receivedMessages()->count());
+ $this->assertEquals(2, $user1->sentMessages()->count());
+ $this->assertEquals(2, $user2->receivedMessages()->count());
+ $this->assertEquals(2, $user2->sentMessages()->count());
+ }
+}
diff --git a/tests/Models/Message.php b/tests/Models/Message.php
new file mode 100644
index 0000000..6ddbddb
--- /dev/null
+++ b/tests/Models/Message.php
@@ -0,0 +1,40 @@
+belongsTo(User::class, 'sender_user_id');
+ }
+
+ /**
+ * @return BelongsTo|User
+ */
+ public function receiver()
+ {
+ return $this->belongsTo(User::class, 'receiver_user_id');
+ }
+}
diff --git a/tests/Models/User.php b/tests/Models/User.php
new file mode 100644
index 0000000..f52d565
--- /dev/null
+++ b/tests/Models/User.php
@@ -0,0 +1,55 @@
+hasManyMerged(Message::class, ['sender_user_id', 'receiver_user_id'], 'other_unique_id');
+ }
+
+ /**
+ * @return HasMany|Message
+ */
+ public function sentMessages()
+ {
+ return $this->hasMany(Message::class, 'sender_user_id', 'other_unique_id');
+ }
+
+ /**
+ * @return HasMany|Message
+ */
+ public function receivedMessages()
+ {
+ return $this->hasMany(Message::class, 'receiver_user_id', 'other_unique_id');
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..b7077d9
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,43 @@
+db = new Manager();
+ $this->db->addConnection([
+ 'driver' => 'sqlite',
+ 'database' => ':memory:',
+ 'prefix' => '',
+ ]);
+ $this->db->setAsGlobal();
+ $this->db->bootEloquent();
+
+ $this->db::schema()->create('users', function (Blueprint $table) {
+ $table->increments('id');
+ $table->unsignedInteger('other_unique_id')->unique();
+ $table->string('name');
+ $table->timestamps();
+ $table->softDeletes();
+ });
+
+ $this->db::schema()->create('messages', function (Blueprint $table) {
+ $table->increments('id');
+ $table->text('content');
+ $table->unsignedInteger('sender_user_id');
+ $table->unsignedInteger('receiver_user_id');
+ $table->timestamps();
+ $table->softDeletes();
+ });
+ }
+}