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(); + }); + } +}