diff --git a/.editorconfig b/.editorconfig index fcdf61edc..80ce1de38 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,4 +6,7 @@ end_of_line = lf insert_final_newline = true indent_style = space indent_size = 4 -trim_trailing_whitespace = true \ No newline at end of file +trim_trailing_whitespace = true + +[*.yml] +indent_size = 2 diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml index 97b1e8a32..3664d752e 100644 --- a/.github/workflows/build-ci.yml +++ b/.github/workflows/build-ci.yml @@ -2,8 +2,6 @@ name: CI on: push: - branches: - tags: pull_request: jobs: @@ -55,7 +53,7 @@ jobs: - name: Show Docker version run: if [[ "$DEBUG" == "true" ]]; then docker version && env; fi env: - DEBUG: ${{secrets.DEBUG}} + DEBUG: ${{ secrets.DEBUG }} - name: Download Composer cache dependencies from cache id: composer-cache run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT @@ -66,14 +64,8 @@ jobs: 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 --prefer-stable') + run: composer update --no-interaction $([[ "${{ matrix.mode }}" == low-deps ]] && echo ' --prefer-lowest --prefer-stable') - name: Run tests - run: | - ./vendor/bin/phpunit --coverage-clover coverage.xml + run: ./vendor/bin/phpunit --coverage-clover coverage.xml env: MONGODB_URI: 'mongodb://127.0.0.1/?replicaSet=rs' - - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: false diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index e75ca3c53..c6f730d33 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -2,8 +2,6 @@ name: "Coding Standards" on: push: - branches: - tags: pull_request: env: @@ -15,6 +13,11 @@ jobs: 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@v4" @@ -50,6 +53,67 @@ jobs: with: composer-options: "--no-suggest" + - name: "Format the code" + continue-on-error: true + run: | + mkdir .cache + ./vendor/bin/phpcbf + # The -q option is required until phpcs v4 is released - name: "Run PHP_CodeSniffer" run: "vendor/bin/phpcs -q --no-colors --report=checkstyle | cs2pr" + + - name: "Commit the changes" + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "apply phpcbf formatting" + + analysis: + runs-on: "ubuntu-22.04" + continue-on-error: true + strategy: + matrix: + php: + - '8.1' + - '8.2' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: curl, mbstring + tools: composer:v2 + coverage: none + + - name: Cache dependencies + id: composer-cache + uses: actions/cache@v3 + 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@v3 + with: + path: .cache + key: "phpstan-result-cache-${{ github.run_id }}" + restore-keys: | + phpstan-result-cache- + + - name: Run PHPStan + run: ./vendor/bin/phpstan analyse --no-interaction --no-progress --ansi + + - name: Save cache PHPStan results + id: phpstan-cache-save + if: always() + uses: actions/cache/save@v3 + with: + path: .cache + key: ${{ steps.phpstan-cache-restore.outputs.cache-primary-key }} diff --git a/.gitignore b/.gitignore index d69c89d6f..80f343333 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,9 @@ *.sublime-workspace .DS_Store .idea/ -.phpunit.cache/ -.phpcs-cache /vendor composer.lock composer.phar phpunit.xml +phpstan.neon +/.cache/ diff --git a/composer.json b/composer.json index 9f605c667..b04425751 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,8 @@ "orchestra/testbench": "^8.0", "mockery/mockery": "^1.4.4", "doctrine/coding-standard": "12.0.x-dev", - "spatie/laravel-query-builder": "^5.6" + "spatie/laravel-query-builder": "^5.6", + "phpstan/phpstan": "^1.10" }, "replace": { "jenssegers/mongodb": "self.version" diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 23bc44ab7..5f402d4ce 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -3,7 +3,7 @@ - + diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 000000000..71a44a395 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,16 @@ +parameters: + ignoreErrors: + - + message: "#^Method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:push\\(\\) invoked with 3 parameters, 0 required\\.$#" + count: 3 + path: src/Relations/BelongsToMany.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 diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 000000000..518fe9ab8 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,16 @@ +includes: + - ./phpstan-baseline.neon + +parameters: + tmpDir: .cache/phpstan + + paths: + - src + + level: 2 + + editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%' + + ignoreErrors: + - '#Unsafe usage of new static#' + - '#Call to an undefined method [a-zA-Z0-9\\_\<\>]+::[a-zA-Z]+\(\)#' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7a38678eb..8e5e9d3d6 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,15 +1,13 @@ - + cacheDirectory=".cache/phpunit" + executionOrder="depends,defects" + beStrictAboutCoverageMetadata="true" + beStrictAboutOutputDuringTests="true" + failOnRisky="true" + failOnWarning="true"> tests/ @@ -20,10 +18,15 @@ + + - + + - ./src + ./src diff --git a/src/Eloquent/Builder.php b/src/Eloquent/Builder.php index 948182ad3..b9005c442 100644 --- a/src/Eloquent/Builder.php +++ b/src/Eloquent/Builder.php @@ -16,6 +16,7 @@ use function is_array; use function iterator_to_array; +/** @method \MongoDB\Laravel\Query\Builder toBase() */ class Builder extends EloquentBuilder { use QueriesRelationships; @@ -219,16 +220,15 @@ protected function ensureOrderForCursorPagination($shouldReverse = false) } if ($shouldReverse) { - $this->query->orders = collect($this->query->orders)->map(function ($direction) { - return $direction === 1 ? -1 : 1; - })->toArray(); + $this->query->orders = collect($this->query->orders) + ->map(static fn (int $direction) => $direction === 1 ? -1 : 1) + ->toArray(); } - return collect($this->query->orders)->map(function ($direction, $column) { - return [ + return collect($this->query->orders) + ->map(static fn ($direction, $column) => [ 'column' => $column, 'direction' => $direction === 1 ? 'asc' : 'desc', - ]; - })->values(); + ])->values(); } } diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php index bbd45a32b..bcb672a3c 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -297,10 +297,10 @@ protected function asDecimal($value, $decimals) public function fromJson($value, $asObject = false) { if (! is_string($value)) { - $value = Json::encode($value ?? ''); + $value = Json::encode($value); } - return Json::decode($value ?? '', ! $asObject); + return Json::decode($value, ! $asObject); } /** @inheritdoc */ diff --git a/src/Relations/BelongsToMany.php b/src/Relations/BelongsToMany.php index a1b028c9f..1d6b84ba8 100644 --- a/src/Relations/BelongsToMany.php +++ b/src/Relations/BelongsToMany.php @@ -15,8 +15,8 @@ use function array_map; use function array_merge; use function array_values; +use function assert; use function count; -use function is_array; use function is_numeric; class BelongsToMany extends EloquentBelongsToMany @@ -82,11 +82,11 @@ protected function setWhere() } /** @inheritdoc */ - public function save(Model $model, array $joining = [], $touch = true) + public function save(Model $model, array $pivotAttributes = [], $touch = true) { $model->save(['touch' => false]); - $this->attach($model, $joining, $touch); + $this->attach($model, $pivotAttributes, $touch); return $model; } @@ -126,12 +126,7 @@ public function sync($ids, $detaching = true) // if they exist in the array of current ones, and if not we will insert. $current = $this->parent->{$this->relatedPivotKey} ?: []; - // See issue #256. - if ($current instanceof Collection) { - $current = $ids->modelKeys(); - } - - $records = $this->formatSyncList($ids); + $records = $this->formatRecordsList($ids); $current = Arr::wrap($current); @@ -171,6 +166,7 @@ public function sync($ids, $detaching = true) public function updateExistingPivot($id, array $attributes, $touch = true) { // Do nothing, we have no pivot table. + return $this; } /** @inheritdoc */ @@ -229,6 +225,8 @@ public function detach($ids = [], $touch = true) } // Remove the relation to the parent. + assert($this->parent instanceof \MongoDB\Laravel\Eloquent\Model); + assert($query instanceof \MongoDB\Laravel\Eloquent\Builder); $query->pull($this->foreignPivotKey, $this->parent->getKey()); if ($touch) { @@ -266,7 +264,7 @@ public function newPivotQuery() /** * Create a new query builder for the related model. * - * @return \Illuminate\Database\Query\Builder + * @return Builder|Model */ public function newRelatedQuery() { @@ -295,28 +293,6 @@ public function getQualifiedRelatedPivotKeyName() return $this->relatedPivotKey; } - /** - * Format the sync list so that it is keyed by ID. (Legacy Support) - * The original function has been renamed to formatRecordsList since Laravel 5.3. - * - * @deprecated - * - * @return array - */ - protected function formatSyncList(array $records) - { - $results = []; - foreach ($records as $id => $attributes) { - if (! is_array($attributes)) { - [$id, $attributes] = [$attributes, []]; - } - - $results[$id] = $attributes; - } - - return $results; - } - /** * Get the name of the "where in" method for eager loading. * diff --git a/src/Relations/EmbedsMany.php b/src/Relations/EmbedsMany.php index b97849f24..2d68af70b 100644 --- a/src/Relations/EmbedsMany.php +++ b/src/Relations/EmbedsMany.php @@ -9,6 +9,8 @@ use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\Paginator; use MongoDB\BSON\ObjectID; +use MongoDB\Driver\Exception\LogicException; +use MongoDB\Laravel\Eloquent\Model as MongoDBModel; use function array_key_exists; use function array_values; @@ -16,6 +18,7 @@ use function in_array; use function is_array; use function method_exists; +use function throw_if; class EmbedsMany extends EmbedsOneOrMany { @@ -82,7 +85,7 @@ public function performUpdate(Model $model) // Get the correct foreign key value. $foreignKey = $this->getForeignKeyValue($model); - $values = $this->getUpdateValues($model->getDirty(), $this->localKey . '.$.'); + $values = self::getUpdateValues($model->getDirty(), $this->localKey . '.$.'); // Update document in database. $result = $this->toBase()->where($this->localKey . '.' . $model->getKeyName(), $foreignKey) @@ -195,10 +198,14 @@ public function destroy($ids = []) /** * Delete all embedded models. * - * @return int + * @param null $id + * + * @note The $id is not used to delete embedded models. */ - public function delete() + 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 => []]); @@ -224,9 +231,9 @@ public function detach($ids = []) /** * Save alias. * - * @return Model + * @return MongoDBModel */ - public function attach(Model $model) + public function attach(MongoDBModel $model) { return $this->save($model); } @@ -322,13 +329,13 @@ protected function getEmbedded() } /** @inheritdoc */ - protected function setEmbedded($models) + protected function setEmbedded($records) { - if (! is_array($models)) { - $models = [$models]; + if (! is_array($records)) { + $records = [$records]; } - return parent::setEmbedded(array_values($models)); + return parent::setEmbedded(array_values($records)); } /** @inheritdoc */ diff --git a/src/Relations/EmbedsOne.php b/src/Relations/EmbedsOne.php index 196415a55..678141cf1 100644 --- a/src/Relations/EmbedsOne.php +++ b/src/Relations/EmbedsOne.php @@ -6,6 +6,10 @@ use Illuminate\Database\Eloquent\Model; use MongoDB\BSON\ObjectID; +use MongoDB\Driver\Exception\LogicException; +use Throwable; + +use function throw_if; class EmbedsOne extends EmbedsOneOrMany { @@ -73,7 +77,7 @@ public function performUpdate(Model $model) return $this->parent->save(); } - $values = $this->getUpdateValues($model->getDirty(), $this->localKey . '.'); + $values = self::getUpdateValues($model->getDirty(), $this->localKey . '.'); $result = $this->toBase()->update($values); @@ -133,10 +137,16 @@ public function dissociate() /** * Delete all embedded models. * - * @return int + * @param ?string $id + * + * @throws LogicException|Throwable + * + * @note The $id is not used to delete embedded models. */ - public function delete() + public function delete($id = null): int { + throw_if($id !== null, new LogicException('The id parameter should not be used.')); + return $this->performDelete(); } diff --git a/src/Relations/EmbedsOneOrMany.php b/src/Relations/EmbedsOneOrMany.php index 46f4f1e72..56fc62041 100644 --- a/src/Relations/EmbedsOneOrMany.php +++ b/src/Relations/EmbedsOneOrMany.php @@ -8,11 +8,15 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model as EloquentModel; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Database\Query\Expression; +use MongoDB\Driver\Exception\LogicException; use MongoDB\Laravel\Eloquent\Model; +use Throwable; use function array_merge; use function count; use function is_array; +use function throw_if; abstract class EmbedsOneOrMany extends Relation { @@ -42,8 +46,8 @@ abstract class EmbedsOneOrMany extends Relation */ public function __construct(Builder $query, Model $parent, Model $related, string $localKey, string $foreignKey, string $relation) { - $this->query = $query; - $this->parent = $parent; + parent::__construct($query, $parent); + $this->related = $related; $this->localKey = $localKey; $this->foreignKey = $foreignKey; @@ -54,8 +58,6 @@ public function __construct(Builder $query, Model $parent, Model $related, strin if ($parentRelation) { $this->query = $parentRelation->getQuery(); } - - $this->addConstraints(); } /** @inheritdoc */ @@ -101,10 +103,16 @@ public function get($columns = ['*']) /** * Get the number of embedded models. * - * @return int + * @param Expression|string $columns + * + * @throws LogicException|Throwable + * + * @note The $column parameter is not used to count embedded models. */ - public function count() + public function count($columns = '*'): int { + throw_if($columns !== '*', new LogicException('The columns parameter should not be used.')); + return count($this->getEmbedded()); } @@ -261,21 +269,21 @@ protected function toCollection(array $records = []) /** * Create a related model instanced. * - * @param array $attributes + * @param mixed $attributes * - * @return Model + * @return Model | null */ - protected function toModel($attributes = []) + protected function toModel(mixed $attributes = []): Model|null { if ($attributes === null) { - return; + return null; } $connection = $this->related->getConnection(); $model = $this->related->newFromBuilder( (array) $attributes, - $connection ? $connection->getName() : null, + $connection?->getName(), ); $model->setParentRelation($this); @@ -394,8 +402,8 @@ public function getQualifiedForeignKeyName() /** * Get the name of the "where in" method for eager loading. * - * @param \Illuminate\Database\Eloquent\Model $model - * @param string $key + * @param EloquentModel $model + * @param string $key * * @return string */ diff --git a/src/Relations/MorphToMany.php b/src/Relations/MorphToMany.php index 9c9576d90..a2c55969f 100644 --- a/src/Relations/MorphToMany.php +++ b/src/Relations/MorphToMany.php @@ -85,11 +85,11 @@ protected function setWhere() } /** @inheritdoc */ - public function save(Model $model, array $joining = [], $touch = true) + public function save(Model $model, array $pivotAttributes = [], $touch = true) { $model->save(['touch' => false]); - $this->attach($model, $joining, $touch); + $this->attach($model, $pivotAttributes, $touch); return $model; } @@ -133,11 +133,6 @@ public function sync($ids, $detaching = true) $current = $this->parent->{$this->relatedPivotKey} ?: []; } - // See issue #256. - if ($current instanceof Collection) { - $current = $this->parseIds($current); - } - $records = $this->formatRecordsList($ids); $current = Arr::wrap($current); @@ -175,7 +170,7 @@ public function sync($ids, $detaching = true) } /** @inheritdoc */ - public function updateExistingPivot($id, array $attributes, $touch = true) + public function updateExistingPivot($id, array $attributes, $touch = true): void { // Do nothing, we have no pivot table. } @@ -272,12 +267,13 @@ public function detach($ids = [], $touch = true) // Remove the relation from the parent. $data = []; foreach ($ids as $item) { - $data = array_merge($data, [ + $data = [ + ...$data, [ $this->relatedPivotKey => $item, - $this->morphType => $this->related->getMorphClass(), + $this->morphType => $this->related->getMorphClass(), ], - ]); + ]; } $this->parent->pull($this->table, $data); @@ -378,8 +374,8 @@ protected function whereInMethod(Model $model, $key) /** * Extract ids from given pivot table data * - * @param array $data - * @param string|null $relatedPivotKey + * @param array $data + * @param string|null $relatedPivotKey * * @return mixed */ diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php index 6dd28d3b2..52a5762f5 100644 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -44,6 +44,8 @@ class Blueprint extends SchemaBlueprint */ public function __construct(Connection $connection, string $collection) { + parent::__construct($collection); + $this->connection = $connection; $this->collection = $this->connection->getCollection($collection); @@ -82,11 +84,11 @@ public function primary($columns = null, $name = null, $algorithm = null, $optio } /** @inheritdoc */ - public function dropIndex($indexOrColumns = null) + public function dropIndex($index = null) { - $indexOrColumns = $this->transformColumns($indexOrColumns); + $index = $this->transformColumns($index); - $this->collection->dropIndex($indexOrColumns); + $this->collection->dropIndex($index); return $this; } @@ -275,6 +277,8 @@ public function create($options = []) public function drop() { $this->collection->drop(); + + return $this; } /** @inheritdoc */ @@ -339,11 +343,11 @@ protected function fluent($columns = null) * Allows the use of unsupported schema methods. * * @param string $method - * @param array $args + * @param array $parameters * * @return Blueprint */ - public function __call($method, $args) + public function __call($method, $parameters) { // Dummy. return $this; diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index af311df6c..bfa0e4715 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -44,9 +44,9 @@ public function hasCollection($name) } /** @inheritdoc */ - public function hasTable($collection) + public function hasTable($table) { - return $this->hasCollection($collection); + return $this->hasCollection($table); } /** @@ -66,15 +66,15 @@ public function collection($collection, Closure $callback) } /** @inheritdoc */ - public function table($collection, Closure $callback) + public function table($table, Closure $callback) { - $this->collection($collection, $callback); + $this->collection($table, $callback); } /** @inheritdoc */ - public function create($collection, ?Closure $callback = null, array $options = []) + public function create($table, ?Closure $callback = null, array $options = []) { - $blueprint = $this->createBlueprint($collection); + $blueprint = $this->createBlueprint($table); $blueprint->create($options); @@ -84,17 +84,17 @@ public function create($collection, ?Closure $callback = null, array $options = } /** @inheritdoc */ - public function dropIfExists($collection) + public function dropIfExists($table) { - if ($this->hasCollection($collection)) { - $this->drop($collection); + if ($this->hasCollection($table)) { + $this->drop($table); } } /** @inheritdoc */ - public function drop($collection) + public function drop($table) { - $blueprint = $this->createBlueprint($collection); + $blueprint = $this->createBlueprint($table); $blueprint->drop(); } @@ -108,9 +108,9 @@ public function dropAllTables() } /** @inheritdoc */ - protected function createBlueprint($collection, ?Closure $callback = null) + protected function createBlueprint($table, ?Closure $callback = null) { - return new Blueprint($this->connection, $collection); + return new Blueprint($this->connection, $table); } /**