diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml new file mode 100644 index 0000000..0bd18f9 --- /dev/null +++ b/.github/workflows/CI.yaml @@ -0,0 +1,100 @@ +name: Tests + +# Run this workflow every time a new commit pushed to your repository +on: + push: + paths-ignore: + - '**/*.md' + pull_request: + paths-ignore: + - '**/*.md' + +jobs: + tests: + runs-on: ${{ matrix.operating-system }} + if: (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository) + + strategy: + fail-fast: false + matrix: + operating-system: [ubuntu-20.04] + php-versions: ['7.4', '8.0', '8.1'] + dependencies: ['no', 'low', 'beta'] + exclude: + - operating-system: ubuntu-20.04 + php-versions: '8.1' + dependencies: 'low' + + name: PHP ${{ matrix.php-versions }} - ${{ matrix.dependencies }} + + env: + COMPOSER_NO_INTERACTION: 1 + extensions: curl json libxml dom + key: cache-v1 # can be any string, change to clear the extension cache. + + steps: + + # Checks out a copy of your repository on the ubuntu machine + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php-versions }} + extensions: ${{ env.extensions }} + key: ${{ env.key }} + + - name: Cache PHP Extensions + uses: actions/cache@v2 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} + + - name: Cache Composer Dependencies + uses: actions/cache@v1 + with: + path: ~/.composer/cache/files + key: dependencies-composer-${{ hashFiles('composer.json') }} + + - name: Fix beta + if: ${{ matrix.dependencies == 'beta' }} + run: perl -pi -e 's/^}$/,"minimum-stability":"beta"}/' composer.json + + - name: Setup PHP Action + uses: shivammathur/setup-php@2.8.0 + with: + php-version: ${{ matrix.php-versions }} + extensions: ${{ env.extensions }} + coverage: xdebug + tools: pecl, composer + + - name: PHP Show modules + run: php -m + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install Composer dependencies + if: ${{ matrix.dependencies != 'low' }} + run: composer update --no-interaction + + - name: Install Composer dependencies + if: ${{ matrix.dependencies == 'low' }} + run: composer update -vvv --prefer-lowest --prefer-stable --no-interaction + + - name: Validate files + run: composer validate-files + + - name: Run tests + run: composer run-tests \ No newline at end of file diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml deleted file mode 100644 index 438e5dd..0000000 --- a/.github/workflows/run-tests.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Unit Tests - -on: - push: - branches: - - master - pull_request: - branches: - - "*" - schedule: - - cron: '0 0 * * *' - -jobs: - php-tests: - runs-on: ubuntu-latest - timeout-minutes: 15 - env: - COMPOSER_NO_INTERACTION: 1 - - strategy: - fail-fast: false - matrix: - php: [8.1, 8.0, 7.4, 7.3, 7.2] - - name: P${{ matrix.php }} - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - coverage: none - tools: composer:v2 - - - name: Install dependencies - run: | - composer install -o --quiet - - - name: Execute Unit Tests - run: composer test diff --git a/.gitignore b/.gitignore index 6bef520..24a2013 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ composer.phar composer.lock .DS_Store +.php-cs-fixer.cache .phpunit.result.cache diff --git a/README.markdown b/README.markdown index 7e7341e..67cd0bd 100644 --- a/README.markdown +++ b/README.markdown @@ -1,11 +1,15 @@ -[![Build Status](https://travis-ci.org/lazychaser/laravel-nestedset.svg?branch=master)](https://travis-ci.org/lazychaser/laravel-nestedset) -[![Total Downloads](https://poser.pugx.org/kalnoy/nestedset/downloads.svg)](https://packagist.org/packages/kalnoy/nestedset) -[![Latest Stable Version](https://poser.pugx.org/kalnoy/nestedset/v/stable.svg)](https://packagist.org/packages/kalnoy/nestedset) -[![Latest Unstable Version](https://poser.pugx.org/kalnoy/nestedset/v/unstable.svg)](https://packagist.org/packages/kalnoy/nestedset) -[![License](https://poser.pugx.org/kalnoy/nestedset/license.svg)](https://packagist.org/packages/kalnoy/nestedset) +[![Total Downloads](https://poser.pugx.org/lychee-org/nestedset/downloads.svg)](https://packagist.org/packages/lychee-org/nestedset) +[![Latest Stable Version](https://poser.pugx.org/lychee-org/nestedset/v/stable.svg)](https://packagist.org/packages/lychee-org/nestedset) +[![Latest Unstable Version](https://poser.pugx.org/lychee-org/nestedset/v/unstable.svg)](https://packagist.org/packages/lychee-org/nestedset) +[![License](https://poser.pugx.org/lychee-org/nestedset/license.svg)](https://packagist.org/packages/lychee-org/nestedset) This is a Laravel 4-8 package for working with trees in relational databases. +It is a fork of [lazychaser/laravel-nestedset](https://github.com/lazychaser/laravel-nestedset) and contains general patches which are required for using the library with [Lychee](https://github.com/LycheeOrg/Lychee). Note that the patches are **not** specific for Lychee, but a generally useful. Inter alia: + + * Routines respect a foreign key constraint on the parent-child-relation by taking care that changes to the tree are applied in the correct order. + * The code does not fail if the model which uses `NoteTrait` does not directly extend `Model` but indirectly inherits `Model` via another parent class. + * **Laravel 8.0** is supported since v6 * **Laravel 5.7, 5.8, 6.0, 7.0** is supported since v5 * **Laravel 5.5, 5.6** is supported since v4.3 @@ -13,10 +17,6 @@ This is a Laravel 4-8 package for working with trees in relational databases. * **Laravel 5.1** is supported in v3 * **Laravel 4** is supported in v2 -Although this project is completely free for use, I appreciate any support! - -- __[Donate via PayPal](https://www.paypal.me/lazychaser)__ - __Contents:__ - [Theory](#what-are-nested-sets) diff --git a/composer.json b/composer.json index ff3df84..fb15894 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { - "name": "kalnoy/nestedset", - "description": "Nested Set Model for Laravel 5.7 and up", + "name": "lychee-org/nestedset", + "description": "Nested Set Model for Laravel 5.7 and up (fork with patches for Lychee)", "keywords": ["laravel", "nested sets", "nsm", "database", "hierarchy"], "license": "MIT", @@ -25,9 +25,19 @@ }, "require-dev": { - "phpunit/phpunit": "7.*|8.*|9.*" + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpunit/phpunit": "^9.5.20" }, + "scripts": { + "run-tests": [ + "vendor/bin/phpunit -c phpunit.xml", + "vendor/bin/phpunit -c phpunit.xml --coverage-clover=coverage.xml" + ], + "validate-files": [ + "vendor/bin/parallel-lint --exclude vendor ." + ] + }, "minimum-stability": "dev", "prefer-stable": true, @@ -41,10 +51,5 @@ "Kalnoy\\Nestedset\\NestedSetServiceProvider" ] } - }, - "scripts": { - "test": [ - "@php ./vendor/bin/phpunit" - ] } } diff --git a/phpunit.xml b/phpunit.xml index 1e71a58..ab23f7f 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,22 +1,13 @@ - - - - ./tests/ - - - - - - ./src - - + + + + ./src + + + + + ./tests/ + + \ No newline at end of file diff --git a/src/BaseRelation.php b/src/BaseRelation.php index 031eecf..e2bd7db 100644 --- a/src/BaseRelation.php +++ b/src/BaseRelation.php @@ -144,7 +144,7 @@ public function addEagerConstraints(array $models) // The first model in the array is always the parent, so add the scope constraints based on that model. // @link https://github.com/laravel/framework/pull/25240 // @link https://github.com/lazychaser/laravel-nestedset/issues/351 - optional($models[0])->applyNestedSetScope($this->query); + optional(reset($models))->applyNestedSetScope($this->query); $this->query->whereNested(function (Builder $inner) use ($models) { // We will use this query in order to apply constraints to the diff --git a/src/NestedSet.php b/src/NestedSet.php index 8ec8e02..7045471 100644 --- a/src/NestedSet.php +++ b/src/NestedSet.php @@ -77,7 +77,7 @@ public static function getDefaultColumns() */ public static function isNode($node) { - return is_object($node) && in_array(NodeTrait::class, (array)$node); + return $node instanceof Node; } } \ No newline at end of file diff --git a/src/Node.php b/src/Node.php new file mode 100644 index 0000000..dfcb12f --- /dev/null +++ b/src/Node.php @@ -0,0 +1,363 @@ + + */ + public function getNextSiblings(array $columns = ['*']); + + /** + * @param array $columns + * + * @return Collection + */ + public function getPrevSiblings(array $columns = ['*']); + + /** + * @param array $columns + * + * @return Node + */ + public function getNextSibling(array $columns = ['*']); + + /** + * @param array $columns + * + * @return Node + */ + public function getPrevSibling(array $columns = ['*']); + + /** + * @return array + */ + public function getBounds(); + + /** + * @param $value + * + * @return $this + */ + public function setLft($value); + + /** + * @param $value + * + * @return $this + */ + public function setRgt($value); + + /** + * @param $value + * + * @return $this + */ + public function setParentId($value); + + /** + * @param array|null $except + * + * @return $this + */ + public function replicate(array $except = null); +} diff --git a/src/NodeTrait.php b/src/NodeTrait.php index 0b985ab..5e9ee78 100644 --- a/src/NodeTrait.php +++ b/src/NodeTrait.php @@ -49,10 +49,9 @@ public static function bootNodeTrait() static::deleting(function ($model) { // We will need fresh data to delete node safely + // We must delete the descendants BEFORE we delete the actual + // album to avoid failing FOREIGN key constraints. $model->refreshNode(); - }); - - static::deleted(function ($model) { $model->deleteDescendants(); }); @@ -628,7 +627,31 @@ protected function deleteDescendants() ? 'forceDelete' : 'delete'; - $this->descendants()->{$method}(); + // We must delete the nodes in correct order to avoid failing + // foreign key constraints when we delete an entire subtree. + // For MySQL we must avoid that a parent is deleted before its + // children although the complete subtree will be deleted eventually. + // Hence, deletion must start with the deepest node, i.e. with the + // highest _lft value first. + // Note: `DELETE ... ORDER BY` is non-standard SQL but required by + // MySQL (see https://dev.mysql.com/doc/refman/8.0/en/delete.html), + // because MySQL only supports "row consistency". + // This means the DB must be consistent before and after every single + // operation on a row. + // This is contrasted by statement and transaction consistency which + // means that the DB must be consistent before and after every + // completed statement/transaction. + // (See https://dev.mysql.com/doc/refman/8.0/en/ansi-diff-foreign-keys.html) + // ANSI Standard SQL requires support for statement/transaction + // consistency, but only PostgreSQL supports it. + // (Good PosgreSQL :-) ) + // PostgreSQL does not support `DELETE ... ORDER BY` but also has no + // need for it. + // The grammar compiler removes the superfluous "ORDER BY" for + // PostgreSQL. + $this->descendants() + ->orderBy($this->getLftName(), 'desc') + ->{$method}(); if ($this->hardDeleting()) { $height = $rgt - $lft + 1; diff --git a/tests/models/Category.php b/tests/models/Category.php index bcce8e9..0d336f3 100644 --- a/tests/models/Category.php +++ b/tests/models/Category.php @@ -2,7 +2,7 @@ use \Illuminate\Database\Eloquent\Model; -class Category extends Model { +class Category extends Model implements \Kalnoy\Nestedset\Node { use \Illuminate\Database\Eloquent\SoftDeletes, \Kalnoy\Nestedset\NodeTrait; diff --git a/tests/models/DuplicateCategory.php b/tests/models/DuplicateCategory.php index a6f619a..34311a9 100644 --- a/tests/models/DuplicateCategory.php +++ b/tests/models/DuplicateCategory.php @@ -1,6 +1,6 @@