diff --git a/.env.testing.example b/.env.testing.example new file mode 100644 index 0000000..df91510 --- /dev/null +++ b/.env.testing.example @@ -0,0 +1,7 @@ +DB_DRIVER=mysql +DB_HOST=127.0.0.1 +DB_PORT=5506 +DB_USERNAME=user +DB_PASSWORD=userpass +DB_NAME=closuretabletest +DB_COLLATION=utf8_unicode_ci \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5826402..1785821 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ +/.idea/ +/docker /vendor +/.env.testing +/docker-compose.yaml +/_ide_helper.php composer.phar -composer.lock -.DS_Store +.DS_Store \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 74d2e8b..b74a4c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,21 +1,115 @@ language: php +services: + - mysql + - postgresql + +cache: + directories: + - .composer + php: - - 5.4 - - 5.5 - - 5.6 - - hhvm + - 7.0 + - 7.1 + - 7.2 + - 7.3 + - 7.4 + +env: + - LARAVEL_VERSION=5.4.* + - LARAVEL_VERSION=5.4.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - LARAVEL_VERSION=5.5.* + - LARAVEL_VERSION=5.5.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - LARAVEL_VERSION=5.6.* + - LARAVEL_VERSION=5.6.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - LARAVEL_VERSION=5.7.* + - LARAVEL_VERSION=5.7.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - LARAVEL_VERSION=5.8.* + - LARAVEL_VERSION=5.8.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - LARAVEL_VERSION=6.* + - LARAVEL_VERSION=6.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - LARAVEL_VERSION=7.* + - LARAVEL_VERSION=7.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + +matrix: + exclude: + - php: 7.0 + env: LARAVEL_VERSION=5.6.* + - php: 7.4 + env: LARAVEL_VERSION=5.6.* + - php: 7.0 + env: LARAVEL_VERSION=5.6.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - php: 7.4 + env: LARAVEL_VERSION=5.6.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - php: 7.0 + env: LARAVEL_VERSION=5.7.* + - php: 7.4 + env: LARAVEL_VERSION=5.7.* + - php: 7.0 + env: LARAVEL_VERSION=5.7.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - php: 7.4 + env: LARAVEL_VERSION=5.7.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - php: 7.0 + env: LARAVEL_VERSION=5.8.* + - php: 7.1 + env: LARAVEL_VERSION=5.8.* + - php: 7.0 + env: LARAVEL_VERSION=5.8.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - php: 7.1 + env: LARAVEL_VERSION=5.8.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - php: 7.0 + env: LARAVEL_VERSION=6.* + - php: 7.1 + env: LARAVEL_VERSION=6.* + - php: 7.0 + env: LARAVEL_VERSION=6.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - php: 7.1 + env: LARAVEL_VERSION=6.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - php: 7.0 + env: LARAVEL_VERSION=7.* + - php: 7.1 + env: LARAVEL_VERSION=7.* + - php: 7.0 + env: LARAVEL_VERSION=7.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - php: 7.1 + env: LARAVEL_VERSION=7.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - php: 7.1 + env: LARAVEL_VERSION=5.4.* + - php: 7.2 + env: LARAVEL_VERSION=5.4.* + - php: 7.3 + env: LARAVEL_VERSION=5.4.* + - php: 7.4 + env: LARAVEL_VERSION=5.4.* + - php: 7.1 + env: LARAVEL_VERSION=5.4.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - php: 7.2 + env: LARAVEL_VERSION=5.4.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - php: 7.3 + env: LARAVEL_VERSION=5.4.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis + - php: 7.4 + env: LARAVEL_VERSION=5.4.* DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=travis DB_NAME=travis -sudo: false +# ensure that the specific Laravel version is required +before_install: + - export COMPOSER_CACHE_DIR=`pwd`/.composer + - composer require "laravel/framework:${LARAVEL_VERSION}" --no-update -install: travis_retry composer install --no-interaction --prefer-source +install: composer update --no-interaction --prefer-dist before_script: - mysql -e 'create database closuretabletest;' + - php ./tests/script-change-testcase-return-type.php -script: vendor/bin/phpunit +script: + - vendor/bin/phpunit branches: only: - master - - feature/laravel-5 + - 6.x + - improve-travis + - postgres-testing + # version tag, e.g. v1.0.0 + - /^v\d+\.\d+(\.\d+)?(-\S*)?$/ + - /^\d+\.\d+?$/ diff --git a/README.md b/README.md index 5ee2d00..e015c4d 100644 --- a/README.md +++ b/README.md @@ -3,219 +3,437 @@ [![Latest Stable Version](https://poser.pugx.org/franzose/closure-table/v/stable.png)](https://packagist.org/packages/franzose/closure-table) [![Total Downloads](https://poser.pugx.org/franzose/closure-table/downloads.png)](https://packagist.org/packages/franzose/closure-table) -## Branches -1. L4 supports Laravel 4 -2. L5.1 supports Laravel < 5.2 -3. L5.3 supports Laravel 5.2-5.3 -4. L5.4 supports Laravel 5.4 -5. master is for any actual Laravel version, so be careful +This is a database manipulation package for the Laravel 5.4+ framework. You may want to use it when you need to store and operate hierarchical data in your database. The package is an implementation of a well-known design pattern called [closure table](https://www.slideshare.net/billkarwin/models-for-hierarchical-data). However, in order to simplify and optimize SQL `SELECT` queries, it uses adjacency lists to query direct parent/child relationships. + +Contents: +- [Installation](#installation) +- [Setup](#setup) +- [Requirements](#requirements) +- Examples → [List of Scopes](#scopes) +- Examples → [Parent/Root](#parentroot) +- Examples → [Ancestors](#ancestors) +- Examples → [Descendants](#descendants) +- Examples → [Children](#children) +- Examples → [Siblings](#siblings) +- Examples → [Tree](#tree) +- Examples → [Collection Methods](#collection-methods) -Hi, this is a database package for Laravel. It's intended to use when you need to operate hierarchical data in database. The package is an implementation of a well-known database design pattern called Closure Table. The package includes generators for models and migrations. ## Installation -To install the package, put the following in your composer.json: - -```json -"require": { - "franzose/closure-table": "4.*" -} +It's strongly recommended to use [Composer](https://getcomposer.org) to install the package: +```bash +$ composer require franzose/closure-table ``` -And to `app/config/app.php`: +If you use Laravel 5.5+, the package's service provider is automatically registered for you thanks to the [package auto-discovery](https://laravel.com/docs/7.x/packages#package-discovery) feature. Otherwise, you have to manually add it to your `config/app.php`: ```php -'providers' => array( - // ... - 'Franzose\ClosureTable\ClosureTableServiceProvider', - ), -``` + [ + Franzose\ClosureTable\ClosureTableServiceProvider::class + ] +]; +``` +## Setup +In a basic scenario, you can simply run the following command: ```bash -php artisan closuretable:make --entity=page +$ php artisan closuretable:make Node ``` +Where `Node` is the name of the entity model. This is what you get from running the above:
+1. Two models in the `app` directory: `App\Node` and `App\NodeClosure`
+2. A new migration in the `database/migrations` directory + +As you can see, the command requires a single argument, name of the entity model. However, it accepts several options in order to provide some sort of customization: + Option | Alias | Meaning + ----------------| ------| ------- + namespace | ns | Custom namespace for generated models. Keep in mind that the given namespace will override model namespaces: `php artisan closuretable:make Foo\\Node --namespace=Qux --closure=Bar\\NodeTree` will generate `Qux\Node` and `Qux\NodeTree` models. + entity-table | et | Database table name for the entity model + closure | c | Class name for the closure model + closure-table | ct | Database table name for the closure model + models-path | mdl | Directory in which to put generated models + migrations-path | mgr | Directory in which to put generated migrations + use-innodb | i | This flag will tell the generator to set database engine to InnoDB. Useful only if you use MySQL + +## Requirements +You have to keep in mind that, by design of this package, the models/tables have a required minimum of attributes/columns: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Entity
Attribute/ColumnCustomized byMeaning
parent_idEntity::getParentIdColumn()ID of the node's immediate parent, simplifies queries for immediate parent/child nodes.
positionEntity::getPositionColumn()Node position, allows to order nodes of the same depth level
ClosureTable
Attribute/ColumnCustomized byMeaning
id
ancestorClosureTable::getAncestorColumn()Parent (self, immediate, distant) node ID
descendantClosureTable::getDescendantColumn()Child (self, immediate, distant) node ID
depthClosureTable::getDepthColumn()Current nesting level, 0+
+ +## Examples +In the examples, let's assume that we've set up a `Node` model which extends the `Franzose\ClosureTable\Models\Entity` model. + +### Scopes +Since ClosureTable 6, a lot of query scopes have become available in the Entity model: -All options of the command:
-1. `--namespace`, `-ns` _[optional]_: namespace for classes, set by `--entity` and `--closure` options, helps to avoid namespace duplication in those options
-2. `--entity`, `-e`: entity class name; if namespaced name is used, then the default closure class name will be prepended with that namespace
-3. `--entity-table`, `-et` _[optional]_: entity table name
-4. `--closure`, `-c` _[optional]_: closure class name
-5. `--closure-table` _[optional]_, `-ct`: closure table name
-6. `--models-path`, `-mdl` _[optional]_: custom models path
-7. `--migrations-path`, `-mgr` _[optional]_: custom migrations path
-8. `--use-innodb` and `-i` _[optional]_: InnoDB migrations have been made optional as well with new paramaters. Setting this will enable the InnoDB engine. - -That's almost all, folks! The ‘dummy’ stuff has just been created for you. You will need to add some fields to your entity migration because the created ‘dummy’ includes just **required** `id`, `parent_id`, `position`, and `real depth` columns:
- -1. **`id`** is a regular autoincremented column
-2. **`parent_id`** column is used to simplify immediate ancestor querying and, for example, to simplify building the whole tree
-3. **`position`** column is used widely by the package to make entities sortable
-4. **`real depth`** column is also used to simplify queries and reduce their number - -By default, entity’s closure table includes the following columns:
-1. **Autoincremented identifier**
-2. **Ancestor column** points on a parent node
-3. **Descendant column** points on a child node
-4. **Depth column** shows a node depth in the tree +```php +ancestors() +ancestorsOf($id) +ancestorsWithSelf() +ancestorsWithSelfOf($id) +descendants() +descendantsOf($id) +descendantsWithSelf() +descendantsWithSelfOf($id) +childNode() +childNodeOf($id) +childAt(int $position) +childOf($id, int $position) +firstChild() +firstChildOf($id) +lastChild() +lastChildOf($id) +childrenRange(int $from, int $to = null) +childrenRangeOf($id, int $from, int $to = null) +sibling() +siblingOf($id) +siblings() +siblingsOf($id) +neighbors() +neighborsOf($id) +siblingAt(int $position) +siblingOfAt($id, int $position) +firstSibling() +firstSiblingOf($id) +lastSibling() +lastSiblingOf($id) +prevSibling() +prevSiblingOf($id) +prevSiblings() +prevSiblingsOf($id) +nextSibling() +nextSiblingOf($id) +nextSiblings() +nextSiblingsOf($id) +siblingsRange(int $from, int $to = null) +siblingsRangeOf($id, int $from, int $to = null) +``` -It is by closure table pattern design, so remember that you must not delete these four columns. +You can learn how to use query scopes from the [Laravel documentation](https://laravel.com/docs/7.x/eloquent#query-scopes). -Remember that many things are made customizable, so see ‘Customization’ for more information. +### Parent/Root +```php + 1]), + new Node(['id' => 2]), + new Node(['id' => 3]), + new Node(['id' => 4, 'parent_id' => 1]) +]; + +foreach ($nodes as $node) { + $node->save(); +} -## Time of coding -Once your models and their database tables are created, at last, you can start actually coding. Here I will show you ClosureTable's specific approaches. +Node::getRoots()->pluck('id')->toArray(); // [1, 2, 3] +Node::find(1)->isRoot(); // true +Node::find(1)->isParent(); // true +Node::find(4)->isRoot(); // false +Node::find(4)->isParent(); // false -### Direct ancestor (parent) +// make node 4 a root at the fourth position (1 => 0, 2 => 1, 3 => 2, 4 => 3) +$node = Node::find(4)->makeRoot(3); +$node->isRoot(); // true +$node->position; // 3 -```php -$parent = Page::find(15)->getParent(); +Node::find(4)->moveTo(0, Node::find(2)); // same as Node::find(4)->moveTo(0, 2); +Node::find(2)->getChildren()->pluck('id')->toArray(); // [4] ``` ### Ancestors - ```php -$page = Page::find(15); -$ancestors = $page->getAncestors(); -$ancestors = $page->getAncestorsTree(); // Tree structure -$ancestors = $page->getAncestorsWhere('position', '=', 1); -$hasAncestors = $page->hasAncestors(); -$ancestorsNumber = $page->countAncestors(); + 1]), + new Node(['id' => 2, 'parent_id' => 1]), + new Node(['id' => 3, 'parent_id' => 2]), + new Node(['id' => 4, 'parent_id' => 3]) +]; + +foreach ($nodes as $node) { + $node->save(); +} + +Node::find(4)->getAncestors()->pluck('id')->toArray(); // [1, 2, 3] +Node::find(4)->countAncestors(); // 3 +Node::find(4)->hasAncestors(); // true +Node::find(4)->ancestors()->where('id', '>', 1)->get()->pluck('id')->toArray(); // [2, 3]; +Node::find(4)->ancestorsWithSelf()->where('id', '>', 1)->get()->pluck('id')->toArray(); // [2, 3, 4]; +Node::ancestorsOf(4)->where('id', '>', 1)->get()->pluck('id')->toArray(); // [2, 3]; +Node::ancestorsWithSelfOf(4)->where('id', '>', 1)->get()->pluck('id')->toArray(); // [2, 3, 4]; ``` -### Direct descendants (children) +There are several methods that have been deprecated since ClosureTable 6: -```php -$page = Page::find(15); -$children = $page->getChildren(); -$hasChildren = $page->hasChildren(); -$childrenNumber = $page->countChildren(); - -$newChild = new Page(array( - 'title' => 'The title', - 'excerpt' => 'The excerpt', - 'content' => 'The content of a child' -)); - -$newChild2 = new Page(array( - 'title' => 'The title', - 'excerpt' => 'The excerpt', - 'content' => 'The content of a child' -)); - -$page->addChild($newChild); - -//you can set child position -$page->addChild($newChild, 5); - -//you can get the child -$child = $page->addChild($newChild, null, true); - -$page->addChildren([$newChild, $newChild2]); - -$page->getChildAt(5); -$page->getFirstChild(); -$page->getLastChild(); -$page->getChildrenRange(0, 2); - -$page->removeChild(0); -$page->removeChild(0, true); //force delete -$page->removeChildren(0, 3); -$page->removeChildren(0, 3, true); //force delete +```diff +-Node::find(4)->getAncestorsTree(); ++Node::find(4)->getAncestors()->toTree(); + +-Node::find(4)->getAncestorsWhere('id', '>', 1); ++Node::find(4)->ancestors()->where('id', '>', 1)->get(); ``` ### Descendants - ```php -$page = Page::find(15); -$descendants = $page->getDescendants(); -$descendants = $page->getDescendantsWhere('position', '=', 1); -$descendantsTree = $page->getDescendantsTree(); -$hasDescendants = $page->hasDescendants(); -$descendantsNumber = $page->countDescendants(); + 1]), + new Node(['id' => 2, 'parent_id' => 1]), + new Node(['id' => 3, 'parent_id' => 2]), + new Node(['id' => 4, 'parent_id' => 3]) +]; + +foreach ($nodes as $node) { + $node->save(); +} + +Node::find(1)->getDescendants()->pluck('id')->toArray(); // [2, 3, 4] +Node::find(1)->countDescendants(); // 3 +Node::find(1)->hasDescendants(); // true +Node::find(1)->descendants()->where('id', '<', 4)->get()->pluck('id')->toArray(); // [2, 3]; +Node::find(1)->descendantsWithSelf()->where('id', '<', 4)->get()->pluck('id')->toArray(); // [1, 2, 3]; +Node::descendantsOf(1)->where('id', '<', 4)->get()->pluck('id')->toArray(); // [2, 3]; +Node::descendantsWithSelfOf(1)->where('id', '<', 4)->get()->pluck('id')->toArray(); // [1, 2, 3]; ``` -### Siblings +There are several methods that have been deprecated since ClosureTable 6: + +```diff +-Node::find(4)->getDescendantsTree(); ++Node::find(4)->getDescendants()->toTree(); +-Node::find(4)->getDescendantsWhere('foo', '=', 'bar'); ++Node::find(4)->descendants()->where('foo', '=', 'bar')->get(); +``` + +### Children ```php -$page = Page::find(15); -$first = $page->getFirstSibling(); //or $page->getSiblingAt(0); -$last = $page->getLastSibling(); -$atpos = $page->getSiblingAt(5); + 1]), + new Node(['id' => 2, 'parent_id' => 1]), + new Node(['id' => 3, 'parent_id' => 1]), + new Node(['id' => 4, 'parent_id' => 1]), + new Node(['id' => 5, 'parent_id' => 1]), + new Node(['id' => 6, 'parent_id' => 2]), + new Node(['id' => 7, 'parent_id' => 3]) +]; + +foreach ($nodes as $node) { + $node->save(); +} -$prevOne = $page->getPrevSibling(); -$prevAll = $page->getPrevSiblings(); -$hasPrevs = $page->hasPrevSiblings(); -$prevsNumber = $page->countPrevSiblings(); +Node::find(1)->getChildren()->pluck('id')->toArray(); // [2, 3, 4, 5] +Node::find(1)->countChildren(); // 3 +Node::find(1)->hasChildren(); // true -$nextOne = $page->getNextSibling(); -$nextAll = $page->getNextSiblings(); -$hasNext = $page->hasNextSiblings(); -$nextNumber = $page->countNextSiblings(); +// get child at the second position (positions start from zero) +Node::find(1)->getChildAt(1)->id; // 3 -//in both directions -$hasSiblings = $page->hasSiblings(); -$siblingsNumber = $page->countSiblings(); +Node::find(1)->getChildrenRange(1)->pluck('id')->toArray(); // [3, 4, 5] +Node::find(1)->getChildrenRange(0, 2)->pluck('id')->toArray(); // [2, 3, 4] -$sibligns = $page->getSiblingsRange(0, 2); +Node::find(1)->getFirstChild()->id; // 2 +Node::find(1)->getLastChild()->id; // 5 -$page->addSibling(new Page); -$page->addSibling(new Page, 3); //third position +Node::find(6)->countChildren(); // 0 +Node::find(6)->hasChildren(); // false -//add and get the sibling -$sibling = $page->addSibling(new Page, null, true); +Node::find(6)->addChild(new Node(['id' => 7])); -$page->addSiblings([new Page, new Page]); -$page->addSiblings([new Page, new Page], 5); //insert from fifth position -``` +Node::find(1)->addChildren([new Node(['id' => 8]), new Node(['id' => 9])], 2); +Node::find(1)->getChildren()->pluck('position', 'id')->toArray(); // [2 => 0, 3 => 1, 8 => 2, 9 => 3, 4 => 4, 5 => 5] -### Roots (entities that have no ancestors) +// remove child by its position +Node::find(1)->removeChild(2); +Node::find(1)->getChildren()->pluck('position', 'id')->toArray(); // [2 => 0, 3 => 1, 9 => 2, 4 => 3, 5 => 4] -```php -$roots = Page::getRoots(); -$isRoot = Page::find(23)->isRoot(); -Page::find(11)->makeRoot(0); //at the moment we always have to set a position when making node a root +Node::find(1)->removeChildren(2, 4); +Node::find(1)->getChildren()->pluck('position', 'id')->toArray(); // [2 => 0, 3 => 1] ``` -### Entire tree - +### Siblings ```php -$tree = Page::getTree(); -$treeByCondition = Page::getTreeWhere('position', '>=', 1); -``` + 1]), + new Node(['id' => 2, 'parent_id' => 1]), + new Node(['id' => 3, 'parent_id' => 1]), + new Node(['id' => 4, 'parent_id' => 1]), + new Node(['id' => 5, 'parent_id' => 1]), + new Node(['id' => 6, 'parent_id' => 1]), + new Node(['id' => 7, 'parent_id' => 1]) +]; + +foreach ($nodes as $node) { + $node->save(); +} -You deal with the collection, thus you can control its items as you usually do. Descendants? They are already loaded. +Node::find(7)->getFirstSibling()->id; // 2 +Node::find(7)->getSiblingAt(0); // 2 +Node::find(2)->getLastSibling(); // 7 +Node::find(7)->getPrevSibling()->id; // 6 +Node::find(7)->getPrevSiblings()->pluck('id')->toArray(); // [2, 3, 4, 5, 6] +Node::find(7)->countPrevSiblings(); // 5 +Node::find(7)->hasPrevSiblings(); // true + +Node::find(2)->getNextSibling()->id; // 3 +Node::find(2)->getNextSiblings()->pluck('id')->toArray(); // [3, 4, 5, 6, 7] +Node::find(2)->countNextSiblings(); // 5 +Node::find(2)->hasNextSiblings(); // true + +Node::find(3)->getSiblings()->pluck('id')->toArray(); // [2, 4, 5, 6, 7] +Node::find(3)->getNeighbors()->pluck('id')->toArray(); // [2, 4] +Node::find(3)->countSiblings(); // 5 +Node::find(3)->hasSiblings(); // true + +Node::find(2)->getSiblingsRange(2)->pluck('id')->toArray(); // [4, 5, 6, 7] +Node::find(2)->getSiblingsRange(2, 4)->pluck('id')->toArray(); // [4, 5, 6] + +Node::find(4)->addSibling(new Node(['id' => 8])); +Node::find(4)->getNextSiblings()->pluck('id')->toArray(); // [5, 6, 7, 8] + +Node::find(4)->addSibling(new Node(['id' => 9]), 1); +Node::find(1)->getChildren()->pluck('position', 'id')->toArray(); +// [2 => 0, 9 => 1, 3 => 2, 4 => 3, 5 => 4, 6 => 5, 7 => 6, 8 => 7] + +Node::find(8)->addSiblings([new Node(['id' => 10]), new Node(['id' => 11])]); +Node::find(1)->getChildren()->pluck('position', 'id')->toArray(); +// [2 => 0, 9 => 1, 3 => 2, 4 => 3, 5 => 4, 6 => 5, 7 => 6, 8 => 7, 10 => 8, 11 => 9] + +Node::find(2)->addSiblings([new Node(['id' => 12]), new Node(['id' => 13])], 3); +Node::find(1)->getChildren()->pluck('position', 'id')->toArray(); +// [2 => 0, 9 => 1, 3 => 2, 12 => 3, 13 => 4, 4 => 5, 5 => 6, 6 => 7, 7 => 8, 8 => 9, 10 => 10, 11 => 11] +``` +### Tree ```php -$tree = Page::getTree(); -$page = $tree->find(15); -$children = $page->getChildren(); -$child = $page->getChildAt(3); -$grandchildren = $page->getChildAt(3)->getChildren(); //and so on + 1, + 'children' => [ + [ + 'id' => 2, + 'children' => [ + [ + 'id' => 3, + 'children' => [ + [ + 'id' => 4, + 'children' => [ + [ + 'id' => 5, + 'children' => [ + [ + 'id' => 6, + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] +]); + +Node::find(4)->deleteSubtree(); +Node::find(1)->getDescendants()->pluck('id')->toArray(); // [2, 3, 4] + +Node::find(4)->deleteSubtree(true); +Node::find(1)->getDescendants()->pluck('id')->toArray(); // [2, 3] ``` -### Moving +There are several methods that have been deprecated since ClosureTable 6: -```php -$page = Page::find(25); -$page->moveTo(0, Page::find(14)); -$page->moveTo(0, 14); +```diff +-Node::getTree(); +-Node::getTreeByQuery(...); +-Node::getTreeWhere('foo', '=', 'bar'); ++Node::where('foo', '=', 'bar')->get()->toTree(); ``` -### Deleting subtree -If you don't use foreign keys for some reason, you can delete subtree manually. This will delete the page and all its descendants: +### Collection methods +This library uses an extended collection class which offers some convenient methods: ```php -$page = Page::find(34); -$page->deleteSubtree(); -$page->deleteSubtree(true); //with subtree ancestor -$page->deleteSubtree(false, true); //without subtree ancestor and force delete -``` - -## Customization -You can customize default things in your own classes created by the ClosureTable `artisan` command:
-1. **Entity table name**: change `protected $table` property
-2. **Closure table name**: do the same in your `ClosureTable` (e.g. `PageClosure`)
-3. **Entity's `parent_id`, `position`, and `real depth` column names**: change return values of `getParentIdColumn()`, `getPositionColumn()`, and `getRealDepthColumn()` respectively
-4. **Closure table's `ancestor`, `descendant`, and `depth` columns names**: change return values of `getAncestorColumn()`, `getDescendantColumn()`, and `getDepthColumn()` respectively. + 1, + 'children' => [ + ['id' => 2], + ['id' => 3], + ['id' => 4], + ['id' => 5], + [ + 'id' => 6, + 'children' => [ + ['id' => 7], + ['id' => 8], + ] + ], + ] +]); + +/** @var Franzose\ClosureTable\Extensions\Collection $children */ +$children = Node::find(1)->getChildren(); +$children->getChildAt(1)->id; // 3 +$children->getFirstChild()->id; // 2 +$children->getLastChild()->id; // 6 +$children->getRange(1)->pluck('id')->toArray(); // [3, 4, 5, 6] +$children->getRange(1, 3)->pluck('id')->toArray(); // [3, 4, 5] +$children->getNeighbors(2)->pluck('id')->toArray(); // [3, 5] +$children->getPrevSiblings(2)->pluck('id')->toArray(); // [2, 3] +$children->getNextSiblings(2)->pluck('id')->toArray(); // [5, 6] +$children->getChildrenOf(4)->pluck('id')->toArray(); // [7, 8] +$children->hasChildren(4); // true +$tree = $children->toTree(); +``` \ No newline at end of file diff --git a/composer.json b/composer.json index ddeb692..acf31d1 100644 --- a/composer.json +++ b/composer.json @@ -12,26 +12,26 @@ } ], "require": { - "php": ">=5.4.0" + "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "dev-master", - "way/phpunit-wrappers": "dev-master", - "way/laravel-test-helpers": "dev-master", - "mockery/mockery": "dev-master", - "orchestra/testbench": "dev-master", - "doctrine/dbal": "dev-master" + "phpunit/phpunit": "^6.0|^7.0|^8.0", + "orchestra/testbench": "^3.4|^4.0|^5.0" }, "autoload": { - "classmap": ["tests"], - "psr-0": { - "Franzose\\ClosureTable": "src/" + "psr-4": { + "Franzose\\ClosureTable\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Franzose\\ClosureTable\\Tests\\": "tests/" } }, "config": { "preferred-install": "dist" }, - "minimum-stability": "dev", + "minimum-stability": "stable", "extra": { "laravel": { "providers": [ diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..36e9eb9 --- /dev/null +++ b/composer.lock @@ -0,0 +1,4065 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "c19f4e5954df4ae09a9cd29f1b90bc95", + "packages": [], + "packages-dev": [ + { + "name": "doctrine/inflector", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "ec3a55242203ffa6a4b27c58176da97ff0a7aec1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/ec3a55242203ffa6a4b27c58176da97ff0a7aec1", + "reference": "ec3a55242203ffa6a4b27c58176da97ff0a7aec1", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Common String Manipulations with regard to casing and singular/plural rules.", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "inflection", + "pluralize", + "singularize", + "string" + ], + "time": "2019-10-30T19:59:35+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", + "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2019-10-21T16:45:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", + "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", + "shasum": "" + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan": "^0.11.8", + "phpunit/phpunit": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "time": "2019-10-30T14:39:59+00:00" + }, + { + "name": "egulias/email-validator", + "version": "2.1.17", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "ade6887fd9bd74177769645ab5c474824f8a418a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ade6887fd9bd74177769645ab5c474824f8a418a", + "reference": "ade6887fd9bd74177769645ab5c474824f8a418a", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.0.1", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.10" + }, + "require-dev": { + "dominicsayers/isemail": "^3.0.7", + "phpunit/phpunit": "^4.8.36|^7.5.15", + "satooshi/php-coveralls": "^1.0.1" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "EmailValidator" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "time": "2020-02-13T22:36:52+00:00" + }, + { + "name": "erusev/parsedown", + "version": "1.7.4", + "source": { + "type": "git", + "url": "https://github.com/erusev/parsedown.git", + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" + }, + "type": "library", + "autoload": { + "psr-0": { + "Parsedown": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "description": "Parser for Markdown.", + "homepage": "http://parsedown.org", + "keywords": [ + "markdown", + "parser" + ], + "time": "2019-12-30T22:54:17+00:00" + }, + { + "name": "fzaninotto/faker", + "version": "v1.9.1", + "source": { + "type": "git", + "url": "https://github.com/fzaninotto/Faker.git", + "reference": "fc10d778e4b84d5bd315dad194661e091d307c6f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/fc10d778e4b84d5bd315dad194661e091d307c6f", + "reference": "fc10d778e4b84d5bd315dad194661e091d307c6f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "ext-intl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7", + "squizlabs/php_codesniffer": "^2.9.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "time": "2019-12-12T13:22:17+00:00" + }, + { + "name": "kylekatarnls/update-helper", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/kylekatarnls/update-helper.git", + "reference": "429be50660ed8a196e0798e5939760f168ec8ce9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kylekatarnls/update-helper/zipball/429be50660ed8a196e0798e5939760f168ec8ce9", + "reference": "429be50660ed8a196e0798e5939760f168ec8ce9", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1.0 || ^2.0.0", + "php": ">=5.3.0" + }, + "require-dev": { + "codeclimate/php-test-reporter": "dev-master", + "composer/composer": "2.0.x-dev || ^2.0.0-dev", + "phpunit/phpunit": ">=4.8.35 <6.0" + }, + "type": "composer-plugin", + "extra": { + "class": "UpdateHelper\\ComposerPlugin" + }, + "autoload": { + "psr-0": { + "UpdateHelper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kyle", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Update helper", + "time": "2020-04-07T20:44:10+00:00" + }, + { + "name": "laravel/framework", + "version": "v5.5.48", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "e3e8d585dcfab5abe6261b060f4df0d48f9924bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/e3e8d585dcfab5abe6261b060f4df0d48f9924bf", + "reference": "e3e8d585dcfab5abe6261b060f4df0d48f9924bf", + "shasum": "" + }, + "require": { + "doctrine/inflector": "~1.1", + "erusev/parsedown": "~1.7", + "ext-mbstring": "*", + "ext-openssl": "*", + "league/flysystem": "^1.0.8", + "monolog/monolog": "~1.12", + "mtdowling/cron-expression": "~1.0", + "nesbot/carbon": "^1.26.0", + "php": ">=7.0", + "psr/container": "~1.0", + "psr/simple-cache": "^1.0", + "ramsey/uuid": "~3.0", + "swiftmailer/swiftmailer": "~6.0", + "symfony/console": "~3.3", + "symfony/debug": "~3.3", + "symfony/finder": "~3.3", + "symfony/http-foundation": "~3.3", + "symfony/http-kernel": "~3.3", + "symfony/process": "~3.3", + "symfony/routing": "~3.3", + "symfony/var-dumper": "~3.3", + "tijsverkoyen/css-to-inline-styles": "~2.2", + "vlucas/phpdotenv": "~2.2" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/log": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "tightenco/collect": "<5.5.33" + }, + "require-dev": { + "aws/aws-sdk-php": "~3.0", + "doctrine/dbal": "~2.5", + "filp/whoops": "^2.1.4", + "mockery/mockery": "~1.0", + "orchestra/testbench-core": "3.5.*", + "pda/pheanstalk": "~3.0", + "phpunit/phpunit": "~6.0", + "predis/predis": "^1.1.1", + "symfony/css-selector": "~3.3", + "symfony/dom-crawler": "~3.3" + }, + "suggest": { + "aws/aws-sdk-php": "Required to use the SQS queue driver and SES mail driver (~3.0).", + "doctrine/dbal": "Required to rename columns and drop SQLite columns (~2.5).", + "ext-pcntl": "Required to use all features of the queue worker.", + "ext-posix": "Required to use all features of the queue worker.", + "fzaninotto/faker": "Required to use the eloquent factory builder (~1.4).", + "guzzlehttp/guzzle": "Required to use the Mailgun and Mandrill mail drivers and the ping methods on schedules (~6.0).", + "laravel/tinker": "Required to use the tinker console command (~1.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (~1.0).", + "league/flysystem-cached-adapter": "Required to use Flysystem caching (~1.0).", + "league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (~1.0).", + "nexmo/client": "Required to use the Nexmo transport (~1.0).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (~3.0).", + "predis/predis": "Required to use the redis cache and queue drivers (~1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (~3.0).", + "symfony/css-selector": "Required to use some of the crawler integration testing tools (~3.3).", + "symfony/dom-crawler": "Required to use most of the crawler integration testing tools (~3.3).", + "symfony/psr-http-message-bridge": "Required to psr7 bridging features (~1.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.5-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "time": "2019-08-20T15:46:40+00:00" + }, + { + "name": "league/flysystem", + "version": "1.0.66", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "021569195e15f8209b1c4bebb78bd66aa4f08c21" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/021569195e15f8209b1c4bebb78bd66aa4f08c21", + "reference": "021569195e15f8209b1c4bebb78bd66aa4f08c21", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": ">=5.5.9" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, + "require-dev": { + "phpspec/phpspec": "^3.4", + "phpunit/phpunit": "^5.7.26" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "ext-ftp": "Allows you to use FTP server storage", + "ext-openssl": "Allows you to use FTPS server storage", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ], + "time": "2020-03-17T18:58:12+00:00" + }, + { + "name": "monolog/monolog", + "version": "1.25.3", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "fa82921994db851a8becaf3787a9e73c5976b6f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fa82921994db851a8becaf3787a9e73c5976b6f1", + "reference": "fa82921994db851a8becaf3787a9e73c5976b6f1", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "jakub-onderka/php-parallel-lint": "0.9", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpunit/phpunit": "~4.5", + "phpunit/phpunit-mock-objects": "2.3.0", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "time": "2019-12-20T14:15:16+00:00" + }, + { + "name": "mtdowling/cron-expression", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/mtdowling/cron-expression.git", + "reference": "9be552eebcc1ceec9776378f7dcc085246cacca6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mtdowling/cron-expression/zipball/9be552eebcc1ceec9776378f7dcc085246cacca6", + "reference": "9be552eebcc1ceec9776378f7dcc085246cacca6", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.0|~5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "abandoned": "dragonmantank/cron-expression", + "time": "2019-12-28T04:23:06+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/b2c28789e80a97badd14145fda39b545d83ca3ef", + "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2020-01-17T21:11:47+00:00" + }, + { + "name": "nesbot/carbon", + "version": "1.39.1", + "source": { + "type": "git", + "url": "https://github.com/briannesbitt/Carbon.git", + "reference": "4be0c005164249208ce1b5ca633cd57bdd42ff33" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4be0c005164249208ce1b5ca633cd57bdd42ff33", + "reference": "4be0c005164249208ce1b5ca633cd57bdd42ff33", + "shasum": "" + }, + "require": { + "kylekatarnls/update-helper": "^1.1", + "php": ">=5.3.9", + "symfony/translation": "~2.6 || ~3.0 || ~4.0" + }, + "require-dev": { + "composer/composer": "^1.2", + "friendsofphp/php-cs-fixer": "~2", + "phpunit/phpunit": "^4.8.35 || ^5.7" + }, + "bin": [ + "bin/upgrade-carbon" + ], + "type": "library", + "extra": { + "update-helper": "Carbon\\Upgrade", + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "http://nesbot.com" + } + ], + "description": "A simple API extension for DateTime.", + "homepage": "http://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "time": "2019-10-14T05:51:36+00:00" + }, + { + "name": "orchestra/testbench", + "version": "v3.5.5", + "source": { + "type": "git", + "url": "https://github.com/orchestral/testbench.git", + "reference": "fd032489df469d611a264083e62db96677c9061e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/testbench/zipball/fd032489df469d611a264083e62db96677c9061e", + "reference": "fd032489df469d611a264083e62db96677c9061e", + "shasum": "" + }, + "require": { + "laravel/framework": "~5.5.34", + "orchestra/testbench-core": "~3.5.9", + "php": ">=7.0", + "phpunit/phpunit": "~6.0" + }, + "require-dev": { + "mockery/mockery": "~1.0", + "orchestra/database": "~3.5.0" + }, + "suggest": { + "orchestra/testbench-browser-kit": "Allow to use legacy BrowserKit for testing (~3.5)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.5-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com", + "homepage": "https://github.com/crynobone" + } + ], + "description": "Laravel Testing Helper for Packages Development", + "homepage": "http://orchestraplatform.com/docs/latest/components/testbench/", + "keywords": [ + "BDD", + "TDD", + "laravel", + "orchestra-platform", + "orchestral", + "testing" + ], + "time": "2018-02-20T05:30:39+00:00" + }, + { + "name": "orchestra/testbench-core", + "version": "v3.5.11", + "source": { + "type": "git", + "url": "https://github.com/orchestral/testbench-core.git", + "reference": "a9c3625a5234ea478546fc0711216d6707ca2509" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/a9c3625a5234ea478546fc0711216d6707ca2509", + "reference": "a9c3625a5234ea478546fc0711216d6707ca2509", + "shasum": "" + }, + "require": { + "fzaninotto/faker": "~1.4", + "php": ">=7.0" + }, + "require-dev": { + "laravel/framework": "~5.5.0", + "mockery/mockery": "~1.0", + "orchestra/database": "~3.5.0", + "phpunit/phpunit": "~6.0" + }, + "suggest": { + "laravel/framework": "Required for testing (~5.5.0).", + "mockery/mockery": "Allow to use Mockery for testing (~1.0).", + "orchestra/database": "Allow to use --realpath migration for testing (~3.5).", + "orchestra/testbench-browser-kit": "Allow to use legacy BrowserKit for testing (~3.5).", + "orchestra/testbench-dusk": "Allow to use Laravel Dusk for testing (~3.5).", + "phpunit/phpunit": "Allow to use PHPUnit for testing (~6.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Orchestra\\Testbench\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com", + "homepage": "https://github.com/crynobone" + } + ], + "description": "Testing Helper for Laravel Development", + "homepage": "http://orchestraplatform.com/docs/latest/components/testbench/", + "keywords": [ + "BDD", + "TDD", + "laravel", + "orchestra-platform", + "orchestral", + "testing" + ], + "time": "2019-12-10T01:08:48+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.99", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "shasum": "" + }, + "require": { + "php": "^7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "time": "2018-07-02T15:55:56+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^1.0.1", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2017-03-05T18:14:27+00:00" + }, + { + "name": "phar-io/version", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2017-03-05T17:38:23+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", + "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2018-08-07T13:53:10+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", + "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", + "shasum": "" + }, + "require": { + "ext-filter": "^7.1", + "php": "^7.2", + "phpdocumentor/reflection-common": "^2.0", + "phpdocumentor/type-resolver": "^1.0", + "webmozart/assert": "^1" + }, + "require-dev": { + "doctrine/instantiator": "^1", + "mockery/mockery": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2020-02-22T12:28:44+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "7462d5f123dfc080dfdf26897032a6513644fc95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/7462d5f123dfc080dfdf26897032a6513644fc95", + "reference": "7462d5f123dfc080dfdf26897032a6513644fc95", + "shasum": "" + }, + "require": { + "php": "^7.2", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "^7.2", + "mockery/mockery": "~1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2020-02-18T18:59:58+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.10.3", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "451c3cd1418cf640de218914901e51b064abb093" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", + "reference": "451c3cd1418cf640de218914901e51b064abb093", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", + "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5 || ^3.2", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2020-03-05T15:02:03+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "5.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "c89677919c5dd6d3b3852f230a663118762218ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac", + "reference": "c89677919c5dd6d3b3852f230a663118762218ac", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.0", + "phpunit/php-file-iterator": "^1.4.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^2.0.1", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-xdebug": "^2.5.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2018-04-06T15:36:58+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2017-11-27T13:52:08+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2017-02-26T11:10:40+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "791198a2c6254db10131eecfe8c06670700904db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", + "reference": "791198a2c6254db10131eecfe8c06670700904db", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2017-11-27T05:48:46+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "6.5.14", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bac23fe7ff13dbdb461481f706f0e9fe746334b7", + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "^1.6.1", + "phar-io/manifest": "^1.0.1", + "phar-io/version": "^1.0", + "php": "^7.0", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^5.3", + "phpunit/php-file-iterator": "^1.4.3", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^1.0.9", + "phpunit/phpunit-mock-objects": "^5.0.9", + "sebastian/comparator": "^2.1", + "sebastian/diff": "^2.0", + "sebastian/environment": "^3.1", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^1.0", + "sebastian/version": "^2.0.1" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2", + "phpunit/dbunit": "<3.0" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-xdebug": "*", + "phpunit/php-invoker": "^1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2019-02-01T05:22:47+00:00" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "5.0.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f", + "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.5", + "php": "^7.0", + "phpunit/php-text-template": "^1.2.1", + "sebastian/exporter": "^3.1" + }, + "conflict": { + "phpunit/phpunit": "<6.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5.11" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "abandoned": true, + "time": "2018-08-09T05:50:03+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "psr/log", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2020-03-23T09:12:05+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "ramsey/uuid", + "version": "3.9.3", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "7e1633a6964b48589b142d60542f9ed31bd37a92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/7e1633a6964b48589b142d60542f9ed31bd37a92", + "reference": "7e1633a6964b48589b142d60542f9ed31bd37a92", + "shasum": "" + }, + "require": { + "ext-json": "*", + "paragonie/random_compat": "^1 | ^2 | 9.99.99", + "php": "^5.4 | ^7 | ^8", + "symfony/polyfill-ctype": "^1.8" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "codeception/aspect-mock": "^1 | ^2", + "doctrine/annotations": "^1.2", + "goaop/framework": "1.0.0-alpha.2 | ^1 | ^2.1", + "jakub-onderka/php-parallel-lint": "^1", + "mockery/mockery": "^0.9.11 | ^1", + "moontoast/math": "^1.1", + "paragonie/random-lib": "^2", + "php-mock/php-mock-phpunit": "^0.3 | ^1.1", + "phpunit/phpunit": "^4.8 | ^5.4 | ^6.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "ext-ctype": "Provides support for PHP Ctype functions", + "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", + "ext-openssl": "Provides the OpenSSL extension for use with the OpenSslGenerator", + "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", + "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Uuid\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + }, + { + "name": "Marijn Huizendveld", + "email": "marijn.huizendveld@gmail.com" + }, + { + "name": "Thibaud Fabre", + "email": "thibaud@aztech.io" + } + ], + "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", + "homepage": "https://github.com/ramsey/uuid", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "time": "2020-02-21T04:36:14+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2017-03-04T06:30:41+00:00" + }, + { + "name": "sebastian/comparator", + "version": "2.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/diff": "^2.0 || ^3.0", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2018-02-01T13:46:46+00:00" + }, + { + "name": "sebastian/diff", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2017-08-03T08:09:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2017-07-01T08:51:00+00:00" + }, + { + "name": "sebastian/exporter", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2019-09-14T09:02:43+00:00" + }, + { + "name": "sebastian/global-state", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2017-04-27T15:39:26+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2017-08-03T12:35:26+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "773f97c67f28de00d397be301821b06708fca0be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", + "reference": "773f97c67f28de00d397be301821b06708fca0be", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "time": "2017-03-29T09:07:27+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2017-03-03T06:23:57+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28T20:34:47+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "swiftmailer/swiftmailer", + "version": "v6.2.3", + "source": { + "type": "git", + "url": "https://github.com/swiftmailer/swiftmailer.git", + "reference": "149cfdf118b169f7840bbe3ef0d4bc795d1780c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/149cfdf118b169f7840bbe3ef0d4bc795d1780c9", + "reference": "149cfdf118b169f7840bbe3ef0d4bc795d1780c9", + "shasum": "" + }, + "require": { + "egulias/email-validator": "~2.0", + "php": ">=7.0.0", + "symfony/polyfill-iconv": "^1.0", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "require-dev": { + "mockery/mockery": "~0.9.1", + "symfony/phpunit-bridge": "^3.4.19|^4.1.8" + }, + "suggest": { + "ext-intl": "Needed to support internationalized email addresses", + "true/punycode": "Needed to support internationalized email addresses, if ext-intl is not installed" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "lib/swift_required.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Corbyn" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Swiftmailer, free feature-rich PHP mailer", + "homepage": "https://swiftmailer.symfony.com", + "keywords": [ + "email", + "mail", + "mailer" + ], + "time": "2019-11-12T09:31:26+00:00" + }, + { + "name": "symfony/console", + "version": "v3.4.39", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "bf60d5e606cd595391c5f82bf6b570d9573fa120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/bf60d5e606cd595391c5f82bf6b570d9573fa120", + "reference": "bf60d5e606cd595391c5f82bf6b570d9573fa120", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0|~4.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.3|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.3|~4.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2020-03-27T17:07:22+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v5.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "5f8d5271303dad260692ba73dfa21777d38e124e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/5f8d5271303dad260692ba73dfa21777d38e124e", + "reference": "5f8d5271303dad260692ba73dfa21777d38e124e", + "shasum": "" + }, + "require": { + "php": "^7.2.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony CssSelector Component", + "homepage": "https://symfony.com", + "time": "2020-03-27T16:56:45+00:00" + }, + { + "name": "symfony/debug", + "version": "v3.4.39", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "ce9f3b5e8e1c50f849fded59b3a1b6bc3562ec29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/ce9f3b5e8e1c50f849fded59b3a1b6bc3562ec29", + "reference": "ce9f3b5e8e1c50f849fded59b3a1b6bc3562ec29", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/http-kernel": "~2.8|~3.0|~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2020-03-23T10:22:40+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v4.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "abc8e3618bfdb55e44c8c6a00abd333f831bbfed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/abc8e3618bfdb55e44c8c6a00abd333f831bbfed", + "reference": "abc8e3618bfdb55e44c8c6a00abd333f831bbfed", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/event-dispatcher-contracts": "^1.1" + }, + "conflict": { + "symfony/dependency-injection": "<3.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "1.1" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/service-contracts": "^1.1|^2", + "symfony/stopwatch": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2020-03-27T16:54:36+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v1.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "c43ab685673fb6c8d84220c77897b1d6cdbe1d18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c43ab685673fb6c8d84220c77897b1d6cdbe1d18", + "reference": "c43ab685673fb6c8d84220c77897b1d6cdbe1d18", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "suggest": { + "psr/event-dispatcher": "", + "symfony/event-dispatcher-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2019-09-17T09:54:03+00:00" + }, + { + "name": "symfony/finder", + "version": "v3.4.39", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "5ec813ccafa8164ef21757e8c725d3a57da59200" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/5ec813ccafa8164ef21757e8c725d3a57da59200", + "reference": "5ec813ccafa8164ef21757e8c725d3a57da59200", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2020-02-14T07:34:21+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v3.4.39", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "a8833c56f6a4abcf17a319d830d71fdb0ba93675" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a8833c56f6a4abcf17a319d830d71fdb0ba93675", + "reference": "a8833c56f6a4abcf17a319d830d71fdb0ba93675", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php70": "~1.6" + }, + "require-dev": { + "symfony/expression-language": "~2.8|~3.0|~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpFoundation Component", + "homepage": "https://symfony.com", + "time": "2020-03-23T12:14:52+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v3.4.39", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "c15b5acab571224b1bf792692ff2ad63239081fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/c15b5acab571224b1bf792692ff2ad63239081fe", + "reference": "c15b5acab571224b1bf792692ff2ad63239081fe", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/log": "~1.0", + "symfony/debug": "^3.3.3|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/http-foundation": "~3.4.12|~4.0.12|^4.1.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php56": "~1.8" + }, + "conflict": { + "symfony/config": "<2.8", + "symfony/dependency-injection": "<3.4.10|<4.0.10,>=4", + "symfony/var-dumper": "<3.3", + "twig/twig": "<1.34|<2.4,>=2" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/cache": "~1.0", + "symfony/browser-kit": "~2.8|~3.0|~4.0", + "symfony/class-loader": "~2.8|~3.0", + "symfony/config": "~2.8|~3.0|~4.0", + "symfony/console": "~2.8|~3.0|~4.0", + "symfony/css-selector": "~2.8|~3.0|~4.0", + "symfony/dependency-injection": "^3.4.10|^4.0.10", + "symfony/dom-crawler": "~2.8|~3.0|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/finder": "~2.8|~3.0|~4.0", + "symfony/process": "~2.8|~3.0|~4.0", + "symfony/routing": "~3.4|~4.0", + "symfony/stopwatch": "~2.8|~3.0|~4.0", + "symfony/templating": "~2.8|~3.0|~4.0", + "symfony/translation": "~2.8|~3.0|~4.0", + "symfony/var-dumper": "~3.3|~4.0" + }, + "suggest": { + "symfony/browser-kit": "", + "symfony/config": "", + "symfony/console": "", + "symfony/dependency-injection": "", + "symfony/finder": "", + "symfony/var-dumper": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpKernel Component", + "homepage": "https://symfony.com", + "time": "2020-03-30T06:25:13+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/4719fa9c18b0464d399f1a63bf624b42b6fa8d14", + "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.15-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2020-02-27T09:26:54+00:00" + }, + { + "name": "symfony/polyfill-iconv", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-iconv.git", + "reference": "ad6d62792bfbcfc385dd34b424d4fcf9712a32c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/ad6d62792bfbcfc385dd34b424d4fcf9712a32c8", + "reference": "ad6d62792bfbcfc385dd34b424d4fcf9712a32c8", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-iconv": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.15-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Iconv\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Iconv extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "iconv", + "polyfill", + "portable", + "shim" + ], + "time": "2020-03-09T19:04:49+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf", + "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.15-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "time": "2020-03-09T19:04:49+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac", + "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.15-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2020-03-09T19:04:49+00:00" + }, + { + "name": "symfony/polyfill-php56", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php56.git", + "reference": "d51ec491c8ddceae7dca8dd6c7e30428f543f37d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/d51ec491c8ddceae7dca8dd6c7e30428f543f37d", + "reference": "d51ec491c8ddceae7dca8dd6c7e30428f543f37d", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/polyfill-util": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.15-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php56\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2020-03-09T19:04:49+00:00" + }, + { + "name": "symfony/polyfill-php70", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "2a18e37a489803559284416df58c71ccebe50bf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/2a18e37a489803559284416df58c71ccebe50bf0", + "reference": "2a18e37a489803559284416df58c71ccebe50bf0", + "shasum": "" + }, + "require": { + "paragonie/random_compat": "~1.0|~2.0|~9.99", + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.15-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php70\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2020-02-27T09:26:54+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "37b0976c78b94856543260ce09b460a7bc852747" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/37b0976c78b94856543260ce09b460a7bc852747", + "reference": "37b0976c78b94856543260ce09b460a7bc852747", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.15-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2020-02-27T09:26:54+00:00" + }, + { + "name": "symfony/polyfill-util", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-util.git", + "reference": "d8e76c104127675d0ea3df3be0f2ae24a8619027" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/d8e76c104127675d0ea3df3be0f2ae24a8619027", + "reference": "d8e76c104127675d0ea3df3be0f2ae24a8619027", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.15-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Util\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony utilities for portability of PHP codes", + "homepage": "https://symfony.com", + "keywords": [ + "compat", + "compatibility", + "polyfill", + "shim" + ], + "time": "2020-03-02T11:55:35+00:00" + }, + { + "name": "symfony/process", + "version": "v3.4.39", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "1dbc09f6e14703ae2398efc86b02ae2bcd9a9931" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/1dbc09f6e14703ae2398efc86b02ae2bcd9a9931", + "reference": "1dbc09f6e14703ae2398efc86b02ae2bcd9a9931", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2020-03-20T06:07:50+00:00" + }, + { + "name": "symfony/routing", + "version": "v3.4.39", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "785e4e6b835e9ab4f9412862855d0e1b7a2b4627" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/785e4e6b835e9ab4f9412862855d0e1b7a2b4627", + "reference": "785e4e6b835e9ab4f9412862855d0e1b7a2b4627", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "conflict": { + "symfony/config": "<3.3.1", + "symfony/dependency-injection": "<3.3", + "symfony/yaml": "<3.4" + }, + "require-dev": { + "doctrine/annotations": "~1.0", + "psr/log": "~1.0", + "symfony/config": "^3.3.1|~4.0", + "symfony/dependency-injection": "~3.3|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/http-foundation": "~2.8|~3.0|~4.0", + "symfony/yaml": "~3.4|~4.0" + }, + "suggest": { + "doctrine/annotations": "For using the annotation loader", + "symfony/config": "For using the all-in-one router or any loader", + "symfony/expression-language": "For using expression matching", + "symfony/http-foundation": "For using a Symfony Request object", + "symfony/yaml": "For using the YAML loader" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Routing Component", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "time": "2020-03-25T12:02:26+00:00" + }, + { + "name": "symfony/translation", + "version": "v4.3.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "46e462be71935ae15eab531e4d491d801857f24c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/46e462be71935ae15eab531e4d491d801857f24c", + "reference": "46e462be71935ae15eab531e4d491d801857f24c", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^1.1.6" + }, + "conflict": { + "symfony/config": "<3.4", + "symfony/dependency-injection": "<3.4", + "symfony/yaml": "<3.4" + }, + "provide": { + "symfony/translation-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.4|~4.0", + "symfony/console": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/finder": "~2.8|~3.0|~4.0", + "symfony/http-kernel": "~3.4|~4.0", + "symfony/intl": "~3.4|~4.0", + "symfony/service-contracts": "^1.1.2", + "symfony/var-dumper": "~3.4|~4.0", + "symfony/yaml": "~3.4|~4.0" + }, + "suggest": { + "psr/log-implementation": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Translation Component", + "homepage": "https://symfony.com", + "time": "2020-01-04T12:24:57+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v1.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "364518c132c95642e530d9b2d217acbc2ccac3e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/364518c132c95642e530d9b2d217acbc2ccac3e6", + "reference": "364518c132c95642e530d9b2d217acbc2ccac3e6", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "suggest": { + "symfony/translation-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2019-09-17T11:12:18+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v3.4.39", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "13c03169ae485fc7f1a5143256622ce1bd3c77eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/13c03169ae485fc7f1a5143256622ce1bd3c77eb", + "reference": "13c03169ae485fc7f1a5143256622ce1bd3c77eb", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" + }, + "require-dev": { + "ext-iconv": "*", + "twig/twig": "~1.34|~2.4" + }, + "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", + "ext-intl": "To show region name in time zone dump", + "ext-symfony_debug": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony mechanism for exploring and dumping PHP variables", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "time": "2020-03-17T22:27:36+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "time": "2019-06-13T22:48:21+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "2.2.2", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "dda2ee426acd6d801d5b7fd1001cde9b5f790e15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/dda2ee426acd6d801d5b7fd1001cde9b5f790e15", + "reference": "dda2ee426acd6d801d5b7fd1001cde9b5f790e15", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^5.5 || ^7.0", + "symfony/css-selector": "^2.7 || ^3.0 || ^4.0 || ^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "time": "2019-10-24T08:53:34+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v2.6.2", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "c4a653ed3f1ff900baa15b4130c8770b57285b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/c4a653ed3f1ff900baa15b4130c8770b57285b62", + "reference": "c4a653ed3f1ff900baa15b4130c8770b57285b62", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/polyfill-ctype": "^1.9" + }, + "require-dev": { + "ext-filter": "*", + "ext-pcre": "*", + "phpunit/phpunit": "^4.8.35 || ^5.0" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator.", + "ext-pcre": "Required to use most of the library." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "http://www.vancelucas.com" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "time": "2020-03-27T23:16:19+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "aed98a490f9a8f78468232db345ab9cf606cf598" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/aed98a490f9a8f78468232db345ab9cf606cf598", + "reference": "aed98a490f9a8f78468232db345ab9cf606cf598", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "vimeo/psalm": "<3.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2020-02-14T12:15:55+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.6.0" + }, + "platform-dev": [] +} diff --git a/docker-compose.yaml.example b/docker-compose.yaml.example new file mode 100644 index 0000000..81f84d1 --- /dev/null +++ b/docker-compose.yaml.example @@ -0,0 +1,30 @@ +version: '3' +services: + mysql: + image: mariadb:10.3 + volumes: + - ./docker/mysql:/var/lib/mysql + ports: + - 5506:3306 + environment: + MYSQL_DATABASE: closuretabletest + MYSQL_USER: user + MYSQL_PASSWORD: userpass + MYSQL_ROOT_PASSWORD: root + + postgresql: + image: postgres:latest + volumes: + - ./docker/postgresql:/var/lib/postgresql/data + ports: + - 5507:5432 + environment: + POSTGRES_DB: closuretabletest + POSTGRES_USER: user + POSTGRES_PASSWORD: userpass + php: + image: php:5.6-cli + tty: true + command: /bin/sh + volumes: + - ./:/usr/src/myapp diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 92d3498..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,177 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ClosureTable.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ClosureTable.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/ClosureTable" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ClosureTable" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 15c0ee0..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,258 +0,0 @@ -# -*- coding: utf-8 -*- -# -# ClosureTable documentation build configuration file, created by -# sphinx-quickstart on Sun Apr 06 11:52:08 2014. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'ClosureTable' -copyright = u'2014, Jan Iwanow' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '3.1.0' -# The full version, including alpha/beta/rc tags. -release = '3.1.0' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'ClosureTabledoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ('index', 'ClosureTable.tex', u'ClosureTable Documentation', - u'Jan Iwanow', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'closuretable', u'ClosureTable Documentation', - [u'Jan Iwanow'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'ClosureTable', u'ClosureTable Documentation', - u'Jan Iwanow', 'ClosureTable', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False diff --git a/docs/create.rst b/docs/create.rst deleted file mode 100644 index 73a9ac8..0000000 --- a/docs/create.rst +++ /dev/null @@ -1,43 +0,0 @@ -.. index:: - single: Setup your ClosureTable - -Setup your ClosureTable -======================= - -Create models and migrations ----------------------------- - -For example, let's assume you're working on pages. You can just use an ``artisan`` command to create models and migrations automatically without preparing all the stuff by hand. Open terminal and put the following: - -.. code-block:: bash - - php artisan closuretable:make --entity=page - -All options of the command: - -1. ``--namespace``, ``-ns`` *[optional]*: namespace for classes, set by ``--entity`` and ``--closure`` options, helps to avoid namespace duplication in those options -2. ``--entity``, ``-e``: entity class name; if namespaced name is used, then the default closure class name will be prepended with that namespace -3. ``--entity-table``, ``-et`` *[optional]*: entity table name -4. ``--closure``, ``-c`` *[optional]*: closure class name -5. ``--closure-table``, ``-ct`` *[optional]*: closure table name -6. ``--models-path``, ``-mdl`` *[optional]*: custom models path -7. ``--migrations-path``, ``-mgr`` *[optional]*: custom migrations path -8. ``--use-innodb`` and ``-i`` *[optional]*: InnoDB migrations have been made optional as well with new paramaters. Setting this will enable the InnoDB engine. - -That's almost all, folks! The ‘dummy’ stuff has just been created for you. You will need to add some fields to your entity migration because the created ‘dummy’ includes just **required** ``id``, ``parent_id``, ``position``, and ``real depth`` columns: - -1. ``id`` is a regular autoincremented column -2. ``parent_id`` column is used to simplify immediate ancestor querying and, for example, to simplify building the whole tree -3. ``position`` column is used widely by the package to make entities sortable -4. ``real depth`` column is also used to simplify queries and reduce their number - -By default, entity’s closure table includes the following columns: - -1. **Autoincremented identifier** -2. **Ancestor column** points on a parent node -3. **Descendant column** points on a child node -4. **Depth column** shows a node depth in the tree - -It is by closure table pattern design, so remember that you must not delete these four columns. - -Remember that many things are made customizable, so see :doc:`Customization` for more information. \ No newline at end of file diff --git a/docs/customization.rst b/docs/customization.rst deleted file mode 100644 index 2e9fc4a..0000000 --- a/docs/customization.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. index:: - single: Customization - -Customization -============= - -You can customize the default things in your classes created by the ClosureTable ``artisan`` command: - -1. **Entity table name**: change ``protected $table`` property -2. **Closure table name**: do the same in your ``ClosureTable`` (e.g. ``PageClosure``) -3. **Entity's ``parent_id``, ``position``, and ``real depth`` column names**: change return values of ``getParentIdColumn()``, ``getPositionColumn()``, and ``getRealDepthColumn()`` respectively -4. **Closure table's ``ancestor``, ``descendant``, and ``depth`` columns names**: change return values of ``getAncestorColumn()``, ``getDescendantColumn()``, and ``getDepthColumn()`` respectively. \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 3052178..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,33 +0,0 @@ -ClosureTable Documentation -========================== - -## NOTE: Master branch is now for Laravel 5.
If you use Laravel 4, please see L4 branch! - -Hi, this is a database package for Laravel. It's intended to use when you need to operate hierarchical data in database. The package is an implementation of a well-known database design pattern called Closure Table. The package includes generators for models and migrations. - -.. image:: https://travis-ci.org/franzose/ClosureTable.png - :target: https://travis-ci.org/franzose/ClosureTable - -.. image:: https://poser.pugx.org/franzose/closure-table/v/stable.png - :target: https://packagist.org/packages/franzose/closure-table - -.. image:: https://poser.pugx.org/franzose/closure-table/downloads.png - :target: https://packagist.org/packages/franzose/closure-table - -Contents -======== - -.. toctree:: - :maxdepth: 2 - - installation - create - usage - customization - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` \ No newline at end of file diff --git a/docs/installation.rst b/docs/installation.rst deleted file mode 100644 index bffba6c..0000000 --- a/docs/installation.rst +++ /dev/null @@ -1,23 +0,0 @@ -.. index:: - single: Installation - -Installation -============ - -To install the package, put the following in your composer.json: - -.. code-block:: json - - "require": { - "franzose/closure-table": "4.*" - } - - -And to ``app/config/app.php``: - -.. code-block:: php - - 'providers' => array( - // ... - 'Franzose\ClosureTable\ClosureTableServiceProvider', - ), \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 65760cb..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,242 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ClosureTable.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ClosureTable.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end diff --git a/docs/usage.rst b/docs/usage.rst deleted file mode 100644 index e4b1127..0000000 --- a/docs/usage.rst +++ /dev/null @@ -1,166 +0,0 @@ -.. index:: - single: Usage - -Usage -===== - -Time of coding --------------- - -Once your models and their database tables are created, at last, you can start actually coding. Here I will show you ClosureTable's specific approaches. - -Direct ancestor (parent) ------------------------- - -.. code-block:: php - - $parent = Page::find(15)->getParent(); - -Ancestors ---------- - -.. code-block:: php - - $page = Page::find(15); - $ancestors = $page->getAncestors(); - $ancestors = $page->getAncestorsWhere('position', '=', 1); - $hasAncestors = $page->hasAncestors(); - $ancestorsNumber = $page->countAncestors(); - -Direct descendants (children) ------------------------------ - -.. code-block:: php - - $page = Page::find(15); - $children = $page->getChildren(); - $hasChildren = $page->hasChildren(); - $childrenNumber = $page->countChildren(); - - $newChild = new Page(array( - 'title' => 'The title', - 'excerpt' => 'The excerpt', - 'content' => 'The content of a child' - )); - - $newChild2 = new Page(array( - 'title' => 'The title', - 'excerpt' => 'The excerpt', - 'content' => 'The content of a child' - )); - - $page->addChild($newChild); - - //you can set child position - $page->addChild($newChild, 5); - - //you can get the child - $child = $page->addChild($newChild, null, true); - - $page->addChildren([$newChild, $newChild2]); - - $page->getChildAt(5); - $page->getFirstChild(); - $page->getLastChild(); - $page->getChildrenRange(0, 2); - - $page->removeChild(0); - $page->removeChild(0, true); //force delete - $page->removeChildren(0, 3); - $page->removeChildren(0, 3, true); //force delete - -Descendants ------------ - -.. code-block:: php - - $page = Page::find(15); - $descendants = $page->getDescendants(); - $descendants = $page->getDescendantsWhere('position', '=', 1); - $descendantsTree = $page->getDescendantsTree(); - $hasDescendants = $page->hasDescendants(); - $descendantsNumber = $page->countDescendants(); - -Siblings --------- - -.. code-block:: php - - $page = Page::find(15); - $first = $page->getFirstSibling(); //or $page->getSiblingAt(0); - $last = $page->getLastSibling(); - $atpos = $page->getSiblingAt(5); - - $prevOne = $page->getPrevSibling(); - $prevAll = $page->getPrevSiblings(); - $hasPrevs = $page->hasPrevSiblings(); - $prevsNumber = $page->countPrevSiblings(); - - $nextOne = $page->getNextSibling(); - $nextAll = $page->getNextSiblings(); - $hasNext = $page->hasNextSiblings(); - $nextNumber = $page->countNextSiblings(); - - //in both directions - $hasSiblings = $page->hasSiblings(); - $siblingsNumber = $page->countSiblings(); - - $sibligns = $page->getSiblingsRange(0, 2); - - $page->addSibling(new Page); - $page->addSibling(new Page, 3); //third position - - //add and get the sibling - $sibling = $page->addSibling(new Page, null, true); - - $page->addSiblings([new Page, new Page]); - $page->addSiblings([new Page, new Page], 5); //insert from fifth position - -Roots (entities that have no ancestors) ---------------------------------------- - -.. code-block:: php - - $roots = Page::getRoots(); - $isRoot = Page::find(23)->isRoot(); - Page::find(11)->makeRoot(); - -Entire tree ------------ - -.. code-block:: php - - $tree = Page::getTree(); - $treeByCondition = Page::getTreeWhere('position', '>=', 1); - -You deal with the collection, thus you can control its items as you usually do. Descendants? They are already loaded. - -.. code-block:: php - - $tree = Page::getTree(); - $page = $tree->find(15); - $children = $page->getChildren(); - $child = $page->getChildAt(3); - $grandchildren = $page->getChildAt(3)->getChildren(); //and so on - -Moving ------- - -.. code-block:: php - - $page = Page::find(25); - $page->moveTo(0, Page::find(14)); - $page->moveTo(0, 14); - -Deleting subtree ----------------- - -If you don't use foreign keys for some reason, you can delete subtree manually. This will delete the page and all its descendants: - -.. code-block:: php - - $page = Page::find(34); - $page->deleteSubtree(); - $page->deleteSubtree(true); //with subtree ancestor - $page->deleteSubtree(false, true); //without subtree ancestor and force delete - diff --git a/phpunit.xml b/phpunit.xml index 0fd022f..66f6b82 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -8,11 +8,13 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="true" - syntaxCheck="true" > - ./tests/ + ./tests/Console + ./tests/Extensions + ./tests/Generators + ./tests/Models diff --git a/src/ClosureTableServiceProvider.php b/src/ClosureTableServiceProvider.php new file mode 100644 index 0000000..17df77d --- /dev/null +++ b/src/ClosureTableServiceProvider.php @@ -0,0 +1,47 @@ +app->singleton('command.closuretable.make', static function ($app) { + return $app[MakeCommand::class]; + }); + + $this->commands('command.closuretable.make'); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return []; + } + +} diff --git a/src/Franzose/ClosureTable/Console/MakeCommand.php b/src/Console/MakeCommand.php similarity index 60% rename from src/Franzose/ClosureTable/Console/MakeCommand.php rename to src/Console/MakeCommand.php index 51e919f..47e9519 100644 --- a/src/Franzose/ClosureTable/Console/MakeCommand.php +++ b/src/Console/MakeCommand.php @@ -6,8 +6,8 @@ use Franzose\ClosureTable\Generators\Migration; use Franzose\ClosureTable\Generators\Model; use Illuminate\Console\Command; -use Illuminate\Container\Container; use Illuminate\Support\Composer; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; /** @@ -36,21 +36,22 @@ class MakeCommand extends Command * * @var \Franzose\ClosureTable\Generators\Migration */ - protected $migrator; + private $migrator; /** * Models generator instance. * * @var \Franzose\ClosureTable\Generators\Model */ - protected $modeler; + private $modeler; /** * User input arguments. * * @var array */ - protected $options; + private $options; + /** * @var Composer */ @@ -116,6 +117,13 @@ protected function writeModels() } } + protected function getArguments() + { + return [ + ['entity', InputArgument::REQUIRED, 'Class name of the entity model'] + ]; + } + /** * Gets the console command options. * @@ -124,14 +132,13 @@ protected function writeModels() protected function getOptions() { return [ - ['namespace', 'ns', InputOption::VALUE_OPTIONAL, 'Namespace for entity and its closure.'], - ['entity', 'e', InputOption::VALUE_REQUIRED, 'Entity class name.'], - ['entity-table', 'et', InputOption::VALUE_OPTIONAL, 'Entity table name.'], - ['closure', 'c', InputOption::VALUE_OPTIONAL, 'Closure class name'], - ['closure-table', 'ct', InputOption::VALUE_OPTIONAL, 'Closure table name.'], - ['models-path', 'mdl', InputOption::VALUE_OPTIONAL, 'Models path.'], - ['migrations-path', 'mgr', InputOption::VALUE_OPTIONAL, 'Migrations path.'], - ['use-innodb', 'i', InputOption::VALUE_OPTIONAL, 'Use InnoDB tables.'], + ['namespace', 'ns', InputOption::VALUE_OPTIONAL, 'Namespace for entity and closure classes. Once given, it will override namespaces of the models of entity and closure'], + ['entity-table', 'et', InputOption::VALUE_OPTIONAL, 'Database table name for entity'], + ['closure', 'c', InputOption::VALUE_OPTIONAL, 'Class name of the closure (relationships) model'], + ['closure-table', 'ct', InputOption::VALUE_OPTIONAL, 'Database table name for closure (relationships)'], + ['models-path', 'mdl', InputOption::VALUE_OPTIONAL, 'Directory in which to put generated models'], + ['migrations-path', 'mgr', InputOption::VALUE_OPTIONAL, 'Directory in which to put generated migrations'], + ['use-innodb', 'i', InputOption::VALUE_OPTIONAL, 'Use InnoDB engine (MySQL only)'], ]; } @@ -142,22 +149,48 @@ protected function getOptions() */ protected function prepareOptions() { + $entity = $this->argument('entity'); $options = $this->getOptions(); - $input = []; + $input = array_map(function (array $option) { + return $this->option($option[0]); + }, $this->getOptions()); + + $this->options[$options[0][0]] = $this->getNamespace($entity, $input[0]); + $this->options['entity'] = $this->getEntityModelName($entity); + $this->options[$options[1][0]] = $input[1] ?: ExtStr::tableize($this->options['entity']); + $this->options[$options[2][0]] = $input[2] + ? $this->getEntityModelName($input[2]) + : $this->options['entity'] . 'Closure'; + + $this->options[$options[3][0]] = $input[3] ?: ExtStr::snake($this->options[$options[2][0]]); + $this->options[$options[4][0]] = $input[4] ?: app_path(); + $this->options[$options[5][0]] = $input[5] ?: app()->databasePath('migrations'); + $this->options[$options[6][0]] = $input[6] ?: false; + } + + private function getNamespace($entity, $original) + { + if (!empty($original)) { + return $original; + } - foreach ($options as $option) { - $input[] = $this->option($option[0]); + $namespace = substr($entity, 0, strrpos($entity, '\\')); + + if (!empty($namespace)) { + return $namespace; } - $lastnsdelim = strrpos($input[1], '\\'); + return rtrim(app()->getNamespace(), '\\'); + } + + private function getEntityModelName($original) + { + $delimpos = strrpos($original, '\\'); + + if ($delimpos === false) { + return $original; + } - $this->options[$options[0][0]] = $input[0] ?: rtrim(Container::getInstance()->getNamespace(), '\\'); - $this->options[$options[1][0]] = substr($input[1], $lastnsdelim); - $this->options[$options[2][0]] = $input[2] ?: ExtStr::tableize($input[1]); - $this->options[$options[3][0]] = $input[3] ?: $this->options[$options[1][0]] . 'Closure'; - $this->options[$options[4][0]] = $input[4] ?: ExtStr::tableize($input[1] . 'Closure'); - $this->options[$options[5][0]] = $input[5] ? $input[5] : './app'; - $this->options[$options[6][0]] = $input[6] ? $input[6] : './database/migrations'; - $this->options[$options[7][0]] = $input[7] ?: false; + return substr($original, $delimpos + 1); } } diff --git a/src/Franzose/ClosureTable/Contracts/ClosureTableInterface.php b/src/Contracts/ClosureTableInterface.php similarity index 97% rename from src/Franzose/ClosureTable/Contracts/ClosureTableInterface.php rename to src/Contracts/ClosureTableInterface.php index 598d51d..1ded8f2 100644 --- a/src/Franzose/ClosureTable/Contracts/ClosureTableInterface.php +++ b/src/Contracts/ClosureTableInterface.php @@ -4,6 +4,7 @@ /** * Basic ClosureTable model interface. * + * @deprecated since 6.0 * @package Franzose\ClosureTable */ interface ClosureTableInterface diff --git a/src/Franzose/ClosureTable/Contracts/EntityInterface.php b/src/Contracts/EntityInterface.php similarity index 94% rename from src/Franzose/ClosureTable/Contracts/EntityInterface.php rename to src/Contracts/EntityInterface.php index a5944d4..3fd5ca0 100644 --- a/src/Franzose/ClosureTable/Contracts/EntityInterface.php +++ b/src/Contracts/EntityInterface.php @@ -4,6 +4,7 @@ /** * Basic Entity model interface. * + * @deprecated since 6.0 * @package Franzose\ClosureTable\Contracts */ interface EntityInterface @@ -22,20 +23,6 @@ public function getParentIdColumn(); */ public function getPositionColumn(); - /** - * Gets the short name of the "real depth" column. - * - * @return string - */ - public function getRealDepthColumn(); - - /** - * Gets the "children" relation index. - * - * @return string - */ - public function getChildrenRelationIndex(); - /** * "Query all models" flag. * @@ -234,10 +221,11 @@ public function addChild(EntityInterface $child, $position = null, $returnChild * Appends multiple children to the model. * * @param array $children + * @param int $from * @return $this * @throws \InvalidArgumentException */ - public function addChildren(array $children); + public function addChildren(array $children, $from = null); /** * Removes a model's child with given position. @@ -419,14 +407,6 @@ public static function getRoots(array $columns = ['*']); */ public function makeRoot($position); - /** - * Retrieves entire tree. - * - * @param array $columns - * @return \Franzose\ClosureTable\Extensions\Collection - */ - public static function getTree(array $columns = ['*']); - /** * Saves models from the given attributes array. * diff --git a/src/Extensions/Collection.php b/src/Extensions/Collection.php new file mode 100644 index 0000000..e3cd8f7 --- /dev/null +++ b/src/Extensions/Collection.php @@ -0,0 +1,182 @@ +filter(static function (Entity $entity) use ($position) { + return $entity->position === $position; + })->first(); + } + + /** + * Returns the first child node. + * + * @return Entity|null + */ + public function getFirstChild() + { + return $this->getChildAt(0); + } + + /** + * Returns the last child node. + * + * @return Entity|null + */ + public function getLastChild() + { + return $this->sortByDesc(static function (Entity $entity) { + return $entity->position; + })->first(); + } + + /** + * Filters the collection by the given positions. + * + * @param int $from + * @param int|null $to + * + * @return Collection + */ + public function getRange($from, $to = null) + { + return $this->filter(static function (Entity $entity) use ($from, $to) { + if ($to === null) { + return $entity->position >= $from; + } + + return $entity->position >= $from && $entity->position <= $to; + }); + } + + /** + * Filters collection to return nodes on the "left" + * and on the "right" from the node with the given position. + * + * @param int $position + * + * @return Collection + */ + public function getNeighbors($position) + { + return $this->filter(static function (Entity $entity) use ($position) { + return $entity->position === $position - 1 || + $entity->position === $position + 1; + }); + } + + /** + * Filters collection to return previous siblings of a node with the given position. + * + * @param int $position + * + * @return Collection + */ + public function getPrevSiblings($position) + { + return $this->filter(static function (Entity $entity) use ($position) { + return $entity->position < $position; + }); + } + + /** + * Filters collection to return next siblings of a node with the given position. + * + * @param int $position + * + * @return Collection + */ + public function getNextSiblings($position) + { + return $this->filter(static function (Entity $entity) use ($position) { + return $entity->position > $position; + }); + } + + /** + * Retrieves children relation. + * + * @param $position + * @return Collection + */ + public function getChildrenOf($position) + { + if (!$this->hasChildren($position)) { + return new static(); + } + + return $this->getChildAt($position)->children; + } + + /** + * Indicates whether an item has children. + * + * @param $position + * @return bool + */ + public function hasChildren($position) + { + $item = $this->getChildAt($position); + + return $item !== null && $item->children->count() > 0; + } + + /** + * Makes tree-like collection. + * + * @return Collection + */ + public function toTree() + { + $items = $this->items; + + return new static($this->makeTree($items)); + } + + /** + * Performs actual tree building. + * + * @param Entity[] $items + * @return array + */ + protected function makeTree(array $items) + { + /** @var Entity[] $result */ + $result = []; + $tops = []; + + foreach ($items as $item) { + $result[$item->getKey()] = $item; + } + + foreach ($items as $item) { + $parentId = $item->parent_id; + + if (array_key_exists($parentId, $result)) { + $result[$parentId]->children->add($item); + } else { + $tops[] = $item; + } + } + + return $tops; + } +} diff --git a/src/Franzose/ClosureTable/Extensions/Str.php b/src/Extensions/Str.php similarity index 76% rename from src/Franzose/ClosureTable/Extensions/Str.php rename to src/Extensions/Str.php index a717edf..de2e130 100644 --- a/src/Franzose/ClosureTable/Extensions/Str.php +++ b/src/Extensions/Str.php @@ -13,7 +13,7 @@ class Str extends BaseStr /** * Makes appropriate class name from given string. * - * @param $name + * @param string $name * @return string */ public static function classify($name) @@ -24,13 +24,15 @@ public static function classify($name) /** * Makes database table name from given class name. * - * @param $name + * @param string $name * @return string */ public static function tableize($name) { $name = str_replace('\\', '', $name); - return (static::endsWith($name, 'Closure') ? static::snake($name) : static::snake(static::plural($name))); + return static::endsWith($name, 'Closure') + ? static::snake($name) + : static::snake(static::plural($name)); } } diff --git a/src/Franzose/ClosureTable/ClosureTableServiceProvider.php b/src/Franzose/ClosureTable/ClosureTableServiceProvider.php deleted file mode 100644 index 7fe80bb..0000000 --- a/src/Franzose/ClosureTable/ClosureTableServiceProvider.php +++ /dev/null @@ -1,68 +0,0 @@ -app->singleton('command.closuretable', function ($app) { - return new ClosureTableCommand; - }); - - $this->app->singleton('command.closuretable.make', function ($app) { - return $app['Franzose\ClosureTable\Console\MakeCommand']; - }); - - $this->commands('command.closuretable', 'command.closuretable.make'); - } - - /** - * Get the services provided by the provider. - * - * @return array - */ - public function provides() - { - return array(); - } - -} diff --git a/src/Franzose/ClosureTable/Console/ClosureTableCommand.php b/src/Franzose/ClosureTable/Console/ClosureTableCommand.php deleted file mode 100644 index 072bec6..0000000 --- a/src/Franzose/ClosureTable/Console/ClosureTableCommand.php +++ /dev/null @@ -1,39 +0,0 @@ -info('ClosureTable v' . CT::VERSION); - $this->line('Closure Table database design pattern implementation for Laravel framework.'); - $this->comment('Copyright (c) 2013-2014 Jan Iwanow'); - } -} diff --git a/src/Franzose/ClosureTable/Extensions/Collection.php b/src/Franzose/ClosureTable/Extensions/Collection.php deleted file mode 100644 index a0d52f1..0000000 --- a/src/Franzose/ClosureTable/Extensions/Collection.php +++ /dev/null @@ -1,88 +0,0 @@ -hasChildren($position)) { - return null; - } - - $item = $this->get($position); - $relation = $item->getChildrenRelationIndex(); - - return $item->getRelation($relation); - } - - /** - * Indicates whether an item has children. - * - * @param $position - * @return bool - */ - public function hasChildren($position) - { - $item = $this->get($position); - $relation = $item->getChildrenRelationIndex(); - - return array_key_exists($relation, $item->getRelations()); - } - - /** - * Makes tree-like collection. - * - * @return Collection - */ - public function toTree() - { - $items = $this->items; - - return new static($this->makeTree($items)); - } - - /** - * Performs actual tree building. - * - * @param array $items - * @return array - */ - protected function makeTree(array &$items) - { - $result = []; - $tops = []; - - /** - * @var Entity $item - */ - foreach ($items as $item) { - $result[$item->getKey()] = $item; - } - - foreach ($items as $item) { - $parentId = $item->{$item->getParentIdColumn()}; - - if (array_key_exists($parentId, $result)) { - $result[$parentId]->appendRelation($item->getChildrenRelationIndex(), $item); - } else { - $tops[] = $item; - } - } - - return $tops; - } -} diff --git a/src/Franzose/ClosureTable/Extensions/QueryBuilder.php b/src/Franzose/ClosureTable/Extensions/QueryBuilder.php deleted file mode 100644 index 8ab21ff..0000000 --- a/src/Franzose/ClosureTable/Extensions/QueryBuilder.php +++ /dev/null @@ -1,30 +0,0 @@ -where($column, '>=', $values[0]); - } else { - $this->whereIn($column, range($values[0], $values[1])); - } - - return $this; - } -} diff --git a/src/Franzose/ClosureTable/Generators/stubs/models/closuretableinterface.php b/src/Franzose/ClosureTable/Generators/stubs/models/closuretableinterface.php deleted file mode 100644 index 3eb0e53..0000000 --- a/src/Franzose/ClosureTable/Generators/stubs/models/closuretableinterface.php +++ /dev/null @@ -1,8 +0,0 @@ -pos or $this->position. - * - * @property int position Alias for the current position attribute name - * @property int parent_id Alias for the direct ancestor identifier attribute name - * @property int real_depth Alias for the real depth attribute name - * - * @package Franzose\ClosureTable - */ -class Entity extends Eloquent implements EntityInterface -{ - /** - * ClosureTable model instance. - * - * @var ClosureTable - */ - protected $closure = 'Franzose\ClosureTable\Models\ClosureTable'; - - /** - * Cached "previous" (i.e. before the model is moved) direct ancestor id of this model. - * - * @var int - */ - protected $old_parent_id; - - /** - * Cached "previous" (i.e. before the model is moved) model position. - * - * @var int - */ - protected $old_position; - - /** - * Cached "previous" (i.e. before the model is moved) model real depth. - * - * @var int - */ - protected $old_real_depth; - - /** - * Indicates if the model should soft delete. - * - * @var bool - */ - protected $softDelete = true; - - /** - * Indicates if the model is being moved to another ancestor. - * - * @var bool - */ - protected $isMoved = false; - - /** - * Indicates if the model should be timestamped. - * - * @var bool - */ - public $timestamps = false; - - public static $debug = false; - - /** - * Entity constructor. - * - * @param array $attributes - */ - public function __construct(array $attributes = []) - { - $position = $this->getPositionColumn(); - $depth = $this->getRealDepthColumn(); - - $this->fillable(array_merge($this->getFillable(), [$position, $depth])); - - if (!isset($attributes[$depth])) { - $attributes[$depth] = 0; - } - - $this->closure = new $this->closure; - - // The default class name of the closure table was not changed - // so we define and set default closure table name automagically. - // This can prevent useless copy paste of closure table models. - if (get_class($this->closure) == 'Franzose\ClosureTable\Models\ClosureTable') { - $table = $this->getTable() . '_closure'; - $this->closure->setTable($table); - } - - parent::__construct($attributes); - } - - public function newFromBuilder($attributes = array(), $connection = null) - { - $instance = parent::newFromBuilder($attributes); - $instance->old_parent_id = $instance->parent_id; - $instance->old_position = $instance->position; - return $instance; - } - - /** - * Gets value of the "parent id" attribute. - * - * @return int - */ - public function getParentIdAttribute() - { - return $this->getAttributeFromArray($this->getParentIdColumn()); - } - - /** - * Sets new parent id and caches the old one. - * - * @param int $value - */ - public function setParentIdAttribute($value) - { - if ($this->parent_id === $value) { - return; - } - $this->old_parent_id = $this->parent_id; - $this->attributes[$this->getParentIdColumn()] = $value; - } - - /** - * Gets the fully qualified "parent id" column. - * - * @return string - */ - public function getQualifiedParentIdColumn() - { - return $this->getTable() . '.' . $this->getParentIdColumn(); - } - - /** - * Gets the short name of the "parent id" column. - * - * @return string - */ - public function getParentIdColumn() - { - return 'parent_id'; - } - - /** - * Gets value of the "position" attribute. - * - * @return int - */ - public function getPositionAttribute() - { - return $this->getAttributeFromArray($this->getPositionColumn()); - } - - /** - * Sets new position and caches the old one. - * - * @param int $value - */ - public function setPositionAttribute($value) - { - if ($this->position === $value) { - return; - } - $this->old_position = $this->position; - $this->attributes[$this->getPositionColumn()] = intval($value); - } - - /** - * Gets the fully qualified "position" column. - * - * @return string - */ - public function getQualifiedPositionColumn() - { - return $this->getTable() . '.' . $this->getPositionColumn(); - } - - /** - * Gets the short name of the "position" column. - * - * @return string - */ - public function getPositionColumn() - { - return 'position'; - } - - /** - * Gets value of the "real depth" attribute. - * - * @return int - */ - public function getRealDepthAttribute() - { - return $this->getAttributeFromArray($this->getRealDepthColumn()); - } - - /** - * Sets value of the "real depth" attribute. - * - * @param int $value - */ - protected function setRealDepthAttribute($value) - { - if ($this->real_depth === $value) { - return; - } - $this->old_real_depth = $this->real_depth; - $this->attributes[$this->getRealDepthColumn()] = intval($value); - } - - /** - * Gets the fully qualified "real depth" column. - * - * @return string - */ - public function getQualifiedRealDepthColumn() - { - return $this->getTable() . '.' . $this->getRealDepthColumn(); - } - - /** - * Gets the short name of the "real depth" column. - * - * @return string - */ - public function getRealDepthColumn() - { - return 'real_depth'; - } - - /** - * Gets the "children" relation index. - * - * @return string - */ - public function getChildrenRelationIndex() - { - return 'children'; - } - - /** - * The "booting" method of the model. - * - * @return void - */ - public static function boot() - { - parent::boot(); - - // If model's parent identifier was changed, - // the closure table rows will update automatically. - static::saving(function (Entity $entity) { - $entity->clampPosition(); - $entity->moveNode(); - }); - - // When entity is created, the appropriate - // data will be put into the closure table. - static::created(function (Entity $entity) { - $entity->old_parent_id = false; - $entity->old_position = $entity->position; - $entity->insertNode(); - }); - - // Everytime the model's position or parent - // is changed, its siblings reordering will happen, - // so they will always keep the proper order. - static::saved(function (Entity $entity) { - $entity->reorderSiblings(); - }); - } - - /** - * Indicates whether the model is a parent. - * - * @return bool - */ - public function isParent() - { - return $this->hasChildren(); - } - - /** - * Indicates whether the model has no ancestors. - * - * @return bool - */ - public function isRoot() - { - if (!$this->exists) { - return false; - } - - return is_null($this->parent_id); - } - - /** - * Retrieves direct ancestor of a model. - * - * @param array $columns - * @return Entity - */ - public function getParent(array $columns = ['*']) - { - return static::find($this->parent_id, $columns); - } - - /** - * Builds closure table join based on the given column. - * - * @param string $column - * @param bool $withSelf - * @return QueryBuilder - */ - protected function joinClosureBy($column, $withSelf = false) - { - $primary = $this->getQualifiedKeyName(); - $closure = $this->closure->getTable(); - $ancestor = $this->closure->getQualifiedAncestorColumn(); - $descendant = $this->closure->getQualifiedDescendantColumn(); - - switch ($column) { - case 'ancestor': - $query = $this->join($closure, $ancestor, '=', $primary) - ->where($descendant, '=', $this->getKey()); - break; - - case 'descendant': - $query = $this->join($closure, $descendant, '=', $primary) - ->where($ancestor, '=', $this->getKey()); - break; - } - - $depthOperator = ($withSelf === true ? '>=' : '>'); - - $query->where($this->closure->getQualifiedDepthColumn(), $depthOperator, 0); - - return $query; - } - - /** - * Builds closure table "where in" query on the given column. - * - * @param string $column - * @param bool $withSelf - * @return QueryBuilder - */ - protected function subqueryClosureBy($column, $withSelf = false) - { - $self = $this; - - return $this->whereIn($this->getQualifiedKeyName(), function ($qb) use ($self, $column, $withSelf) { - switch ($column) { - case 'ancestor': - $selectedColumn = $self->closure->getAncestorColumn(); - $whereColumn = $self->closure->getDescendantColumn(); - break; - - case 'descendant': - $selectedColumn = $self->closure->getDescendantColumn(); - $whereColumn = $self->closure->getAncestorColumn(); - break; - } - - $depthOperator = ($withSelf === true ? '>=' : '>'); - - return $qb->select($selectedColumn) - ->from($self->closure->getTable()) - ->where($whereColumn, '=', $self->getKey()) - ->where($self->closure->getDepthColumn(), $depthOperator, 0); - }); - } - - /** - * Retrieves all ancestors of a model. - * - * @param array $columns - * @return Collection - */ - public function getAncestors(array $columns = ['*']) - { - return $this->joinClosureBy('ancestor')->get($columns); - } - - /** - * Retrieves tree structured ancestors of a model. - * - * @param array $columns - * @return Collection - */ - public function getAncestorsTree(array $columns = ['*']) - { - return $this->getAncestors($columns)->toTree(); - } - - /** - * Retrieves ancestors applying given conditions. - * - * @param mixed $column - * @param mixed $operator - * @param mixed $value - * @param array $columns - * @return Collection - */ - public function getAncestorsWhere($column, $operator = null, $value = null, array $columns = ['*']) - { - return $this->joinClosureBy('ancestor')->where($column, $operator, $value)->get($columns); - } - - /** - * Returns a number of model's ancestors. - * - * @return int - */ - public function countAncestors() - { - return $this->joinClosureBy('ancestor')->count(); - } - - /** - * Indicates whether a model has ancestors. - * - * @return bool - */ - public function hasAncestors() - { - return !!$this->countAncestors(); - } - - /** - * Retrieves all descendants of a model. - * - * @param array $columns - * @return Collection - */ - public function getDescendants(array $columns = ['*']) - { - return $this->joinClosureBy('descendant')->get($columns); - } - - /** - * Retrieves tree structured descendants of a model. - * - * @param array $columns - * @return Collection - */ - public function getDescendantsTree(array $columns = ['*']) - { - return $this->getDescendants($columns)->toTree(); - } - - /** - * Retrieves descendants applying given conditions. - * - * @param mixed $column - * @param mixed $operator - * @param mixed $value - * @param array $columns - * @return Collection - */ - public function getDescendantsWhere($column, $operator = null, $value = null, array $columns = ['*']) - { - return $this->joinClosureBy('descendant')->where($column, $operator, $value)->get($columns); - } - - /** - * Returns a number of model's descendants. - * - * @return int - */ - public function countDescendants() - { - return $this->joinClosureBy('descendant')->count(); - } - - /** - * Indicates whether a model has descendants. - * - * @return bool - */ - public function hasDescendants() - { - return !!$this->countDescendants(); - } - - /** - * Shorthand of the children query part. - * - * @param array|int|null $position - * @param string $order - * @return QueryBuilder - */ - protected function children($position = null, $order = 'asc') - { - $query = $this->queryByParentId(); - - if (!is_null($position)) { - if (is_array($position)) { - $query->buildWherePosition($this->getPositionColumn(), $position); - } else { - if ($position === static::QUERY_LAST) { - $query->orderBy($this->getPositionColumn(), 'desc'); - } else { - $query->where($this->getPositionColumn(), '=', $position); - } - } - } - - if ($position !== static::QUERY_LAST) { - $query->orderBy($this->getPositionColumn(), $order); - } - - return $query; - } - - /** - * Starts a query by parent identifier. - * - * @param mixed $id - * @return QueryBuilder - */ - protected function queryByParentId($id = null) - { - $id = ($id ?: $this->getKey()); - - return $this->where($this->getParentIdColumn(), '=', $id); - } - - /** - * Retrieves all children of a model. - * - * @param array $columns - * @return \Franzose\ClosureTable\Extensions\Collection - */ - public function getChildren(array $columns = ['*']) - { - if ($this->hasChildrenRelation()) { - $result = $this->getRelation($this->getChildrenRelationIndex()); - } else { - $result = $this->children()->get($columns); - } - - return $result; - } - - /** - * Returns a number of model's children. - * - * @return int - */ - public function countChildren() - { - if ($this->hasChildrenRelation()) { - $result = $this->getRelation($this->getChildrenRelationIndex())->count(); - } else { - $result = $this->queryByParentId()->count(); - } - - return $result; - } - - /** - * Indicates whether a model has children. - * - * @return bool - */ - public function hasChildren() - { - return !!$this->countChildren(); - } - - /** - * Indicates whether a model has children as a relation. - * - * @return bool - */ - public function hasChildrenRelation() - { - return array_key_exists($this->getChildrenRelationIndex(), $this->getRelations()); - } - - /** - * Pushes a new item to a relation. - * - * @param $relation - * @param $value - * @return $this - */ - public function appendRelation($relation, $value) - { - if (!array_key_exists($relation, $this->getRelations())) { - $this->setRelation($relation, new Collection([$value])); - } else { - $this->getRelation($relation)->add($value); - } - - return $this; - } - - /** - * Retrieves a child with given position. - * - * @param $position - * @param array $columns - * @return Entity - */ - public function getChildAt($position, array $columns = ['*']) - { - if ($this->hasChildrenRelation()) { - $result = $this->getRelation($this->getChildrenRelationIndex())->get($position); - } else { - $result = $this->children($position)->first($columns); - } - - return $result; - } - - /** - * Retrieves the first child. - * - * @param array $columns - * @return Entity - */ - public function getFirstChild(array $columns = ['*']) - { - return $this->getChildAt(0, $columns); - } - - /** - * Retrieves the last child. - * - * @param array $columns - * @return Entity - */ - public function getLastChild(array $columns = ['*']) - { - if ($this->hasChildrenRelation()) { - $result = $this->getRelation($this->getChildrenRelationIndex())->last(); - } else { - $result = $this->children(static::QUERY_LAST)->first($columns); - } - - return $result; - } - - /** - * Retrieves children within given positions range. - * - * @param int $from - * @param int $to - * @param array $columns - * @return Collection - */ - public function getChildrenRange($from, $to = null, array $columns = ['*']) - { - return $this->children([$from, $to])->get($columns); - } - - /** - * Gets last child position. - * - * @return int - */ - protected function getLastChildPosition() - { - $lastChild = $this->getLastChild([$this->getPositionColumn()]); - - return (is_null($lastChild) ? 0 : $lastChild->position); - } - - /** - * Appends a child to the model. - * - * @param EntityInterface $child - * @param int $position - * @param bool $returnChild - * @return EntityInterface - */ - public function addChild(EntityInterface $child, $position = null, $returnChild = false) - { - if ($this->exists) { - if (is_null($position)) { - $position = $this->getNextAfterLastPosition($this->getKey()); - } - - $child->moveTo($position, $this); - } - - return ($returnChild === true ? $child : $this); - } - - /** - * Appends a collection of children to the model. - * - * @param array $children - * @return $this - * @throws \InvalidArgumentException - */ - public function addChildren(array $children) - { - if ($this->exists) { - \DB::connection($this->connection)->transaction(function () use ($children) { - $lastChildPosition = $this->getLastChildPosition(); - - foreach ($children as $child) { - if (!$child instanceof EntityInterface) { - if (isset($child['id'])) { - unset($child['id']); - } - - $child = new static($child); - } - - $this->addChild($child, $lastChildPosition); - $lastChildPosition++; - } - }); - } - - return $this; - } - - /** - * Removes a model's child with given position. - * - * @param int $position - * @param bool $forceDelete - * @return $this - */ - public function removeChild($position = null, $forceDelete = false) - { - if ($this->exists) { - $action = ($forceDelete === true ? 'forceDelete' : 'delete'); - - $this->children($position)->$action(); - } - - return $this; - } - - /** - * Removes model's children within a range of positions. - * - * @param int $from - * @param int $to - * @param bool $forceDelete - * @return $this - * @throws \InvalidArgumentException - */ - public function removeChildren($from, $to = null, $forceDelete = false) - { - if (!is_numeric($from) || (!is_null($to) && !is_numeric($to))) { - throw new \InvalidArgumentException('`from` and `to` are the position boundaries. They must be of type int.'); - } - - if ($this->exists) { - $action = ($forceDelete === true ? 'forceDelete' : 'delete'); - - $this->children([$from, $to])->$action(); - } - - return $this; - } - - /** - * Builds a part of the siblings query. - * - * @param string|int|array $direction - * @param int|bool $parentId - * @param string $order - * @return QueryBuilder - */ - protected function siblings($direction = '', $parentId = false, $order = 'asc') - { - $parentId = ($parentId === false ? $this->parent_id : $parentId); - - /** - * @var QueryBuilder $query - */ - $query = $this->where($this->getParentIdColumn(), '=', $parentId); - - $column = $this->getPositionColumn(); - - switch ($direction) { - case static::QUERY_ALL: - $query->where($column, '<>', $this->position)->orderBy($column, $order); - break; - - case static::QUERY_PREV_ALL: - $query->where($column, '<', $this->position)->orderBy($column, $order); - break; - - case static::QUERY_PREV_ONE: - $query->where($column, '=', $this->position - 1); - break; - - case static::QUERY_NEXT_ALL: - $query->where($column, '>', $this->position)->orderBy($column, $order); - break; - - case static::QUERY_NEXT_ONE: - $query->where($column, '=', $this->position + 1); - break; - - case static::QUERY_NEIGHBORS: - $query->whereIn($column, [$this->position - 1, $this->position + 1]); - break; - - case static::QUERY_LAST: - $query->orderBy($column, 'desc'); - break; - } - - if (is_int($direction)) { - $query->where($column, '=', $direction); - } else if (is_array($direction)) { - $query->buildWherePosition($this->getPositionColumn(), $direction); - } - - return $query; - } - - /** - * Retrives all siblings of a model. - * - * @param array $columns - * @return \Franzose\ClosureTable\Extensions\Collection - */ - public function getSiblings(array $columns = ['*']) - { - return $this->siblings(static::QUERY_ALL)->get($columns); - } - - /** - * Returns number of model's siblings. - * - * @return int - */ - public function countSiblings() - { - return $this->siblings(static::QUERY_ALL)->count(); - } - - /** - * Indicates whether a model has siblings. - * - * @return bool - */ - public function hasSiblings() - { - return !!$this->countSiblings(); - } - - /** - * Retrieves neighbors (immediate previous and immediate next models) of a model. - * - * @param array $columns - * @return \Franzose\ClosureTable\Extensions\Collection - */ - public function getNeighbors(array $columns = ['*']) - { - return $this->siblings(static::QUERY_NEIGHBORS)->get($columns); - } - - /** - * Retrieves a model's sibling with given position. - * - * @param int $position - * @param array $columns - * @return Entity - */ - public function getSiblingAt($position, array $columns = ['*']) - { - return $this->siblings($position)->first($columns); - } - - /** - * Retrieves the first model's sibling. - * - * @param array $columns - * @return Entity - */ - public function getFirstSibling(array $columns = ['*']) - { - return $this->getSiblingAt(0, $columns); - } - - /** - * Retrieves the last model's sibling. - * - * @param array $columns - * @return Entity - */ - public function getLastSibling(array $columns = ['*']) - { - return $this->siblings(static::QUERY_LAST)->first($columns); - } - - /** - * Retrieves immediate previous sibling of a model. - * - * @param array $columns - * @return Entity - */ - public function getPrevSibling(array $columns = ['*']) - { - return $this->siblings(static::QUERY_PREV_ONE)->first($columns); - } - - /** - * Retrieves all previous siblings of a model. - * - * @param array $columns - * @return \Franzose\ClosureTable\Extensions\Collection - */ - public function getPrevSiblings(array $columns = ['*']) - { - return $this->siblings(static::QUERY_PREV_ALL)->get($columns); - } - - /** - * Returns number of previous siblings of a model. - * - * @return int - */ - public function countPrevSiblings() - { - return $this->siblings(static::QUERY_PREV_ALL)->count(); - } - - /** - * Indicates whether a model has previous siblings. - * - * @return bool - */ - public function hasPrevSiblings() - { - return !!$this->countPrevSiblings(); - } - - /** - * Retrieves immediate next sibling of a model. - * - * @param array $columns - * @return Entity - */ - public function getNextSibling(array $columns = ['*']) - { - return $this->siblings(static::QUERY_NEXT_ONE)->first($columns); - } - - /** - * Retrieves all next siblings of a model. - * - * @param array $columns - * @return \Franzose\ClosureTable\Extensions\Collection - */ - public function getNextSiblings(array $columns = ['*']) - { - return $this->siblings(static::QUERY_NEXT_ALL)->get($columns); - } - - /** - * Returns number of next siblings of a model. - * - * @return int - */ - public function countNextSiblings() - { - return $this->siblings(static::QUERY_NEXT_ALL)->count(); - } - - /** - * Indicates whether a model has next siblings. - * - * @return bool - */ - public function hasNextSiblings() - { - return !!$this->countNextSiblings(); - } - - /** - * Retrieves siblings within given positions range. - * - * @param int $from - * @param int $to - * @param array $columns - * @return Collection - */ - public function getSiblingsRange($from, $to = null, array $columns = ['*']) - { - return $this->siblings([$from, $to])->get($columns); - } - - /** - * Appends a sibling within the current depth. - * - * @param EntityInterface $sibling - * @param int|null $position - * @param bool $returnSibling - * @return EntityInterface - */ - public function addSibling(EntityInterface $sibling, $position = null, $returnSibling = false) - { - if ($this->exists) { - if (is_null($position)) { - $position = $this->getNextAfterLastPosition(); - } - - $sibling->moveTo($position, $this->parent_id); - } - - return ($returnSibling === true ? $sibling : $this); - } - - /** - * Appends multiple siblings within the current depth. - * - * @param array $siblings - * @param int|null $from - * @return $this - */ - public function addSiblings(array $siblings, $from = null) - { - if ($this->exists) { - if (is_null($from)) { - $from = $this->getNextAfterLastPosition(); - } - - $parent = $this->getParent(); - /** - * @var Entity $sibling - */ - foreach ($siblings as $sibling) { - $sibling->moveTo($from, $parent); - $from++; - } - } - - return $this; - } - - /** - * Retrieves root (with no ancestors) models. - * - * @param array $columns - * @return \Franzose\ClosureTable\Extensions\Collection - */ - public static function getRoots(array $columns = ['*']) - { - /** - * @var Entity $instance - */ - $instance = new static; - - return $instance->whereNull($instance->getParentIdColumn())->get($columns); - } - - /** - * Makes model a root with given position. - * - * @param int $position - * @return $this - */ - public function makeRoot($position) - { - return $this->moveTo($position, null); - } - - /** - * Adds "parent id" column to columns list for proper tree querying. - * - * @param array $columns - * @return array - */ - protected function prepareTreeQueryColumns(array $columns) - { - return ($columns === ['*'] ? $columns : array_merge($columns, [$this->getParentIdColumn()])); - } - - /** - * Retrieves entire tree. - * - * @param array $columns - * @return \Franzose\ClosureTable\Extensions\Collection - */ - public static function getTree(array $columns = ['*']) - { - /** - * @var Entity $instance - */ - $instance = new static; - - return $instance->orderBy($instance->getParentIdColumn())->orderBy($instance->getPositionColumn()) - ->get($instance->prepareTreeQueryColumns($columns))->toTree(); - } - - /** - * Retrieves tree by condition. - * - * @param mixed $column - * @param mixed $operator - * @param mixed $value - * @param array $columns - * @return \Franzose\ClosureTable\Extensions\Collection - */ - public static function getTreeWhere($column, $operator = null, $value = null, array $columns = ['*']) - { - /** - * @var Entity $instance - */ - $instance = new static; - $columns = $instance->prepareTreeQueryColumns($columns); - - return $instance->where($column, $operator, $value)->get($columns)->toTree(); - } - - /** - * Retrieves tree with any conditions using QueryBuilder - * @param EloquentBuilder $query - * @param array $columns - * @return \Franzose\ClosureTable\Extensions\Collection - */ - public static function getTreeByQuery(EloquentBuilder $query, array $columns = ['*']) - { - /** - * @var Entity $instance - */ - $instance = new static; - $columns = $instance->prepareTreeQueryColumns($columns); - return $query->get($columns)->toTree(); - } - - /** - * Saves models from the given attributes array. - * - * @param array $tree - * @param \Franzose\ClosureTable\Contracts\EntityInterface $parent - * @return \Franzose\ClosureTable\Extensions\Collection - */ - public static function createFromArray(array $tree, EntityInterface $parent = null) - { - $childrenRelationIndex = with(new static)->getChildrenRelationIndex(); - $entities = []; - - foreach ($tree as $item) { - $children = Arr::pull($item, $childrenRelationIndex); - - /** - * @var Entity $entity - */ - $entity = new static($item); - $entity->parent_id = $parent ? $parent->getKey() : null; - $entity->save(); - - if (!is_null($children)) { - $children = static::createFromArray($children, $entity); - $entity->setRelation($childrenRelationIndex, $children); - $entity->addChildren($children->all()); - } - - $entities[] = $entity; - } - - return new Collection($entities); - } - - /** - * Makes the model a child or a root with given position. Do not use moveTo to move a node within the same ancestor (call position = value and save instead). - * - * @param int $position - * @param EntityInterface|int $ancestor - * @return Entity - * @throws \InvalidArgumentException - */ - public function moveTo($position, $ancestor = null) - { - $parentId = (!$ancestor instanceof EntityInterface ? $ancestor : $ancestor->getKey()); - - if ($this->parent_id == $parentId && !is_null($this->parent_id)) { - return $this; - } - - if ($this->getKey() == $parentId) { - throw new \InvalidArgumentException('Target entity is equal to the sender.'); - } - - $this->parent_id = $parentId; - $this->position = $position; - $this->real_depth = $this->getNewRealDepth($ancestor); - - $this->isMoved = true; - - $this->save(); - - $this->isMoved = false; - - return $this; - } - - /** - * Gets real depth of the new ancestor of the model. - * - * @param Entity|int|null $ancestor - * @return int - */ - protected function getNewRealDepth($ancestor) - { - if (!$ancestor instanceof EntityInterface) { - if (is_null($ancestor)) { - return 0; - } else { - return static::find($ancestor)->real_depth + 1; - } - } else { - return $ancestor->real_depth + 1; - } - } - - /** - * Perform a model insert operation. - * - * @param EloquentBuilder $query - * @param array $options - * - * @return bool - */ - protected function performInsert(EloquentBuilder $query, array $options = []) - { - if ($this->isMoved === false) { - $this->position = $this->position !== null ? $this->position : $this->getNextAfterLastPosition(); - $this->real_depth = $this->getNewRealDepth($this->parent_id); - } - - return parent::performInsert($query, $options); - } - - /** - * Perform a model update operation. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param array $options - * - * @return bool - */ - protected function performUpdate(EloquentBuilder $query, array $options = []) - { - if (parent::performUpdate($query, $options)) { - if ($this->real_depth != $this->old_real_depth && $this->isMoved === true) { - $action = ($this->real_depth > $this->old_real_depth ? 'increment' : 'decrement'); - $amount = abs($this->real_depth - $this->old_real_depth); - - $this->subqueryClosureBy('descendant')->$action($this->getRealDepthColumn(), $amount); - } - - return true; - } - - return false; - } - - /** - * Gets the next sibling position after the last one at the given ancestor. - * - * @param int|bool $parentId - * @return int - */ - public function getNextAfterLastPosition($parentId = false) - { - $position = $this->getLastPosition($parentId); - return $position === null ? 0 : $position + 1; - } - - public function getLastPosition($parentId = false) - { - $positionColumn = $this->getPositionColumn(); - $parentIdColumn = $this->getParentIdColumn(); - - $parentId = ($parentId === false ? $this->parent_id : $parentId); - - $entity = $this->select($positionColumn) - ->where($parentIdColumn, '=', $parentId) - ->orderBy($positionColumn, 'desc') - ->first(); - - return !is_null($entity) ? (int)$entity->position : null; - } - - /** - * Reorders model's siblings when one is moved to another position or ancestor. - * - * @param bool $parentIdChanged - * @return void - */ - protected function reorderSiblings($parentIdChanged = false) - { - list($range, $action) = $this->setupReordering($parentIdChanged); - - $positionColumn = $this->getPositionColumn(); - - // As the method called twice (before moving and after moving), - // first we gather "old" siblings by the old parent id value of the model. - if ($parentIdChanged === true) { - $query = $this->siblings(false, $this->old_parent_id); - } else { - $query = $this->siblings(); - } - - if ($action) { - $query->buildWherePosition($positionColumn, $range) - ->where($this->getKeyName(), '<>', $this->getKey()) - ->$action($positionColumn); - } - } - - /** - * Setups model's siblings reordering. - * - * Actually, the method determines siblings that will be reordered - * by creating range of theirs positions and determining the action - * that will be used in reordering ('increment' or 'decrement'). - * - * @param bool $parentIdChanged - * @return array - */ - protected function setupReordering($parentIdChanged) - { - $range = $action = null; - // If the model's parent was changed, firstly we decrement - // positions of the 'old' next siblings of the model. - if ($parentIdChanged === true) { - $range = $this->old_position; - $action = 'decrement'; - } else { - // TODO: There's probably a bug here where if you just created an entity and you set it to be - // a root (parent_id = null) then it comes in here (while it should have gone in the else) - // Reordering within the same ancestor - if ($this->old_parent_id !== false && $this->old_parent_id == $this->parent_id) { - if ($this->position > $this->old_position) { - $range = [$this->old_position, $this->position]; - $action = 'decrement'; - } else if ($this->position < $this->old_position) { - $range = [$this->position, $this->old_position]; - $action = 'increment'; - } - } // Ancestor has changed - else { - $range = $this->position; - $action = 'increment'; - } - } - - if (!is_array($range)) { - $range = [$range, null]; - } - - return [$range, $action]; - } - - /** - * Inserts new node to closure table. - * - * @return void - */ - protected function insertNode() - { - $descendant = $this->getKey(); - $ancestor = (isset($this->parent_id) ? $this->parent_id : $descendant); - - $this->closure->insertNode($ancestor, $descendant); - } - - /** - * Moves node to another ancestor. - * - * @return void - */ - protected function moveNode() - { - if ($this->exists) { - if (is_null($this->closure->ancestor)) { - $primaryKey = $this->getKey(); - $this->closure->ancestor = $primaryKey; - $this->closure->descendant = $primaryKey; - $this->closure->depth = 0; - } - - if ($this->isDirty($this->getParentIdColumn())) { - $this->reorderSiblings(true); - $this->closure->moveNodeTo($this->parent_id); - } - } - } - - /** - * Clamp the position between 0 and the last position of the current parent. - */ - protected function clampPosition() - { - if (!$this->isDirty($this->getPositionColumn())) { - return; - } - $newPosition = max(0, min($this->position, $this->getNextAfterLastPosition())); - $this->attributes[$this->getPositionColumn()] = $newPosition; - } - - /** - * Deletes a subtree from database. - * - * @param bool $withSelf - * @param bool $forceDelete - * @return void - */ - public function deleteSubtree($withSelf = false, $forceDelete = false) - { - $action = ($forceDelete === true ? 'forceDelete' : 'delete'); - - $ids = $this->joinClosureBy('descendant', $withSelf)->pluck($this->getQualifiedKeyName()); - - if ($forceDelete) { - $this->closure->whereIn($this->closure->getDescendantColumn(), $ids)->delete(); - } - - $this->whereIn($this->getKeyName(), $ids)->$action(); - } - - /** - * Create a new Eloquent Collection instance. - * - * @param array $models - * @return \Illuminate\Database\Eloquent\Collection - */ - public function newCollection(array $models = array()) - { - return new Collection($models); - } - - /** - * Get a new query builder instance for the connection. - * - * @return QueryBuilder - */ - protected function newBaseQueryBuilder() - { - $conn = $this->getConnection(); - $grammar = $conn->getQueryGrammar(); - - return new QueryBuilder($conn, $grammar, $conn->getPostProcessor()); - } -} diff --git a/src/Franzose/ClosureTable/Generators/Generator.php b/src/Generators/Generator.php similarity index 100% rename from src/Franzose/ClosureTable/Generators/Generator.php rename to src/Generators/Generator.php diff --git a/src/Franzose/ClosureTable/Generators/Migration.php b/src/Generators/Migration.php similarity index 66% rename from src/Franzose/ClosureTable/Generators/Migration.php rename to src/Generators/Migration.php index 256a373..72bae95 100644 --- a/src/Franzose/ClosureTable/Generators/Migration.php +++ b/src/Generators/Migration.php @@ -11,11 +11,6 @@ */ class Migration extends Generator { - /** - * @var array - */ - private $usedTimestamps = []; - /** * Creates migration files. * @@ -26,19 +21,19 @@ public function create(array $options) { $entityClass = $this->getClassName($options['entity-table']); $closureClass = $this->getClassName($options['closure-table']); - $useInnoDB = $options['use-innodb']; - $stubPrefix = $useInnoDB ? '-innodb' : ''; + $innoDb = $options['use-innodb'] ? "\n" . ' $table->engine = \'InnoDB\';' : ''; $path = $this->getPath($options['entity-table'], $options['migrations-path']); - $stub = $this->getStub('migration' . $stubPrefix, 'migrations'); + $stub = $this->getStub('migration', 'migrations'); $this->filesystem->put( $path, $this->parseStub($stub, [ - 'entity_table' => $options['entity-table'], - 'entity_class' => $entityClass, - 'closure_table' => $options['closure-table'], - 'closure_class' => $closureClass, + 'entity_table' => $options['entity-table'], + 'entity_class' => $entityClass, + 'closure_table' => $options['closure-table'], + 'closure_class' => $closureClass, + 'innodb' => $innoDb ]) ); @@ -76,13 +71,6 @@ protected function getClassName($name) */ protected function getPath($name, $path) { - $timestamp = Carbon::now(); - - if (in_array($timestamp, $this->usedTimestamps)) { - $timestamp->addSecond(); - } - $this->usedTimestamps[] = $timestamp; - - return $path . '/' . $timestamp->format('Y_m_d_His') . '_' . $this->getName($name) . '_migration.php'; + return $path . '/' . Carbon::now()->format('Y_m_d_His') . '_' . $this->getName($name) . '_migration.php'; } } diff --git a/src/Franzose/ClosureTable/Generators/Model.php b/src/Generators/Model.php similarity index 52% rename from src/Franzose/ClosureTable/Generators/Model.php rename to src/Generators/Model.php index 791f065..00122b0 100644 --- a/src/Franzose/ClosureTable/Generators/Model.php +++ b/src/Generators/Model.php @@ -2,6 +2,7 @@ namespace Franzose\ClosureTable\Generators; use Franzose\ClosureTable\Extensions\Str as ExtStr; +use Illuminate\Support\Str; /** * ClosureTable specific models generator class. @@ -20,33 +21,26 @@ public function create(array $options) { $paths = []; - $nsplaceholder = (!empty($options['namespace']) ? "namespace " . $options['namespace'] . ";" : ''); + $nsplaceholder = !empty($options['namespace']) + ? sprintf('namespace %s;', $options['namespace']) + : ''; - $closureInterface = $options['closure'] . 'Interface'; $qualifiedEntityName = $options['entity']; - $qualifiedEntityInterfaceName = $qualifiedEntityName . 'Interface'; $qualifiedClosureName = $options['closure']; - $qualifiedClosureInterfaceName = $qualifiedClosureName . 'Interface'; // First, we make entity classes $paths[] = $path = $this->getPath($qualifiedEntityName, $options['models-path']); $stub = $this->getStub('entity', 'models'); + $closureClass = ucfirst($options['closure']); + $namespaceWithDelimiter = $options['namespace'] . '\\'; $this->filesystem->put($path, $this->parseStub($stub, [ 'namespace' => $nsplaceholder, - 'entity_class' => $options['entity'], + 'entity_class' => ucfirst($options['entity']), 'entity_table' => $options['entity-table'], - 'closure_class' => $options['namespace'] . '\\' . $options['closure'], - 'closure_class_short' => $options['closure'], - 'closure_interface' => $closureInterface - ])); - - $paths[] = $path = $this->getPath($qualifiedEntityInterfaceName, $options['models-path']); - $stub = $this->getStub('entityinterface', 'models'); - - $this->filesystem->put($path, $this->parseStub($stub, [ - 'namespace' => $nsplaceholder, - 'entity_class' => $options['entity'] + 'closure_class' => Str::startsWith($closureClass, $namespaceWithDelimiter) + ? $closureClass + : $namespaceWithDelimiter . $closureClass, ])); // Second, we make closure classes @@ -55,19 +49,10 @@ public function create(array $options) $this->filesystem->put($path, $this->parseStub($stub, [ 'namespace' => $nsplaceholder, - 'closure_class' => $options['closure'], + 'closure_class' => $closureClass, 'closure_table' => $options['closure-table'] ])); - - $paths[] = $path = $this->getPath($qualifiedClosureInterfaceName, $options['models-path']); - $stub = $this->getStub('closuretableinterface', 'models'); - - $this->filesystem->put($path, $this->parseStub($stub, [ - 'namespace' => $nsplaceholder, - 'closure_class' => $options['closure'] - ])); - return $paths; } @@ -80,6 +65,11 @@ public function create(array $options) */ protected function getPath($name, $path) { - return $path . '/' . ExtStr::classify($name) . '.php'; + $delimpos = strrpos($name, '\\'); + $filename = $delimpos === false + ? ExtStr::classify($name) + : substr(ExtStr::classify($name), $delimpos + 1); + + return $path . '/' . $filename . '.php'; } } diff --git a/src/Franzose/ClosureTable/Generators/stubs/migrations/migration.php b/src/Generators/stubs/migrations/migration.php similarity index 79% rename from src/Franzose/ClosureTable/Generators/stubs/migrations/migration.php rename to src/Generators/stubs/migrations/migration.php index deef709..b2df114 100644 --- a/src/Franzose/ClosureTable/Generators/stubs/migrations/migration.php +++ b/src/Generators/stubs/migrations/migration.php @@ -11,13 +11,13 @@ public function up() $table->increments('id'); $table->integer('parent_id')->unsigned()->nullable(); $table->integer('position', false, true); - $table->integer('real_depth', false, true); $table->softDeletes(); $table->foreign('parent_id') ->references('id') ->on('{{entity_table}}') ->onDelete('set null'); +{{innodb}} }); Schema::create('{{closure_table}}', function (Blueprint $table) { @@ -36,17 +36,13 @@ public function up() ->references('id') ->on('{{entity_table}}') ->onDelete('cascade'); +{{innodb}} }); } public function down() { - Schema::table('{{closure_table}}', function (Blueprint $table) { - Schema::dropIfExists('{{closure_table}}'); - }); - - Schema::table('{{entity_table}}', function (Blueprint $table) { - Schema::dropIfExists('{{entity_table}}'); - }); + Schema::dropIfExists('{{closure_table}}'); + Schema::dropIfExists('{{entity_table}}'); } } diff --git a/src/Franzose/ClosureTable/Generators/stubs/models/closuretable.php b/src/Generators/stubs/models/closuretable.php similarity index 70% rename from src/Franzose/ClosureTable/Generators/stubs/models/closuretable.php rename to src/Generators/stubs/models/closuretable.php index 406015f..001deef 100644 --- a/src/Franzose/ClosureTable/Generators/stubs/models/closuretable.php +++ b/src/Generators/stubs/models/closuretable.php @@ -3,7 +3,7 @@ use Franzose\ClosureTable\Models\ClosureTable; -class {{closure_class}} extends ClosureTable implements {{closure_class}}Interface +class {{closure_class}} extends ClosureTable { /** * The table associated with the model. diff --git a/src/Franzose/ClosureTable/Generators/stubs/models/entity.php b/src/Generators/stubs/models/entity.php similarity index 73% rename from src/Franzose/ClosureTable/Generators/stubs/models/entity.php rename to src/Generators/stubs/models/entity.php index e201f7b..e167ffd 100644 --- a/src/Franzose/ClosureTable/Generators/stubs/models/entity.php +++ b/src/Generators/stubs/models/entity.php @@ -3,7 +3,7 @@ use Franzose\ClosureTable\Models\Entity; -class {{entity_class}} extends Entity implements {{entity_class}}Interface +class {{entity_class}} extends Entity { /** * The table associated with the model. @@ -15,7 +15,7 @@ class {{entity_class}} extends Entity implements {{entity_class}}Interface /** * ClosureTable model instance. * - * @var {{closure_class_short}} + * @var \{{closure_class}} */ protected $closure = '{{closure_class}}'; } diff --git a/src/Franzose/ClosureTable/Models/ClosureTable.php b/src/Models/ClosureTable.php similarity index 72% rename from src/Franzose/ClosureTable/Models/ClosureTable.php rename to src/Models/ClosureTable.php index 654ef8e..0f3ded3 100644 --- a/src/Franzose/ClosureTable/Models/ClosureTable.php +++ b/src/Models/ClosureTable.php @@ -1,15 +1,14 @@ selectRowsToInsert($ancestorId, $descendantId); + + if (count($rows) > 0) { + $this->insert($rows); + } + } + + private function selectRowsToInsert($ancestorId, $descendantId) { $table = $this->getPrefixedTable(); $ancestor = $this->getAncestorColumn(); $descendant = $this->getDescendantColumn(); $depth = $this->getDepthColumn(); - $query = " - INSERT INTO {$table} ({$ancestor}, {$descendant}, {$depth}) - SELECT tbl.{$ancestor}, {$descendantId}, tbl.{$depth}+1 + $select = " + SELECT tbl.{$ancestor} AS ancestor, ? AS descendant, tbl.{$depth}+1 AS depth FROM {$table} AS tbl - WHERE tbl.{$descendant} = {$ancestorId} + WHERE tbl.{$descendant} = ? UNION ALL - SELECT {$descendantId}, {$descendantId}, 0 + SELECT ? AS ancestor, ? AS descendant, 0 AS depth "; - DB::connection($this->connection)->statement($query); + $rows = $this->getConnection()->select($select, [ + $descendantId, + $ancestorId, + $descendantId, + $descendantId + ]); + + return array_map(static function ($row) { + return (array) $row; + }, $rows); } /** * Make a node a descendant of another ancestor or makes it a root node. * - * @param int $ancestorId + * @param mixed $ancestorId * @return void - * @throws \InvalidArgumentException */ public function moveNodeTo($ancestorId = null) { @@ -78,11 +92,8 @@ public function moveNodeTo($ancestorId = null) $descendant = $this->getDescendantColumn(); $depth = $this->getDepthColumn(); - $thisAncestorId = $this->ancestor; - $thisDescendantId = $this->descendant; - // Prevent constraint collision - if (!is_null($ancestorId) && $thisAncestorId === $ancestorId) { + if ($ancestorId !== null && $this->ancestor === $ancestorId) { return; } @@ -91,7 +102,7 @@ public function moveNodeTo($ancestorId = null) // Since we have already unbound the node relationships, // given null ancestor id, we have nothing else to do, // because now the node is already root. - if (is_null($ancestorId)) { + if ($ancestorId === null) { return; } @@ -100,11 +111,14 @@ public function moveNodeTo($ancestorId = null) SELECT supertbl.{$ancestor}, subtbl.{$descendant}, supertbl.{$depth}+subtbl.{$depth}+1 FROM {$table} as supertbl CROSS JOIN {$table} as subtbl - WHERE supertbl.{$descendant} = {$ancestorId} - AND subtbl.{$ancestor} = {$thisDescendantId} + WHERE supertbl.{$descendant} = ? + AND subtbl.{$ancestor} = ? "; - DB::connection($this->connection)->statement($query); + $this->getConnection()->statement($query, [ + $ancestorId, + $this->descendant + ]); } /** @@ -117,26 +131,29 @@ protected function unbindRelationships() $table = $this->getPrefixedTable(); $ancestorColumn = $this->getAncestorColumn(); $descendantColumn = $this->getDescendantColumn(); - $descendant = $this->descendant; $query = " DELETE FROM {$table} WHERE {$descendantColumn} IN ( SELECT d FROM ( - SELECT {$descendantColumn} as d FROM {$table} - WHERE {$ancestorColumn} = {$descendant} - ) as dct + SELECT {$descendantColumn} AS d FROM {$table} + WHERE {$ancestorColumn} = ? + ) AS dct ) AND {$ancestorColumn} IN ( SELECT a FROM ( SELECT {$ancestorColumn} AS a FROM {$table} - WHERE {$descendantColumn} = {$descendant} - AND {$ancestorColumn} <> {$descendant} - ) as ct + WHERE {$descendantColumn} = ? + AND {$ancestorColumn} <> ? + ) AS ct ) "; - DB::connection($this->connection)->delete($query); + $this->getConnection()->delete($query, [ + $this->descendant, + $this->descendant, + $this->descendant + ]); } /** @@ -146,7 +163,7 @@ protected function unbindRelationships() */ public function getPrefixedTable() { - return DB::connection($this->connection)->getTablePrefix() . $this->getTable(); + return $this->getConnection()->getTablePrefix() . $this->getTable(); } /** @@ -166,7 +183,7 @@ public function getAncestorAttribute() */ public function setAncestorAttribute($value) { - $this->attributes[$this->getAncestorColumn()] = intval($value); + $this->attributes[$this->getAncestorColumn()] = $value; } /** @@ -206,7 +223,7 @@ public function getDescendantAttribute() */ public function setDescendantAttribute($value) { - $this->attributes[$this->getDescendantColumn()] = intval($value); + $this->attributes[$this->getDescendantColumn()] = $value; } /** @@ -246,7 +263,7 @@ public function getDepthAttribute() */ public function setDepthAttribute($value) { - $this->attributes[$this->getDepthColumn()] = intval($value); + $this->attributes[$this->getDepthColumn()] = (int) $value; } /** diff --git a/src/Models/Entity.php b/src/Models/Entity.php new file mode 100644 index 0000000..e73b0ae --- /dev/null +++ b/src/Models/Entity.php @@ -0,0 +1,1871 @@ +pos or $this->position. + * + * @property int position Alias for the current position attribute name + * @property int parent_id Alias for the direct ancestor identifier attribute name + * @property Collection children Child nodes loaded from the database + * @method Builder ancestors() + * @method Builder ancestorsOf($id) + * @method Builder ancestorsWithSelf() + * @method Builder ancestorsWithSelfOf($id) + * @method Builder descendants() + * @method Builder descendantsOf($id) + * @method Builder descendantsWithSelf() + * @method Builder descendantsWithSelfOf($id) + * @method Builder childNode() + * @method Builder childNodeOf($id) + * @method Builder childAt(int $position) + * @method Builder childOf($id, int $position) + * @method Builder firstChild() + * @method Builder firstChildOf($id) + * @method Builder lastChild() + * @method Builder lastChildOf($id) + * @method Builder childrenRange(int $from, int $to = null) + * @method Builder childrenRangeOf($id, int $from, int $to = null) + * @method Builder sibling() + * @method Builder siblingOf($id) + * @method Builder siblings() + * @method Builder siblingsOf($id) + * @method Builder neighbors() + * @method Builder neighborsOf($id) + * @method Builder siblingAt(int $position) + * @method Builder siblingOfAt($id, int $position) + * @method Builder firstSibling() + * @method Builder firstSiblingOf($id) + * @method Builder lastSibling() + * @method Builder lastSiblingOf($id) + * @method Builder prevSibling() + * @method Builder prevSiblingOf($id) + * @method Builder prevSiblings() + * @method Builder prevSiblingsOf($id) + * @method Builder nextSibling() + * @method Builder nextSiblingOf($id) + * @method Builder nextSiblings() + * @method Builder nextSiblingsOf($id) + * @method Builder siblingsRange(int $from, int $to = null) + * @method Builder siblingsRangeOf($id, int $from, int $to = null) + * + * @package Franzose\ClosureTable + */ +class Entity extends Eloquent implements EntityInterface +{ + /** + * ClosureTable model instance. + * + * @var ClosureTable + */ + protected $closure = ClosureTable::class; + + /** + * Cached "previous" (i.e. before the model is moved) direct ancestor id of this model. + * + * @var int + */ + private $previousParentId; + + /** + * Cached "previous" (i.e. before the model is moved) model position. + * + * @var int + */ + private $previousPosition; + + /** + * Whether this node is being moved to another parent node. + * + * @var bool + */ + private $isMoved = false; + + /** + * Indicates if the model should soft delete. + * + * @var bool + */ + protected $softDelete = true; + + /** + * Indicates if the model should be timestamped. + * + * @var bool + */ + public $timestamps = false; + + /** + * Entity constructor. + * + * @param array $attributes + */ + public function __construct(array $attributes = []) + { + $position = $this->getPositionColumn(); + + $this->fillable(array_merge($this->getFillable(), [$position])); + + if (isset($attributes[$position]) && $attributes[$position] < 0) { + $attributes[$position] = 0; + } + + $this->closure = new $this->closure; + + // The default class name of the closure table was not changed + // so we define and set default closure table name automagically. + // This can prevent useless copy paste of closure table models. + if (get_class($this->closure) === ClosureTable::class) { + $table = $this->getTable() . '_closure'; + $this->closure->setTable($table); + } + + parent::__construct($attributes); + } + + public function newFromBuilder($attributes = [], $connection = null) + { + $instance = parent::newFromBuilder($attributes); + $instance->previousParentId = $instance->parent_id; + $instance->previousPosition = $instance->position; + return $instance; + } + + /** + * Gets value of the "parent id" attribute. + * + * @return int + */ + public function getParentIdAttribute() + { + return $this->getAttributeFromArray($this->getParentIdColumn()); + } + + /** + * Sets new parent id and caches the old one. + * + * @param int $value + */ + public function setParentIdAttribute($value) + { + if ($this->parent_id === $value) { + return; + } + + $parentId = $this->getParentIdColumn(); + $this->previousParentId = isset($this->original[$parentId]) ? $this->original[$parentId] : null; + $this->attributes[$parentId] = $value; + } + + /** + * Gets the fully qualified "parent id" column. + * + * @return string + */ + public function getQualifiedParentIdColumn() + { + return $this->getTable() . '.' . $this->getParentIdColumn(); + } + + /** + * Gets the short name of the "parent id" column. + * + * @return string + */ + public function getParentIdColumn() + { + return 'parent_id'; + } + + /** + * Gets value of the "position" attribute. + * + * @return int + */ + public function getPositionAttribute() + { + return $this->getAttributeFromArray($this->getPositionColumn()); + } + + /** + * Sets new position and caches the old one. + * + * @param int $value + */ + public function setPositionAttribute($value) + { + if ($this->position === $value) { + return; + } + + $position = $this->getPositionColumn(); + $this->previousPosition = isset($this->original[$position]) ? $this->original[$position] : null; + $this->attributes[$position] = max(0, (int) $value); + } + + /** + * Gets the fully qualified "position" column. + * + * @return string + */ + public function getQualifiedPositionColumn() + { + return $this->getTable() . '.' . $this->getPositionColumn(); + } + + /** + * Gets the short name of the "position" column. + * + * @return string + */ + public function getPositionColumn() + { + return 'position'; + } + + /** + * Gets the fully qualified "real depth" column. + * + * @return string + */ + public function getQualifiedRealDepthColumn() + { + return $this->getTable() . '.' . $this->getRealDepthColumn(); + } + + /** + * Gets the short name of the "real depth" column. + * + * @return string + * @deprecated since 6.0 + */ + public function getRealDepthColumn() + { + return 'real_depth'; + } + + /** + * Gets the "children" relation index. + * + * @return string + * @deprecated since 6.0 + */ + public function getChildrenRelationIndex() + { + return 'children'; + } + + /** + * The "booting" method of the model. + * + * @return void + */ + public static function boot() + { + parent::boot(); + + static::saving(static function (Entity $entity) { + if ($entity->isDirty($entity->getPositionColumn())) { + $latest = static::getLatestPosition($entity); + + if (!$entity->isMoved) { + $latest--; + } + + $entity->position = max(0, min($entity->position, $latest)); + } elseif (!$entity->exists) { + $entity->position = static::getLatestPosition($entity); + } + }); + + // When entity is created, the appropriate + // data will be put into the closure table. + static::created(static function (Entity $entity) { + $entity->previousParentId = null; + $entity->previousPosition = null; + + $descendant = $entity->getKey(); + $ancestor = isset($entity->parent_id) ? $entity->parent_id : $descendant; + + $entity->closure->insertNode($ancestor, $descendant); + }); + + static::saved(static function (Entity $entity) { + $parentIdChanged = $entity->isDirty($entity->getParentIdColumn()); + + if ($parentIdChanged || $entity->isDirty($entity->getPositionColumn())) { + $entity->reorderSiblings(); + } + + if ($entity->closure->ancestor === null) { + $primaryKey = $entity->getKey(); + $entity->closure->ancestor = $primaryKey; + $entity->closure->descendant = $primaryKey; + $entity->closure->depth = 0; + } + + if ($parentIdChanged) { + $entity->closure->moveNodeTo($entity->parent_id); + } + }); + } + + /** + * Indicates whether the model is a parent. + * + * @return bool + */ + public function isParent() + { + return $this->exists && $this->hasChildren(); + } + + /** + * Indicates whether the model has no ancestors. + * + * @return bool + */ + public function isRoot() + { + return $this->exists && $this->parent_id === null; + } + + /** + * Retrieves direct ancestor of a model. + * + * @param array $columns + * @return Entity|null + */ + public function getParent(array $columns = ['*']) + { + return $this->exists ? $this->find($this->parent_id, $columns) : null; + } + + /** + * Returns query builder for ancestors. + * + * @param Builder $builder + * + * @return Builder + */ + public function scopeAncestors(Builder $builder) + { + return $this->buildAncestorsQuery($builder, $this->getKey(), false); + } + + /** + * Returns query builder for ancestors of the node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopeAncestorsOf(Builder $builder, $id) + { + return $this->buildAncestorsQuery($builder, $id, false); + } + + /** + * Returns query builder for ancestors including the current node. + * + * @param Builder $builder + * + * @return Builder + */ + public function scopeAncestorsWithSelf(Builder $builder) + { + return $this->buildAncestorsQuery($builder, $this->getKey(), true); + } + + /** + * Returns query builder for ancestors of the node with given ID including that node also. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopeAncestorsWithSelfOf(Builder $builder, $id) + { + return $this->buildAncestorsQuery($builder, $id, true); + } + + /** + * Builds base ancestors query. + * + * @param Builder $builder + * @param mixed $id + * @param bool $withSelf + * + * @return Builder + */ + private function buildAncestorsQuery(Builder $builder, $id, $withSelf) + { + $depthOperator = $withSelf ? '>=' : '>'; + + return $builder + ->join( + $this->closure->getTable(), + $this->closure->getAncestorColumn(), + '=', + $this->getQualifiedKeyName() + ) + ->where($this->closure->getDescendantColumn(), '=', $id) + ->where($this->closure->getDepthColumn(), $depthOperator, 0); + } + + /** + * Retrieves all ancestors of a model. + * + * @param array $columns + * @return Collection + */ + public function getAncestors(array $columns = ['*']) + { + return $this->ancestors()->get($columns); + } + + /** + * Retrieves tree structured ancestors of a model. + * + * @param array $columns + * @return Collection + * @deprecated since 6.0, use {@link Collection::toTree()} instead + */ + public function getAncestorsTree(array $columns = ['*']) + { + return $this->getAncestors($columns)->toTree(); + } + + /** + * Retrieves ancestors applying given conditions. + * + * @param mixed $column + * @param mixed $operator + * @param mixed $value + * @param array $columns + * @return Collection + * @deprecated since 6.0, use {@link Entity::ancestors()} scope instead + */ + public function getAncestorsWhere($column, $operator = null, $value = null, array $columns = ['*']) + { + return $this->ancestors()->where($column, $operator, $value)->get($columns); + } + + /** + * Returns a number of model's ancestors. + * + * @return int + */ + public function countAncestors() + { + return $this->ancestors()->count(); + } + + /** + * Indicates whether a model has ancestors. + * + * @return bool + */ + public function hasAncestors() + { + return (bool) $this->countAncestors(); + } + + /** + * Returns query builder for descendants. + * + * @param Builder $builder + * @param bool $withSelf + * + * @return Builder + */ + public function scopeDescendants(Builder $builder) + { + return $this->buildDescendantsQuery($builder, $this->getKey(), false); + } + + /** + * Returns query builder for descendants of the node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopeDescendantsOf(Builder $builder, $id) + { + return $this->buildDescendantsQuery($builder, $id, false); + } + + /** + * Returns query builder for descendants including the current node. + * + * @param Builder $builder + * + * @return Builder + */ + public function scopeDescendantsWithSelf(Builder $builder) + { + return $this->buildDescendantsQuery($builder, $this->getKey(), true); + } + + /** + * Returns query builder for descendants including the current node of the given ID. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopeDescendantsWithSelfOf(Builder $builder, $id) + { + return $this->buildDescendantsQuery($builder, $id, true); + } + + /** + * Builds base descendants query. + * + * @param Builder $builder + * @param mixed $id + * @param bool $withSelf + * + * @return Builder + */ + private function buildDescendantsQuery(Builder $builder, $id, $withSelf) + { + $depthOperator = $withSelf ? '>=' : '>'; + + return $builder + ->join( + $this->closure->getTable(), + $this->closure->getDescendantColumn(), + '=', + $this->getQualifiedKeyName() + ) + ->where($this->closure->getAncestorColumn(), '=', $id) + ->where($this->closure->getDepthColumn(), $depthOperator, 0); + } + + /** + * Retrieves all descendants of a model. + * + * @param array $columns + * @return Collection + */ + public function getDescendants(array $columns = ['*']) + { + return $this->descendants()->get($columns); + } + + /** + * Retrieves tree structured descendants of a model. + * + * @param array $columns + * @return Collection + * @deprecated since 6.0, use {@link Collection::toTree()} instead + */ + public function getDescendantsTree(array $columns = ['*']) + { + return $this->getDescendants($columns)->toTree(); + } + + /** + * Retrieves descendants applying given conditions. + * + * @param mixed $column + * @param mixed $operator + * @param mixed $value + * @param array $columns + * @return Collection + * @deprecated since 6.0, use {@link Entity::descendants()} scope instead + */ + public function getDescendantsWhere($column, $operator = null, $value = null, array $columns = ['*']) + { + return $this->descendants()->where($column, $operator, $value)->get($columns); + } + + /** + * Returns a number of model's descendants. + * + * @return int + */ + public function countDescendants() + { + return $this->descendants()->count(); + } + + /** + * Indicates whether a model has descendants. + * + * @return bool + */ + public function hasDescendants() + { + return (bool) $this->countDescendants(); + } + + /** + * Returns one-to-many relationship to child nodes. + * + * @return HasMany + */ + public function children() + { + return $this->hasMany(get_class($this), $this->getParentIdColumn()); + } + + /** + * Retrieves all children of a model. + * + * @param array $columns + * + * @return Collection + */ + public function getChildren(array $columns = ['*']) + { + return $this->children()->get($columns); + } + + /** + * Returns a number of model's children. + * + * @return int + */ + public function countChildren() + { + return $this->children()->count(); + } + + /** + * Indicates whether a model has children. + * + * @return bool + */ + public function hasChildren() + { + return (bool) $this->countChildren(); + } + + /** + * Indicates whether a model has children as a relation. + * + * @return bool + * @deprecated from 6.0 + */ + public function hasChildrenRelation() + { + return $this->relationLoaded($this->getChildrenRelationIndex()); + } + + /** + * Returns query builder for child nodes. + * + * @param Builder $builder + * + * @return Builder + */ + public function scopeChildNode(Builder $builder) + { + return $this->scopeChildNodeOf($builder, $this->getKey()); + } + + /** + * Returns query builder for child nodes of the node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopeChildNodeOf(Builder $builder, $id) + { + $parentId = $this->getParentIdColumn(); + + return $builder + ->whereNotNull($parentId) + ->where($parentId, '=', $id); + } + + /** + * Returns query builder for a child at the given position. + * + * @param Builder $builder + * @param int $position + * + * @return Builder + */ + public function scopeChildAt(Builder $builder, $position) + { + return $this + ->scopeChildNode($builder) + ->where($this->getPositionColumn(), '=', $position); + } + + /** + * Returns query builder for a child at the given position of the node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * @param int $position + * + * @return Builder + */ + public function scopeChildOf(Builder $builder, $id, $position) + { + return $this + ->scopeChildNodeOf($builder, $id) + ->where($this->getPositionColumn(), '=', $position); + } + + /** + * Retrieves a child with given position. + * + * @param int $position + * @param array $columns + * @return Entity + */ + public function getChildAt($position, array $columns = ['*']) + { + return $this->childAt($position)->first($columns); + } + + /** + * Returns query builder for the first child node. + * + * @param Builder $builder + * + * @return Builder + */ + public function scopeFirstChild(Builder $builder) + { + return $this->scopeChildAt($builder, 0); + } + + /** + * Returns query builder for the first child node of the node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopeFirstChildOf(Builder $builder, $id) + { + return $this->scopeChildOf($builder, $id, 0); + } + + /** + * Retrieves the first child. + * + * @param array $columns + * @return Entity + */ + public function getFirstChild(array $columns = ['*']) + { + return $this->getChildAt(0, $columns); + } + + /** + * Returns query builder for the last child node. + * + * @param Builder $builder + * + * @return Builder + */ + public function scopeLastChild(Builder $builder) + { + return $this->scopeChildNode($builder)->orderByDesc($this->getPositionColumn()); + } + + /** + * Returns query builder for the last child node of the node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopeLastChildOf(Builder $builder, $id) + { + return $this->scopeChildNodeOf($builder, $id)->orderByDesc($this->getPositionColumn()); + } + + /** + * Retrieves the last child. + * + * @param array $columns + * @return Entity + */ + public function getLastChild(array $columns = ['*']) + { + return $this->lastChild()->first($columns); + } + + /** + * Returns query builder to child nodes in the range of the given positions. + * + * @param Builder $builder + * @param int $from + * @param int|null $to + * + * @return Builder + */ + public function scopeChildrenRange(Builder $builder, $from, $to = null) + { + $position = $this->getPositionColumn(); + $query = $this->scopeChildNode($builder)->where($position, '>=', $from); + + if ($to !== null) { + $query->where($position, '<=', $to); + } + + return $query; + } + + /** + * Returns query builder to child nodes in the range of the given positions for the node of the given ID. + * + * @param Builder $builder + * @param mixed $id + * @param int $from + * @param int|null $to + * + * @return Builder + */ + public function scopeChildrenRangeOf(Builder $builder, $id, $from, $to = null) + { + $position = $this->getPositionColumn(); + $query = $this->scopeChildNodeOf($builder, $id)->where($position, '>=', $from); + + if ($to !== null) { + $query->where($position, '<=', $to); + } + + return $query; + } + + /** + * Retrieves children within given positions range. + * + * @param int $from + * @param int $to + * @param array $columns + * @return Collection + */ + public function getChildrenRange($from, $to = null, array $columns = ['*']) + { + return $this->childrenRange($from, $to)->get($columns); + } + + /** + * Appends a child to the model. + * + * @param EntityInterface $child + * @param int $position + * @param bool $returnChild + * @return EntityInterface + */ + public function addChild(EntityInterface $child, $position = null, $returnChild = false) + { + if ($this->exists) { + $position = $position !== null ? $position : $this->getLatestChildPosition(); + + $child->moveTo($position, $this); + } + + return $returnChild === true ? $child : $this; + } + + /** + * Returns the latest child position. + * + * @return int + */ + private function getLatestChildPosition() + { + $lastChild = $this->lastChild()->first([$this->getPositionColumn()]); + + return $lastChild !== null ? $lastChild->position + 1 : 0; + } + + /** + * Appends a collection of children to the model. + * + * @param Entity[] $children + * @param int $from + * + * @return Entity + * @throws InvalidArgumentException + * @throws \Throwable + */ + public function addChildren(array $children, $from = null) + { + if (!$this->exists) { + return $this; + } + + $this->transactional(function () use (&$from, $children) { + foreach ($children as $child) { + $this->addChild($child, $from); + $from++; + } + }); + + return $this; + } + + /** + * Removes a model's child with given position. + * + * @param int $position + * @param bool $forceDelete + * + * @return $this + * @throws \Throwable + */ + public function removeChild($position = null, $forceDelete = false) + { + if (!$this->exists) { + return $this; + } + + $child = $this->getChildAt($position, [ + $this->getKeyName(), + $this->getParentIdColumn(), + $this->getPositionColumn() + ]); + + if ($child === null) { + return $this; + } + + $this->transactional(function () use ($child, $forceDelete) { + $action = ($forceDelete === true ? 'forceDelete' : 'delete'); + + $child->{$action}(); + + $child->nextSiblings()->decrement($this->getPositionColumn()); + }); + + return $this; + } + + /** + * Removes model's children within a range of positions. + * + * @param int $from + * @param int $to + * @param bool $forceDelete + * + * @return $this + * @throws InvalidArgumentException + * @throws \Throwable + */ + public function removeChildren($from, $to = null, $forceDelete = false) + { + if (!is_numeric($from) || ($to !== null && !is_numeric($to))) { + throw new InvalidArgumentException('`from` and `to` are the position boundaries. They must be of type int.'); + } + + if (!$this->exists) { + return $this; + } + + $this->transactional(function () use ($from, $to, $forceDelete) { + $action = ($forceDelete === true ? 'forceDelete' : 'delete'); + + $this->childrenRange($from, $to)->{$action}(); + + if ($to !== null) { + $this + ->childrenRange($to) + ->decrement($this->getPositionColumn(), $to - $from + 1); + } + }); + + return $this; + } + + /** + * Returns sibling query builder. + * + * @param Builder $builder + * + * @return Builder + */ + public function scopeSibling(Builder $builder) + { + return $builder->where($this->getParentIdColumn(), '=', $this->parent_id); + } + + /** + * Returns query builder for siblings of a node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopeSiblingOf(Builder $builder, $id) + { + return $this->buildSiblingQuery($builder, $id); + } + + /** + * Returns siblings query builder. + * + * @param Builder $builder + * + * @return Builder + */ + public function scopeSiblings(Builder $builder) + { + return $this + ->scopeSibling($builder) + ->where($this->getPositionColumn(), '<>', $this->position); + } + + /** + * Return query builder for siblings of a node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopeSiblingsOf(Builder $builder, $id) + { + return $this->buildSiblingQuery($builder, $id, function ($position) { + return function (Builder $builder) use ($position) { + $builder->where($this->getPositionColumn(), '<>', $position); + }; + }); + } + + /** + * Retrives all siblings of a model. + * + * @param array $columns + * + * @return Collection + */ + public function getSiblings(array $columns = ['*']) + { + return $this->siblings()->get($columns); + } + + /** + * Returns number of model's siblings. + * + * @return int + */ + public function countSiblings() + { + return $this->siblings()->count(); + } + + /** + * Indicates whether a model has siblings. + * + * @return bool + */ + public function hasSiblings() + { + return (bool) $this->countSiblings(); + } + + /** + * Returns neighbors query builder. + * + * @param Builder $builder + * + * @return Builder + */ + public function scopeNeighbors(Builder $builder) + { + $position = $this->position; + + return $this + ->scopeSiblings($builder) + ->whereIn($this->getPositionColumn(), [$position - 1, $position + 1]); + } + + /** + * Returns query builder for the neighbors of a node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopeNeighborsOf(Builder $builder, $id) + { + return $this->buildSiblingQuery($builder, $id, function ($position) { + return function (Builder $builder) use ($position) { + return $builder->whereIn($this->getPositionColumn(), [$position - 1, $position + 1]); + }; + }); + } + + /** + * Retrieves neighbors (immediate previous and immediate next models) of a model. + * + * @param array $columns + * + * @return Collection + */ + public function getNeighbors(array $columns = ['*']) + { + return $this->neighbors()->get($columns); + } + + /** + * Returns query builder for a sibling at the given position. + * + * @param Builder $builder + * @param int $position + * + * @return Builder + */ + public function scopeSiblingAt(Builder $builder, $position) + { + return $this + ->scopeSiblings($builder) + ->where($this->getPositionColumn(), '=', $position); + } + + /** + * Returns query builder for a sibling at the given position of a node of the given ID. + * + * @param Builder $builder + * @param mixed $id + * @param int $position + * + * @return Builder + */ + public function scopeSiblingOfAt(Builder $builder, $id, $position) + { + return $this + ->scopeSiblingOf($builder, $id) + ->where($this->getPositionColumn(), '=', $position); + } + + /** + * Retrieves a model's sibling with given position. + * + * @param int $position + * @param array $columns + * @return Entity + */ + public function getSiblingAt($position, array $columns = ['*']) + { + return $this->siblingAt($position)->first($columns); + } + + /** + * Returns query builder for the first sibling. + * + * @param Builder $builder + * + * @return Builder + */ + public function scopeFirstSibling(Builder $builder) + { + return $this->scopeSiblingAt($builder, 0); + } + + /** + * Returns query builder for the first sibling of a node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopeFirstSiblingOf(Builder $builder, $id) + { + return $this->scopeSiblingOfAt($builder, $id, 0); + } + + /** + * Retrieves the first model's sibling. + * + * @param array $columns + * @return Entity + */ + public function getFirstSibling(array $columns = ['*']) + { + return $this->getSiblingAt(0, $columns); + } + + /** + * Returns query builder for the last sibling. + * + * @param Builder $builder + * + * @return Builder + */ + public function scopeLastSibling(Builder $builder) + { + return $this->scopeSiblings($builder)->orderByDesc($this->getPositionColumn()); + } + + /** + * Returns query builder for the last sibling of a node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopeLastSiblingOf(Builder $builder, $id) + { + return $this + ->scopeSiblingOf($builder, $id) + ->orderByDesc($this->getPositionColumn()) + ->limit(1); + } + + /** + * Retrieves the last model's sibling. + * + * @param array $columns + * @return Entity + */ + public function getLastSibling(array $columns = ['*']) + { + return $this->lastSibling()->first($columns); + } + + /** + * Returns query builder for the previous sibling. + * + * @param Builder $builder + * + * @return Builder + */ + public function scopePrevSibling(Builder $builder) + { + return $this + ->scopeSibling($builder) + ->where($this->getPositionColumn(), '=', $this->position - 1); + } + + /** + * Returns query builder for the previous sibling of a node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopePrevSiblingOf(Builder $builder, $id) + { + return $this->buildSiblingQuery($builder, $id, function ($position) { + return function (Builder $builder) use ($position) { + return $builder->where($this->getPositionColumn(), '=', $position - 1); + }; + }); + } + + /** + * Retrieves immediate previous sibling of a model. + * + * @param array $columns + * @return Entity + */ + public function getPrevSibling(array $columns = ['*']) + { + return $this->prevSibling()->first($columns); + } + + /** + * Returns query builder for the previous siblings. + * + * @param Builder $builder + * + * @return Builder + */ + public function scopePrevSiblings(Builder $builder) + { + return $this + ->scopeSibling($builder) + ->where($this->getPositionColumn(), '<', $this->position); + } + + /** + * Returns query builder for the previous siblings of a node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopePrevSiblingsOf(Builder $builder, $id) + { + return $this->buildSiblingQuery($builder, $id, function ($position) { + return function (Builder $builder) use ($position) { + return $builder->where($this->getPositionColumn(), '<', $position); + }; + }); + } + + /** + * Retrieves all previous siblings of a model. + * + * @param array $columns + * + * @return Collection + */ + public function getPrevSiblings(array $columns = ['*']) + { + return $this->prevSiblings()->get($columns); + } + + /** + * Returns number of previous siblings of a model. + * + * @return int + */ + public function countPrevSiblings() + { + return $this->prevSiblings()->count(); + } + + /** + * Indicates whether a model has previous siblings. + * + * @return bool + */ + public function hasPrevSiblings() + { + return (bool) $this->countPrevSiblings(); + } + + /** + * Returns query builder for the next sibling. + * + * @param Builder $builder + * + * @return Builder + */ + public function scopeNextSibling(Builder $builder) + { + return $this + ->scopeSibling($builder) + ->where($this->getPositionColumn(), '=', $this->position + 1); + } + + /** + * Returns query builder for the next sibling of a node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopeNextSiblingOf(Builder $builder, $id) + { + return $this->buildSiblingQuery($builder, $id, function ($position) { + return function (Builder $builder) use ($position) { + return $builder->where($this->getPositionColumn(), '=', $position + 1); + }; + }); + } + + /** + * Retrieves immediate next sibling of a model. + * + * @param array $columns + * @return Entity + */ + public function getNextSibling(array $columns = ['*']) + { + return $this->nextSibling()->first($columns); + } + + /** + * Returns query builder for the next siblings. + * + * @param Builder $builder + * + * @return Builder + */ + public function scopeNextSiblings(Builder $builder) + { + return $this + ->scopeSibling($builder) + ->where($this->getPositionColumn(), '>', $this->position); + } + + /** + * Returns query builder for the next siblings of a node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * + * @return Builder + */ + public function scopeNextSiblingsOf(Builder $builder, $id) + { + return $this->buildSiblingQuery($builder, $id, function ($position) { + return function (Builder $builder) use ($position) { + return $builder->where($this->getPositionColumn(), '>', $position); + }; + }); + } + + /** + * Retrieves all next siblings of a model. + * + * @param array $columns + * + * @return Collection + */ + public function getNextSiblings(array $columns = ['*']) + { + return $this->nextSiblings()->get($columns); + } + + /** + * Returns number of next siblings of a model. + * + * @return int + */ + public function countNextSiblings() + { + return $this->nextSiblings()->count(); + } + + /** + * Indicates whether a model has next siblings. + * + * @return bool + */ + public function hasNextSiblings() + { + return (bool) $this->countNextSiblings(); + } + + /** + * Returns query builder for a range of siblings. + * + * @param Builder $builder + * @param int $from + * @param int|null $to + * + * @return Builder + */ + public function scopeSiblingsRange(Builder $builder, $from, $to = null) + { + $position = $this->getPositionColumn(); + + $query = $this + ->scopeSiblings($builder) + ->where($position, '>=', $from); + + if ($to !== null) { + $query->where($position, '<=', $to); + } + + return $query; + } + + /** + * Returns query builder for a range of siblings of a node with the given ID. + * + * @param Builder $builder + * @param mixed $id + * @param int $from + * @param int|null $to + * + * @return Builder + */ + public function scopeSiblingsRangeOf(Builder $builder, $id, $from, $to = null) + { + $position = $this->getPositionColumn(); + + $query = $this + ->buildSiblingQuery($builder, $id) + ->where($position, '>=', $from); + + if ($to !== null) { + $query->where($position, '<=', $to); + } + + return $query; + } + + /** + * Retrieves siblings within given positions range. + * + * @param int $from + * @param int $to + * @param array $columns + * @return Collection + */ + public function getSiblingsRange($from, $to = null, array $columns = ['*']) + { + return $this->siblingsRange($from, $to)->get($columns); + } + + /** + * Builds query for siblings. + * + * @param Builder $builder + * @param mixed $id + * @param callable|null $positionCallback + * + * @return Builder + */ + private function buildSiblingQuery(Builder $builder, $id, callable $positionCallback = null) + { + $parentIdColumn = $this->getParentIdColumn(); + $positionColumn = $this->getPositionColumn(); + + $entity = $this + ->select([$this->getKeyName(), $parentIdColumn, $positionColumn]) + ->from($this->getTable()) + ->where($this->getKeyName(), '=', $id) + ->limit(1) + ->first(); + + if ($entity === null) { + return $builder; + } + + if ($entity->parent_id === null) { + $builder->whereNull($parentIdColumn); + } else { + $builder->where($parentIdColumn, '=', $entity->parent_id); + } + + if (is_callable($positionCallback)) { + $builder->where($positionCallback($entity->position)); + } + + return $builder; + } + + /** + * Appends a sibling within the current depth. + * + * @param EntityInterface $sibling + * @param int|null $position + * @param bool $returnSibling + * @return EntityInterface + */ + public function addSibling(EntityInterface $sibling, $position = null, $returnSibling = false) + { + if ($this->exists) { + $position = $position === null ? static::getLatestPosition($this) : $position; + + $sibling->moveTo($position, $this->parent_id); + + if ($position < $this->position) { + $this->position++; + } + } + + return ($returnSibling === true ? $sibling : $this); + } + + /** + * Appends multiple siblings within the current depth. + * + * @param Entity[] $siblings + * @param int|null $from + * + * @return Entity + * @throws Throwable + */ + public function addSiblings(array $siblings, $from = null) + { + if (!$this->exists) { + return $this; + } + + $from = $from === null ? static::getLatestPosition($this) : $from; + + $this->transactional(function () use ($siblings, &$from) { + foreach ($siblings as $sibling) { + $this->addSibling($sibling, $from); + $from++; + } + }); + + return $this; + } + + /** + * Retrieves root (with no ancestors) models. + * + * @param array $columns + * + * @return Collection + */ + public static function getRoots(array $columns = ['*']) + { + /** + * @var Entity $instance + */ + $instance = new static; + + return $instance->whereNull($instance->getParentIdColumn())->get($columns); + } + + /** + * Makes model a root with given position. + * + * @param int $position + * @return $this + */ + public function makeRoot($position) + { + return $this->moveTo($position, null); + } + + /** + * Adds "parent id" column to columns list for proper tree querying. + * + * @param array $columns + * @return array + */ + protected function prepareTreeQueryColumns(array $columns) + { + return ($columns === ['*'] ? $columns : array_merge($columns, [$this->getParentIdColumn()])); + } + + /** + * Retrieves entire tree. + * + * @param array $columns + * + * @return Collection + * @deprecated since 6.0 + */ + public static function getTree(array $columns = ['*']) + { + /** + * @var Entity $instance + */ + $instance = new static; + + return $instance + ->load('children') + ->orderBy($instance->getParentIdColumn()) + ->orderBy($instance->getPositionColumn()) + ->get($instance->prepareTreeQueryColumns($columns)) + ->toTree(); + } + + /** + * Retrieves tree by condition. + * + * @param mixed $column + * @param mixed $operator + * @param mixed $value + * @param array $columns + * + * @return Collection + * @deprecated since 6.0 + */ + public static function getTreeWhere($column, $operator = null, $value = null, array $columns = ['*']) + { + /** + * @var Entity $instance + */ + $instance = new static; + $columns = $instance->prepareTreeQueryColumns($columns); + + return $instance->where($column, $operator, $value)->get($columns)->toTree(); + } + + /** + * Retrieves tree with any conditions using QueryBuilder + * + * @param Builder $query + * @param array $columns + * + * @return Collection + * @deprecated since 6.0 + */ + public static function getTreeByQuery(Builder $query, array $columns = ['*']) + { + /** + * @var Entity $instance + */ + $instance = new static; + $columns = $instance->prepareTreeQueryColumns($columns); + return $query->get($columns)->toTree(); + } + + /** + * Saves models from the given attributes array. + * + * @param array $tree + * @param EntityInterface $parent + * + * @return Collection + * @throws Throwable + */ + public static function createFromArray(array $tree, EntityInterface $parent = null) + { + $entities = []; + + foreach ($tree as $item) { + $children = Arr::pull($item, 'children'); + + /** + * @var Entity $entity + */ + $entity = new static($item); + $entity->parent_id = $parent ? $parent->getKey() : null; + $entity->save(); + + if ($children !== null) { + $entity->addChildren(static::createFromArray($children, $entity)->all()); + } + + $entities[] = $entity; + } + + return new Collection($entities); + } + + /** + * Makes the model a child or a root with given position. Do not use moveTo to move a node within the same ancestor (call position = value and save instead). + * + * @param int $position + * @param EntityInterface|int $ancestor + * @return Entity + * @throws InvalidArgumentException + */ + public function moveTo($position, $ancestor = null) + { + $parentId = $ancestor instanceof self ? $ancestor->getKey() : $ancestor; + + if ($this->parent_id === $parentId && $this->parent_id !== null) { + return $this; + } + + if ($this->getKey() === $parentId) { + throw new InvalidArgumentException('Target entity is equal to the sender.'); + } + + $this->parent_id = $parentId; + $this->position = $position; + + $this->isMoved = true; + $this->save(); + $this->isMoved = false; + + return $this; + } + + /** + * Gets the next sibling position after the last one. + * + * @param Entity $entity + * + * @return int + */ + public static function getLatestPosition(Entity $entity) + { + $positionColumn = $entity->getPositionColumn(); + $parentIdColumn = $entity->getParentIdColumn(); + + $latest = $entity->select($positionColumn) + ->where($parentIdColumn, '=', $entity->parent_id) + ->latest($positionColumn) + ->first(); + + $position = $latest !== null ? $latest->position : -1; + + return $position + 1; + } + + /** + * Reorders node's siblings when it is moved to another position or ancestor. + * + * @return void + */ + private function reorderSiblings() + { + $position = $this->getPositionColumn(); + + if ($this->previousPosition !== null) { + $this + ->where($this->getKeyName(), '<>', $this->getKey()) + ->where($this->getParentIdColumn(), '=', $this->previousParentId) + ->where($position, '>', $this->previousPosition) + ->decrement($position); + } + + $this + ->sibling() + ->where($this->getKeyName(), '<>', $this->getKey()) + ->where($position, '>=', $this->position) + ->increment($position); + } + + /** + * Deletes a subtree from database. + * + * @param bool $withSelf + * @param bool $forceDelete + * + * @return void + * @throws \Exception + */ + public function deleteSubtree($withSelf = false, $forceDelete = false) + { + $action = ($forceDelete === true ? 'forceDelete' : 'delete'); + + $query = $withSelf ? $this->descendantsWithSelf() : $this->descendants(); + $ids = $query->pluck($this->getKeyName()); + + if ($forceDelete) { + $this->closure->whereIn($this->closure->getDescendantColumn(), $ids)->delete(); + } + + $this->whereIn($this->getKeyName(), $ids)->$action(); + } + + /** + * Create a new Eloquent Collection instance. + * + * @param array $models + * @return \Illuminate\Database\Eloquent\Collection + */ + public function newCollection(array $models = array()) + { + return new Collection($models); + } + + /** + * Executes queries within a transaction. + * + * @param callable $callable + * + * @return mixed + * @throws Throwable + */ + private function transactional(callable $callable) + { + return $this->getConnection()->transaction($callable); + } +} diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index fd96049..59adf3a 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -2,103 +2,74 @@ namespace Franzose\ClosureTable\Tests; use DB; -use Event; +use Dotenv\Dotenv; +use Franzose\ClosureTable\Contracts\ClosureTableInterface; +use Franzose\ClosureTable\Contracts\EntityInterface; +use Franzose\ClosureTable\Models\ClosureTable; +use Illuminate\Contracts\Console\Kernel; +use Illuminate\Foundation\Application; use Orchestra\Testbench\TestCase; -use Mockery; use Franzose\ClosureTable\Models\Entity; -use Way\Tests\ModelHelpers; -/** - * Class BaseTestCase - * @package Franzose\ClosureTable\Tests - */ abstract class BaseTestCase extends TestCase { - use ModelHelpers; - - public static $debug = false; - public static $sqlite_in_memory = false; + const DATABASE_CONNECTION = 'closuretable'; public function setUp() { parent::setUp(); - $this->app->bind('Franzose\ClosureTable\Contracts\EntityInterface', 'Franzose\ClosureTable\Models\Entity'); - $this->app->bind('Franzose\ClosureTable\Contracts\ClosureTableInterface', 'Franzose\ClosureTable\Models\ClosureTable'); - - if (!static::$sqlite_in_memory) { - DB::statement('DROP TABLE IF EXISTS entities_closure'); - DB::statement('DROP TABLE IF EXISTS entities;'); - DB::statement('DROP TABLE IF EXISTS migrations'); - } + $this->app->setBasePath(__DIR__ . '/../'); + $this->app->bind(EntityInterface::class, Entity::class); + $this->app->bind(ClosureTableInterface::class, ClosureTable::class); - $artisan = $this->app->make('Illuminate\Contracts\Console\Kernel'); - $artisan->call('migrate', [ - '--database' => 'closuretable', - '--path' => '../tests/migrations' - ]); + $artisan = $this->app->make(Kernel::class); - $artisan->call('db:seed', [ - '--class' => 'Franzose\ClosureTable\Tests\Seeds\EntitiesSeeder' + $artisan->call('migrate:refresh', [ + '--database' => static::DATABASE_CONNECTION, + '--path' => 'tests/migrations', + '--seeder' => EntitiesSeeder::class ]); - - if (static::$debug) { - Entity::$debug = true; - Event::listen('illuminate.query', function ($sql, $bindings, $time) { - $sql = str_replace(array('%', '?'), array('%%', '%s'), $sql); - $full_sql = vsprintf($sql, $bindings); - echo PHP_EOL . '- BEGIN QUERY -' . PHP_EOL . $full_sql . PHP_EOL . '- END QUERY -' . PHP_EOL; - }); - } } public function tearDown() { - Mockery::close(); + // this is to avoid "too many connection" errors + DB::disconnect(static::DATABASE_CONNECTION); } /** - * @param \Illuminate\Foundation\Application $app + * @param Application $app */ protected function getEnvironmentSetUp($app) { - // reset base path to point to our package's src directory - $app['path.base'] = __DIR__ . '/../src'; + $envFilePath = __DIR__ . '/..'; - $app['config']->set('database.default', 'closuretable'); - - if (static::$sqlite_in_memory) { - $options = [ - 'driver' => 'sqlite', - 'database' => ':memory:', - 'prefix' => '', - ]; - } else { - $options = [ - 'driver' => 'mysql', - 'host' => 'localhost', - 'database' => 'closuretabletest', - 'username' => 'root', - 'password' => '', - 'prefix' => '', - 'charset' => 'utf8', - 'collation' => 'utf8_unicode_ci', - ]; + if (file_exists($envFilePath . '/.env.testing')) { + (new Dotenv($envFilePath, '.env.testing'))->load(); } - $app['config']->set('database.connections.closuretable', $options); + $app['config']->set('database.default', static::DATABASE_CONNECTION); + $app['config']->set('database.connections.' . static::DATABASE_CONNECTION, [ + 'driver' => env('DB_DRIVER', 'mysql'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT'), + 'database' => env('DB_NAME', static::DATABASE_CONNECTION . 'test'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD'), + 'prefix' => '', + 'charset' => 'utf8', + 'collation' => env('DB_COLLATION', 'utf8_unicode_ci'), + ]); } - /** - * Asserts if two arrays have similar values, sorting them before the fact in order to "ignore" ordering. - * @param array $actual - * @param array $expected - * @param string $message - * @param float $delta - * @param int $depth - */ - protected function assertArrayValuesEquals(array $actual, array $expected, $message = '', $delta = 0.0, $depth = 10) + public static function assertModelAttribute($attribute, array $expected) { - $this->assertEquals($actual, $expected, $message, $delta, $depth, true); + $actual = Entity::whereIn('id', array_keys($expected)) + ->get(['id', $attribute]) + ->pluck($attribute, 'id') + ->toArray(); + + static::assertEquals($expected, $actual); } } diff --git a/tests/ClosureTableTestCase.php b/tests/ClosureTableTestCase.php deleted file mode 100644 index aed22a1..0000000 --- a/tests/ClosureTableTestCase.php +++ /dev/null @@ -1,78 +0,0 @@ -ctable = new ClosureTable; - $this->ancestorColumn = $this->ctable->getAncestorColumn(); - $this->descendantColumn = $this->ctable->getDescendantColumn(); - $this->depthColumn = $this->ctable->getDepthColumn(); - } - - /** - * @expectedException \InvalidArgumentException - * @dataProvider insertNodeProvider - */ - public function testInsertNodeValidatesItsArguments($ancestorId, $descendantId) - { - $this->ctable->insertNode($ancestorId, $descendantId); - } - - public function insertNodeProvider() - { - return [ - ['wrong', 12], - [12, 'wrong'], - ['wrong', 'wrong'], - ]; - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testMoveNodeToValidatesItsArgument() - { - $this->ctable->moveNodeTo('wrong'); - } - - public function testAncestorQualifiedKeyName() - { - $this->assertEquals($this->ctable->getTable() . '.' . $this->ancestorColumn, $this->ctable->getQualifiedAncestorColumn()); - } - - public function testDescendantQualifiedKeyName() - { - $this->assertEquals($this->ctable->getTable() . '.' . $this->descendantColumn, $this->ctable->getQualifiedDescendantColumn()); - } - - public function testDepthQualifiedKeyName() - { - $this->assertEquals($this->ctable->getTable() . '.' . $this->depthColumn, $this->ctable->getQualifiedDepthColumn()); - } -} diff --git a/tests/CollectionTestCase.php b/tests/CollectionTestCase.php deleted file mode 100644 index 065a059..0000000 --- a/tests/CollectionTestCase.php +++ /dev/null @@ -1,67 +0,0 @@ -save(); - $childEntity = with(new Entity)->moveTo(0, $rootEntity); - $grandEntity = with(new Entity)->moveTo(0, $childEntity); - - $childrenRelationIndex = $rootEntity->getChildrenRelationIndex(); - - $tree = with(new Collection([$rootEntity, $childEntity, $grandEntity]))->toTree(); - $rootItem = $tree->get(0); - - $this->assertArrayHasKey($childrenRelationIndex, $rootItem->getRelations()); - - $children = $rootItem->getRelation($childrenRelationIndex); - - $this->assertCount(1, $children); - - $childItem = $children->get(0); - - $this->assertEquals($childEntity->getKey(), $childItem->getKey()); - $this->assertArrayHasKey($childrenRelationIndex, $childItem->getRelations()); - - $grandItems = $childItem->getRelation($childrenRelationIndex); - - $this->assertCount(1, $grandItems); - - $grandItem = $grandItems->get(0); - - $this->assertEquals($grandEntity->getKey(), $grandItem->getKey()); - $this->assertArrayNotHasKey($childrenRelationIndex, $grandItem->getRelations()); - } - - public function testHasChildren() - { - $entity = new Entity; - $childrenRelationIndex = $entity->getChildrenRelationIndex(); - - $collection = new Collection([$entity, new Entity, new Entity]); - $collection->get(0)->setRelation($childrenRelationIndex, new Collection([new Entity, new Entity, new Entity])); - - $this->assertTrue($collection->hasChildren(0)); - } - - public function testGetChildrenOf() - { - $entity = new Entity; - $childrenRelationIndex = $entity->getChildrenRelationIndex(); - - $collection = new Collection([$entity, new Entity, new Entity]); - $collection->get(0)->setRelation($childrenRelationIndex, new Collection([new Entity, new Entity, new Entity])); - - $children = $collection->getChildrenOf(0); - - $this->assertInstanceOf('Franzose\ClosureTable\Extensions\Collection', $children); - $this->assertCount(3, $children); - } -} diff --git a/tests/Console/ComposerStub.php b/tests/Console/ComposerStub.php new file mode 100644 index 0000000..4b4f7c9 --- /dev/null +++ b/tests/Console/ComposerStub.php @@ -0,0 +1,29 @@ +getBasePath()), static function ($app) { + $app->bind( + LaravelLoadConfiguration::class, + TestbenchLoadConfiguration::class + ); + }); + } + + public function setUp() + { + parent::setUp(); + + $this->app->setBasePath(__DIR__); + $this->app->register(new ClosureTableServiceProvider($this->app)); + $this->app->bind(Composer::class, static function () { + return new ComposerStub(); + }); + + $this->artisan = $this->app->make(Kernel::class); + $this->modelsPath = $this->app->path(); + $this->migrationsPath = $this->app->databasePath('migrations'); + } + + public function testCommandMustRequireEntityName() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Not enough arguments (missing: "entity").'); + + $this->artisan->call('closuretable:make'); + } + + public function testCommandShouldGenerateFilesAtDefaultPaths() + { + Carbon::setTestNow('2020-04-17 00:00:00'); + + $this->artisan->call('closuretable:make', [ + 'entity' => 'Foo', + '--namespace' => 'Foo', + '--entity-table' => 'foo', + '--closure' => 'FooTree', + '--closure-table' => 'foo_tree', + '--use-innodb' => true + ]); + + Carbon::setTestNow(); + + $actualFoo = $this->modelsPath . '/Foo.php'; + $actualFooClosure = $this->modelsPath . '/FooTree.php'; + $expectedFoo = $this->modelsPath . '/expectedFoo.php'; + $expectedFooClosure = $this->modelsPath . '/expectedFooTree.php'; + $expectedMigrationPath = $this->migrationsPath . '/expectedFooMigration.php'; + $actualMigrationPath = $this->migrationsPath . '/2020_04_17_000000_create_foos_table_migration.php'; + + static::assertFileExists($actualFoo); + static::assertFileExists($actualFooClosure); + static::assertFileEquals($expectedFoo, $actualFoo); + static::assertFileEquals($expectedFooClosure, $actualFooClosure); + static::assertFileExists($actualMigrationPath); + static::assertFileEquals($expectedMigrationPath, $actualMigrationPath); + + unlink($actualFoo); + unlink($actualFooClosure); + unlink($actualMigrationPath); + } + + public function testCommandShouldGenerateFilesAtCustomPaths() + { + $filesystem = new Filesystem(); + + $customModelsPath = $this->modelsPath . '/custom'; + $customMigrationsPath = $this->migrationsPath . '/custom'; + + if (!$filesystem->exists($customModelsPath)) { + $filesystem->makeDirectory($customModelsPath); + } + + if (!$filesystem->exists($customMigrationsPath)) { + $filesystem->makeDirectory($customMigrationsPath); + } + + Carbon::setTestNow('2020-04-17 00:00:00'); + + $this->artisan->call('closuretable:make', [ + 'entity' => 'Foo', + '--namespace' => 'Foo', + '--entity-table' => 'foo', + '--closure' => 'FooTree', + '--closure-table' => 'foo_tree', + '--models-path' => $customModelsPath, + '--migrations-path' => $customMigrationsPath + ]); + + Carbon::setTestNow(); + + $models = $filesystem->files($customModelsPath); + $migrations = $filesystem->files($customMigrationsPath); + + static::assertCount(2, $models); + static::assertCount(1, $migrations); + + $filesystem->deleteDirectory($customModelsPath); + $filesystem->deleteDirectory($customMigrationsPath); + } + + public function testCommandShouldHandleNamespacedModelNames() + { + Carbon::setTestNow('2020-04-17 00:00:00'); + + $this->artisan->call('closuretable:make', [ + 'entity' => 'Foo\\Bar', + '--closure' => 'Foo\\BarTree', + ]); + + Carbon::setTestNow(); + + $actualFoo = $this->modelsPath . '/Bar.php'; + $actualFooClosure = $this->modelsPath . '/BarTree.php'; + $expectedFoo = $this->modelsPath . '/expectedBar.php'; + $expectedFooClosure = $this->modelsPath . '/expectedBarTree.php'; + $expectedMigrationPath = $this->migrationsPath . '/expectedBarMigration.php'; + $actualMigrationPath = $this->migrationsPath . '/2020_04_17_000000_create_bars_table_migration.php'; + + static::assertFileExists($actualFoo); + static::assertFileExists($actualFooClosure); + static::assertFileEquals($expectedFoo, $actualFoo); + static::assertFileEquals($expectedFooClosure, $actualFooClosure); + static::assertFileExists($actualMigrationPath); + static::assertFileEquals($expectedMigrationPath, $actualMigrationPath); + + unlink($actualFoo); + unlink($actualFooClosure); + unlink($actualMigrationPath); + } +} diff --git a/tests/Console/app/expectedBar.php b/tests/Console/app/expectedBar.php new file mode 100644 index 0000000..8a62681 --- /dev/null +++ b/tests/Console/app/expectedBar.php @@ -0,0 +1,21 @@ +increments('id'); + $table->integer('parent_id')->unsigned()->nullable(); + $table->integer('position', false, true); + $table->softDeletes(); + + $table->foreign('parent_id') + ->references('id') + ->on('bars') + ->onDelete('set null'); + + }); + + Schema::create('bar_tree', function (Blueprint $table) { + $table->increments('closure_id'); + + $table->integer('ancestor', false, true); + $table->integer('descendant', false, true); + $table->integer('depth', false, true); + + $table->foreign('ancestor') + ->references('id') + ->on('bars') + ->onDelete('cascade'); + + $table->foreign('descendant') + ->references('id') + ->on('bars') + ->onDelete('cascade'); + + }); + } + + public function down() + { + Schema::dropIfExists('bar_tree'); + Schema::dropIfExists('bars'); + } +} diff --git a/src/Franzose/ClosureTable/Generators/stubs/migrations/migration-innodb.php b/tests/Console/database/migrations/expectedFooMigration.php similarity index 61% rename from src/Franzose/ClosureTable/Generators/stubs/migrations/migration-innodb.php rename to tests/Console/database/migrations/expectedFooMigration.php index 46a42d4..c33137f 100644 --- a/src/Franzose/ClosureTable/Generators/stubs/migrations/migration-innodb.php +++ b/tests/Console/database/migrations/expectedFooMigration.php @@ -3,53 +3,48 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; -class {{entity_class}}Migration extends Migration +class CreateFoosTableMigration extends Migration { public function up() { - Schema::create('{{entity_table}}', function (Blueprint $table) { - $table->engine = 'InnoDB'; - + Schema::create('foo', function (Blueprint $table) { $table->increments('id'); $table->integer('parent_id')->unsigned()->nullable(); $table->integer('position', false, true); - $table->integer('real_depth', false, true); $table->softDeletes(); $table->foreign('parent_id') ->references('id') - ->on('{{entity_table}}') + ->on('foo') ->onDelete('set null'); - }); - Schema::create('{{closure_table}}', function (Blueprint $table) { $table->engine = 'InnoDB'; + }); + Schema::create('foo_tree', function (Blueprint $table) { $table->increments('closure_id'); + $table->integer('ancestor', false, true); $table->integer('descendant', false, true); $table->integer('depth', false, true); $table->foreign('ancestor') ->references('id') - ->on('{{entity_table}}') + ->on('foo') ->onDelete('cascade'); $table->foreign('descendant') ->references('id') - ->on('{{entity_table}}') + ->on('foo') ->onDelete('cascade'); + + $table->engine = 'InnoDB'; }); } public function down() { - Schema::table('{{closure_table}}', function (Blueprint $table) { - Schema::dropIfExists('{{closure_table}}'); - }); - - Schema::table('{{entity_table}}', function (Blueprint $table) { - Schema::dropIfExists('{{entity_table}}'); - }); + Schema::dropIfExists('foo_tree'); + Schema::dropIfExists('foo'); } } diff --git a/tests/seeds/EntitiesSeeder.php b/tests/EntitiesSeeder.php similarity index 60% rename from tests/seeds/EntitiesSeeder.php rename to tests/EntitiesSeeder.php index 3482da6..9c1ca51 100644 --- a/tests/seeds/EntitiesSeeder.php +++ b/tests/EntitiesSeeder.php @@ -1,5 +1,5 @@ 15 foreach (range(0, 8) as $idx) { - DB::insert($entitiesSql, [$idx + 1, null, 'The title', 'The excerpt', 'The body', $idx, 0]); + DB::insert($entitiesSql, [null, 'The title', 'The excerpt', 'The body', $idx]); DB::insert($closuresSql, [$idx + 1, $idx + 1, 0]); } - DB::insert($entitiesSql, [10, 9, 'The title', 'The excerpt', 'The body', 0, 1]); - DB::insert($entitiesSql, [11, 10, 'The title', 'The excerpt', 'The body', 0, 2]); - DB::insert($entitiesSql, [12, 11, 'The title', 'The excerpt', 'The body', 0, 3]); + DB::insert($entitiesSql, [9, 'The title', 'The excerpt', 'The body', 0]); + DB::insert($entitiesSql, [10, 'The title', 'The excerpt', 'The body', 0]); + DB::insert($entitiesSql, [11, 'The title', 'The excerpt', 'The body', 0]); DB::insert($closuresSql, [10, 10, 0]); DB::insert($closuresSql, [11, 11, 0]); DB::insert($closuresSql, [12, 12, 0]); @@ -44,15 +44,15 @@ public function run() DB::insert($closuresSql, [10, 12, 2]); DB::insert($closuresSql, [9, 12, 3]); - DB::insert($entitiesSql, [13, 9, 'The title', 'The excerpt', 'The body', 1, 1]); + DB::insert($entitiesSql, [9, 'The title', 'The excerpt', 'The body', 1]); DB::insert($closuresSql, [13, 13, 0]); DB::insert($closuresSql, [9, 13, 1]); - DB::insert($entitiesSql, [14, 9, 'The title', 'The excerpt', 'The body', 2, 1]); + DB::insert($entitiesSql, [9, 'The title', 'The excerpt', 'The body', 2]); DB::insert($closuresSql, [14, 14, 0]); DB::insert($closuresSql, [9, 14, 1]); - DB::insert($entitiesSql, [15, 9, 'The title', 'The excerpt', 'The body', 3, 1]); + DB::insert($entitiesSql, [9, 'The title', 'The excerpt', 'The body', 3]); DB::insert($closuresSql, [15, 15, 0]); DB::insert($closuresSql, [9, 15, 1]); } diff --git a/tests/EntityTestCase.php b/tests/EntityTestCase.php deleted file mode 100644 index f1860e8..0000000 --- a/tests/EntityTestCase.php +++ /dev/null @@ -1,922 +0,0 @@ -entity = new Entity; - $this->entity->fillable(['title', 'excerpt', 'body', 'position', 'real_depth']); - - $this->childrenRelationIndex = $this->entity->getChildrenRelationIndex(); - } - - public function testPositionIsFillable() - { - $this->assertContains($this->entity->getPositionColumn(), $this->entity->getFillable()); - } - - public function testPositionDefaultValue() - { - $this->assertEquals(0, $this->entity->position); - } - - public function testRealDepthIsFillable() - { - $this->assertContains($this->entity->getRealDepthColumn(), $this->entity->getFillable()); - } - - public function testRealDepthDefaultValue() - { - $this->assertEquals(0, $this->entity->real_depth); - } - - public function testIsParent() - { - $this->assertFalse($this->entity->isParent()); - } - - public function testIsRoot() - { - $this->assertFalse($this->entity->isRoot()); - $this->assertTrue(Entity::find(1)->isRoot()); - } - - public function testCreate() - { - DB::statement("SET foreign_key_checks=0"); - ClosureTable::truncate(); - Entity::truncate(); - DB::statement("SET foreign_key_checks=1"); - - $entity1 = new Entity; - $entity1->save(); - - $this->assertEquals(0, $entity1->position); - - $entity2 = new Entity; - $entity2->save(); - $this->assertEquals(1, $entity2->position); - } - - public function testCreateSetsPosition() - { - $entity = new Page(['title' => 'Item 1']); - - $this->assertEquals(null, $entity->position); - $this->assertEquals(null, $this->readAttribute($entity, 'old_position')); - $this->assertEquals(null, $entity->parent_id); - $this->assertEquals(null, $this->readAttribute($entity, 'old_parent_id')); - - $entity->save(); - - $this->assertEquals(9, $entity->position); - $this->assertEquals($entity->position, $this->readAttribute($entity, 'old_position')); - $this->assertEquals(null, $entity->parent_id); - $this->assertEquals($entity->parent_id, $this->readAttribute($entity, 'old_parent_id')); - } - - /** - * @dataProvider createUseGivenPositionProvider - */ - public function testCreateUseGivenPosition($initial_position, $test_entity, $assign_position, $expected_position, $test_position) - { - $this->assertEquals($initial_position, Page::find($test_entity)->position, 'Prerequisite doesn\'t match expectation'); - - $entity = new Page(['title' => 'Item 1']); - $entity->position = $assign_position; - $entity->save(); - - $this->assertEquals($expected_position, $entity->position, 'Saved position should match expected position'); - $this->assertEquals($test_position, Page::find($test_entity)->position, 'Test entity should have expected position'); - } - - public function createUseGivenPositionProvider() - { - return [ - [0, 1, -1, 0, 1, 1], // Negative clamps to 0 - [0, 1, 0, 0, 1], // 0 moves previous 0 to 1 - [3, 4, 3, 3, 4], // Test in mid range - [8, 9, 8, 8, 9], // Last existing entity - [8, 9, 9, 9, 8], // Add after last position - [8, 9, null, 9, 8], // Do not specify position = after last position - ]; - } - - public function testCreateDoesNotChangePositionOfSiblings() - { - $entity1 = new Page(['title' => 'Item 1']); - $entity1->save(); - - $id = $entity1->getKey(); - - $entity2 = new Page(['title' => 'Item 2']); - $entity2->save(); - - $this->assertEquals(10, $entity2->position); - $this->assertEquals(9, Entity::find($id)->position); - } - - public function testCreateSetsRealDepth() - { - $entity = new Page(['title' => 'Item 3']); - $entity->parent_id = 9; - $entity->save(); - - $this->assertEquals(1, $entity->real_depth); - } - - public function testSavingLoadedEntityShouldNotTriggerReordering() - { - $entity1 = new Page(['title' => 'Item 1']); - $entity1->save(); - - $id = $entity1->getKey(); - - $entity1 = Page::find($id); - - $this->assertEquals(8, Page::find(9)->position); // Sibling node that shouldn't move - - $this->assertEquals($entity1->position, $this->readAttribute($entity1, 'old_position'), 'Position should be the same after a load'); - $this->assertEquals($entity1->parent_id, $this->readAttribute($entity1, 'old_parent_id'), 'Parent should be the same after a load'); - - $entity1->title = 'New title'; - $entity1->save(); - - $this->assertEquals(8, Page::find(9)->position, 'Sibling node should not have moved'); - $this->assertEquals($entity1->position, $this->readAttribute($entity1, 'old_position')); - $this->assertEquals($entity1->parent_id, $this->readAttribute($entity1, 'old_parent_id')); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testMoveToThrowsException() - { - $this->entity->moveTo(0, $this->entity); - } - - public function testMoveTo() - { - $ancestor = Entity::find(1); - $result = $this->entity->moveTo(0, $ancestor); - - $this->assertSame($this->entity, $result); - $this->assertEquals(0, $result->position); - $this->assertEquals(1, $result->parent_id); - $this->assertEquals($this->entity->getParent()->getKey(), $ancestor->getKey()); - } - - public function testClampPosition() - { - $ancestor = Entity::find(9); - $entity = Entity::find(15); - $entity->position = -1; - $entity->save(); - - $this->assertEquals(0, $entity->position); - - $entity->position = 100; - $entity->save(); - - $this->assertEquals($ancestor->countChildren(), $entity->position); - } - - public function testGetParent() - { - $entity = Entity::find(10); - $parent = $entity->getParent(); - - $this->assertInstanceOf('Franzose\ClosureTable\Models\Entity', $parent); - $this->assertEquals(9, $parent->getKey()); - } - - public function testGetParentAfterMovingToAnAncestor() - { - $entity = Entity::find(10); - $entity->moveTo(0, 15); - $parent = $entity->getParent(); - - $this->assertInstanceOf('Franzose\ClosureTable\Models\Entity', $parent); - $this->assertEquals(15, $parent->getKey()); - } - - public function testGetAncestors() - { - $entity = Entity::find(12); - $ancestors = $entity->getAncestors(); - - $this->assertInstanceOf('Franzose\ClosureTable\Extensions\Collection', $ancestors); - $this->assertCount(3, $ancestors); - $this->assertArrayValuesEquals($ancestors->modelKeys(), [9, 10, 11]); - } - - public function testGetAncestorsWhere() - { - $entity = Entity::find(12); - $ancestors = $entity->getAncestorsWhere('excerpt', '=', ''); - - $this->assertInstanceOf('Franzose\ClosureTable\Extensions\Collection', $ancestors); - $this->assertCount(0, $ancestors); - - $ancestors = $entity->getAncestorsWhere($this->entity->getPositionColumn(), '=', 0); - $this->assertCount(2, $ancestors); - $this->assertArrayValuesEquals($ancestors->modelKeys(), [10, 11]); - } - - public function testCountAncestors() - { - $entity = Entity::find(12); - $ancestors = $entity->countAncestors(); - - $this->assertEquals(3, $ancestors); - } - - public function testHasAncestors() - { - $entity = Entity::find(12); - $hasAncestors = $entity->hasAncestors(); - - $this->assertTrue($hasAncestors); - } - - public function testGetDescendants() - { - $entity = Entity::find(9); - $descendants = $entity->getDescendants(); - - $this->assertInstanceOf('Franzose\ClosureTable\Extensions\Collection', $descendants); - $this->assertCount(6, $descendants); - $this->assertArrayValuesEquals($descendants->modelKeys(), [10, 11, 12, 13, 14, 15]); - } - - public function testGetDescendantsWhere() - { - $entity = Entity::find(9); - - $descendants = $entity->getDescendantsWhere($this->entity->getPositionColumn(), '=', 1); - $this->assertCount(1, $descendants); - $this->assertArrayValuesEquals($descendants->modelKeys(), [13]); - } - - public function testCountDescendants() - { - $entity = Entity::find(9); - $descendants = $entity->countDescendants(); - - $this->assertEquals(6, $descendants); - } - - public function testHasDescendants() - { - $entity = Entity::find(9); - $hasDescendants = $entity->hasDescendants(); - - $this->assertTrue($hasDescendants); - } - - public function testGetChildren() - { - $entity = Entity::find(9); - $children = $entity->getChildren(); - - $this->assertInstanceOf('Franzose\ClosureTable\Extensions\Collection', $children); - $this->assertCount(4, $children); - $this->assertArrayValuesEquals($children->modelKeys(), [10, 13, 14, 15]); - } - - public function testCountChildren() - { - $entity = Entity::find(9); - $children = $entity->countChildren(); - - $this->assertEquals(4, $children); - } - - public function testGetChildAt() - { - $entity = Entity::find(9); - $child = $entity->getChildAt(2); - - $this->assertInstanceOf('Franzose\ClosureTable\Models\Entity', $child); - $this->assertEquals(2, $child->position); - } - - public function testGetFirstChild() - { - $entity = Entity::find(9); - $child = $entity->getFirstChild(); - - $this->assertInstanceOf('Franzose\ClosureTable\Models\Entity', $child); - $this->assertEquals(0, $child->position); - } - - public function testGetLastChild() - { - $entity = Entity::find(9); - $child = $entity->getLastChild(); - - $this->assertInstanceOf('Franzose\ClosureTable\Models\Entity', $child); - $this->assertEquals(3, $child->position); - } - - public function testGetChildrenRange() - { - $entity = Entity::find(9); - $children = $entity->getChildrenRange(0, 2); - - $this->assertInstanceOf('Franzose\ClosureTable\Extensions\Collection', $children); - $this->assertCount(3, $children); - $this->assertEquals(0, $children[0]->position); - $this->assertEquals(1, $children[1]->position); - $this->assertEquals(2, $children[2]->position); - - $children = $entity->getChildrenRange(2); - - $this->assertCount(2, $children); - $this->assertEquals(2, $children[0]->position); - $this->assertEquals(3, $children[1]->position); - } - - public function testAddChildWithPosition() - { - $entity = Entity::find(15); - $newone = new Entity; - $result = $entity->addChild($newone, 0); - - $this->assertEquals(0, $newone->position); - $this->assertTrue($entity->isParent()); - $this->assertSame($entity, $result); - } - - public function testAddChildWithoutPosition() - { - $entity = Entity::find(9); - $newone = new Entity; - $result = $entity->addChild($newone); - - $this->assertEquals(4, $newone->position); - $this->assertTrue($entity->isParent()); - $this->assertSame($entity, $result); - } - - public function testAddChildren() - { - $entity = Entity::find(15); - $child1 = new Entity; - $child2 = new Entity; - $child3 = new Entity; - - $result = $entity->addChildren([$child1, $child2, $child3]); - - $this->assertSame($entity, $result); - $this->assertEquals(3, $entity->countChildren()); - - $this->assertEquals(0, $child1->position); - $this->assertEquals(1, $child2->position); - $this->assertEquals(2, $child3->position); - } - - public function testRemoveChild() - { - $entity = Entity::find(9); - $entity->removeChild(0); - - $child = Entity::find(10); - - $this->assertNull($child); - $this->assertEquals(3, $entity->countChildren()); - } - - public function testRemoveChildren() - { - $entity = Entity::find(9); - $entity->removeChildren(0, 1); - - $this->assertEquals(2, $entity->countChildren()); - } - - public function testRemoveChildrenToTheEnd() - { - $entity = Entity::find(9); - $entity->removeChildren(1); - - $this->assertInstanceOf('Franzose\ClosureTable\Models\Entity', $entity->getFirstChild()); - $this->assertEquals(1, $entity->countChildren()); - } - - public function testGetSiblings() - { - $entity = Entity::find(13); - $siblings = $entity->getSiblings(); - - $this->assertInstanceOf('Franzose\ClosureTable\Extensions\Collection', $siblings); - $this->assertCount(3, $siblings); - $this->assertEquals(10, $siblings[0]->getKey()); - $this->assertEquals(14, $siblings[1]->getKey()); - $this->assertEquals(15, $siblings[2]->getKey()); - } - - public function testsCountSiblings() - { - $entity = Entity::find(13); - $number = $entity->countSiblings(); - - $this->assertEquals(3, $number); - } - - public function testsHasSiblings() - { - $entity = Entity::find(13); - $hasSiblings = $entity->hasSiblings(); - - $this->assertTrue($hasSiblings); - } - - public function testsGetNeighbors() - { - $entity = Entity::find(13); - $neighbors = $entity->getNeighbors(); - - $this->assertCount(2, $neighbors); - $this->assertEquals(10, $neighbors[0]->getKey()); - $this->assertEquals(14, $neighbors[1]->getKey()); - } - - public function testsGetSiblingAt() - { - $entity = Entity::find(13); - $sibling = $entity->getSiblingAt(0); - - $this->assertEquals(10, $sibling->getKey()); - - $sibling = $entity->getSiblingAt(2); - - $this->assertEquals(14, $sibling->getKey()); - } - - public function testGetFirstSibling() - { - $entity = Entity::find(13); - $sibling = $entity->getFirstSibling(); - - $this->assertEquals(10, $sibling->getKey()); - } - - public function testGetLastSibling() - { - $entity = Entity::find(13); - $sibling = $entity->getLastSibling(); - - $this->assertEquals(15, $sibling->getKey()); - } - - public function testGetPrevSibling() - { - $entity = Entity::find(15); - $sibling = $entity->getPrevSibling(); - - $this->assertEquals(14, $sibling->getKey()); - } - - public function testGetPrevSiblings() - { - $entity = Entity::find(15); - $siblings = $entity->getPrevSiblings(); - - $this->assertCount(3, $siblings); - $this->assertEquals(10, $siblings[0]->getKey()); - $this->assertEquals(13, $siblings[1]->getKey()); - $this->assertEquals(14, $siblings[2]->getKey()); - } - - public function testsCountPrevSiblings() - { - $entity = Entity::find(15); - $siblings = $entity->countPrevSiblings(); - - $this->assertEquals(3, $siblings); - } - - public function testsHasPrevSiblings() - { - $entity = Entity::find(15); - $hasPrevSiblings = $entity->hasPrevSiblings(); - - $this->assertTrue($hasPrevSiblings); - } - - public function testGetNextSibling() - { - $entity = Entity::find(10); - $sibling = $entity->getNextSibling(); - - $this->assertEquals(13, $sibling->getKey()); - } - - public function testGetNextSiblings() - { - $entity = Entity::find(10); - $siblings = $entity->getNextSiblings(); - - $this->assertCount(3, $siblings); - $this->assertEquals(13, $siblings[0]->getKey()); - $this->assertEquals(14, $siblings[1]->getKey()); - $this->assertEquals(15, $siblings[2]->getKey()); - } - - public function testCountNextSiblings() - { - $entity = Entity::find(10); - $siblings = $entity->countNextSiblings(); - - $this->assertEquals(3, $siblings); - } - - public function testsHasNextSiblings() - { - $entity = Entity::find(10); - $hasNextSiblings = $entity->hasNextSiblings(); - - $this->assertTrue($hasNextSiblings); - } - - public function testGetSiblingsRange() - { - $entity = Entity::find(15); - $siblings = $entity->getSiblingsRange(1, 2); - - $this->assertCount(2, $siblings); - $this->assertEquals(1, $siblings[0]->position); - $this->assertEquals(2, $siblings[1]->position); - } - - public function testAddSibling() - { - $entity = Entity::find(15); - $entity->addSibling(new Entity); - - $sibling = $entity->getNextSibling(); - - $this->assertInstanceOf('Franzose\ClosureTable\Models\Entity', $sibling); - $this->assertEquals(4, $sibling->position); - } - - public function testAddSiblings() - { - $entity = Entity::find(15); - $entity->addSiblings([new Entity, new Entity, new Entity]); - - $siblings = $entity->getNextSiblings(); - - $this->assertCount(3, $siblings); - $this->assertEquals(4, $siblings[0]->position); - $this->assertEquals(5, $siblings[1]->position); - $this->assertEquals(6, $siblings[2]->position); - } - - public function testAddSiblingsFromPosition() - { - $entity = Entity::find(15); - - $entity->addSiblings([new Entity, new Entity, new Entity, new Entity], 1); - - $siblings = $entity->getSiblingsRange(1, 4); - - $this->assertEquals(16, $siblings[0]->getKey()); - $this->assertEquals(17, $siblings[1]->getKey()); - $this->assertEquals(18, $siblings[2]->getKey()); - $this->assertEquals(19, $siblings[3]->getKey()); - } - - public function testGetRoots() - { - $roots = Entity::getRoots(); - - $this->assertCount(9, $roots); - - foreach ($roots as $idx => $root) { - $this->assertEquals($idx + 1, $roots->get($idx)->getKey()); - } - } - - public function testGetTree() - { - $tree = Entity::getTree(); - - $this->assertCount(9, $tree); - - $ninth = $tree[8]; - $this->assertArrayHasKey($this->childrenRelationIndex, $ninth->getRelations()); - - $tenth = $ninth->getChildren(); - - $this->assertCount(4, $tenth); - } - - public function testGetTreeWhere() - { - $tree = Entity::getTreeWhere($this->entity->getPositionColumn(), '>=', 1, [ - $this->entity->getKeyName(), - $this->entity->getPositionColumn() - ]); - - $this->assertCount(8, $tree); - $this->assertEquals(1, $tree[0]->position); - - $eight = $tree[7]; - - $this->assertArrayHasKey($this->childrenRelationIndex, $eight->getRelations()); - $this->assertEquals(1, $eight->getChildAt(0)->position); - - $ninth = $eight->getChildren(); - - $this->assertCount(3, $ninth); - } - - public function testDeleteSubtree() - { - $entity = Entity::find(9); - $entity->deleteSubtree(); - - $this->assertEquals(1, Entity::whereBetween('id', [9, 15])->count()); - $this->assertEquals(8, Entity::whereBetween('id', [1, 8])->count()); - } - - public function testDeleteSubtreeWithAncestor() - { - $entity = Entity::find(9); - $entity->deleteSubtree(true); - - $this->assertEquals(0, Entity::whereBetween('id', [9, 15])->count()); - $this->assertEquals(8, Entity::whereBetween('id', [1, 8])->count()); - } - - public function testForceDeleteSubtree() - { - $entity = Entity::find(9); - $entity->deleteSubtree(false, true); - - $this->assertEquals(1, Entity::whereBetween('id', [9, 15])->count()); - $this->assertEquals(1, ClosureTable::whereBetween('ancestor', [9, 15])->count()); - } - - public function testForceDeleteDeepSubtree() - { - Entity::find(9)->moveTo(0, 8); - Entity::find(8)->moveTo(0, 7); - Entity::find(7)->moveTo(0, 6); - Entity::find(6)->moveTo(0, 5); - Entity::find(5)->moveTo(0, 4); - Entity::find(4)->moveTo(0, 3); - Entity::find(3)->moveTo(0, 2); - Entity::find(2)->moveTo(0, 1); - - Entity::find(1)->deleteSubtree(false, true); - - $this->assertEquals(1, Entity::whereBetween('id', [1, 9])->count()); - $this->assertEquals(1, ClosureTable::whereBetween('ancestor', [1, 9])->count()); - } - - public function testForceDeleteSubtreeWithSelf() - { - $entity = Entity::find(9); - $entity->deleteSubtree(true, true); - - $this->assertEquals(0, Entity::whereBetween('id', [9, 15])->count()); - $this->assertEquals(0, ClosureTable::whereBetween('ancestor', [9, 15])->count()); - } - - public function testCreateFromArray() - { - $array = [ - [ - 'id' => 90, - 'title' => 'About', - 'position' => 0, - 'children' => [ - [ - 'id' => 93, - 'title' => 'Testimonials' - ] - ] - ], - [ - 'id' => 91, - 'title' => 'Blog', - 'position' => 1 - ], - [ - 'id' => 92, - 'title' => 'Portfolio', - 'position' => 2 - ], - ]; - - $pages = Page::createFromArray($array); - - $this->assertInstanceOf('Franzose\ClosureTable\Extensions\Collection', $pages); - $this->assertCount(3, $pages); - - $pageZero = $pages[0]; - - $this->assertTrue($pageZero->hasChildrenRelation()); - - $this->assertEquals(90, $pageZero->getKey()); - $this->assertEquals(91, $pages[1]->getKey()); - $this->assertEquals(92, $pages[2]->getKey()); - $this->assertEquals(93, $pageZero->getChildAt(0)->getKey()); - } - - public function testCreateFromArrayBug81() - { - $array = [ - [ - 'title' => 'About', - 'children' => [ - [ - 'title' => 'Testimonials', - 'children' => [ - [ - 'title' => 'child 1', - ], - [ - 'title' => 'child 2', - ], - ] - ] - ] - ], - [ - 'title' => 'Blog', - ], - [ - 'title' => 'Portfolio', - ], - ]; - - $pages = Page::createFromArray($array); - - $about = $pages[0]; - $this->assertEquals('About', $about->title); - $this->assertEquals(1, $about->countChildren()); - $this->assertEquals(16, $about->getKey()); - - $blog = $pages[1]; - $this->assertEquals('Blog', $blog->title); - $this->assertEquals(0, $blog->countChildren()); - $this->assertEquals(20, $blog->getKey()); - - $portfolio = $pages[2]; - $this->assertEquals('Portfolio', $portfolio->title); - $this->assertEquals(0, $portfolio->countChildren()); - $this->assertEquals(21, $portfolio->getKey()); - - $pages = $pages[0]->getChildren(); - - $testimonials = $pages[0]; - $this->assertEquals('Testimonials', $testimonials->title); - $this->assertEquals(2, $testimonials->countChildren()); - $this->assertEquals(17, $testimonials->getKey()); - - $pages = $pages[0]->getChildren(); - - $child1 = $pages[0]; - $this->assertEquals('child 1', $child1->title); - $this->assertEquals(0, $child1->countChildren()); - $this->assertEquals(18, $child1->getKey()); - - $child2 = $pages[1]; - $this->assertEquals('child 2', $child2->title); - $this->assertEquals(0, $child2->countChildren()); - $this->assertEquals(19, $child2->getKey()); - } - - public function testInsertNode() - { - $entity = Entity::create(['title' => 'abcde']); - $closure = ClosureTable::whereDescendant($entity->getKey())->first(); - - $this->assertNotNull($closure); - $this->assertEquals($entity->getKey(), $closure->ancestor); - $this->assertEquals(0, $closure->depth); - } - - public function testInsertedNodeDepth() - { - $entity = Entity::create(['title' => 'abcde']); - $child = Entity::create(['title' => 'abcde']); - $child->moveTo(0, $entity); - - $closure = ClosureTable::whereDescendant($child->getKey()) - ->whereAncestor($entity->getKey())->first(); - - $this->assertNotNull($closure); - $this->assertEquals(1, $closure->depth); - } - - public function testValidNumberOfRowsInsertedByInsertNode() - { - $ancestor = Entity::create(['title' => 'abcde']); - $descendant = Entity::create(['title' => 'abcde']); - $descendant->moveTo(0, $ancestor); - - $ancestorRows = ClosureTable::whereDescendant($ancestor->getKey())->count(); - $descendantRows = ClosureTable::whereDescendant($descendant->getKey())->count(); - - $this->assertEquals(1, $ancestorRows); - $this->assertEquals(2, $descendantRows); - } - - public function testMoveNodeToAnotherAncestor() - { - $descendant = Entity::find(1); - $descendant->moveTo(0, 2); - - $ancestors = ClosureTable::whereDescendant(2)->count(); - $descendants = ClosureTable::whereDescendant(1)->count(); - - $this->assertEquals(1, $ancestors); - $this->assertEquals(2, $descendants); - } - - public function testMoveNodeToDeepNesting() - { - $item = Entity::find(1); - $item->moveTo(0, 2); - - $item = Entity::find(2); - $item->moveTo(0, 3); - - $item = Entity::find(3); - $item->moveTo(0, 4); - - $item = Entity::find(4); - $item->moveTo(0, 5); - - $descendantRows = ClosureTable::whereDescendant(1)->count(); - $ancestorRows = ClosureTable::whereDescendant(2)->count(); - - $this->assertEquals(4, $ancestorRows); - $this->assertEquals(5, $descendantRows); - } - - public function testMoveNodeToBecomeRoot() - { - $item = Entity::find(1); - $item->moveTo(0, 2); - - $item = Entity::find(2); - $item->moveTo(0, 3); - - $item = Entity::find(3); - $item->moveTo(0, 4); - - $item = Entity::find(4); - $item->moveTo(0, 5); - - $item = Entity::find(1); - $item->moveTo(0); - - $this->assertEquals(1, ClosureTable::whereDescendant(1)->count()); - } -} diff --git a/tests/Extensions/CollectionTests.php b/tests/Extensions/CollectionTests.php new file mode 100644 index 0000000..31c49cb --- /dev/null +++ b/tests/Extensions/CollectionTests.php @@ -0,0 +1,169 @@ + 0]), + new Page(['position' => 1]), + new Page(['position' => 2]), + ]); + + static::assertEquals(1, $collection->getChildAt(1)->position); + static::assertNull($collection->getChildAt(999)); + } + + public function testGetFirstChild() + { + $collection = new Collection([ + new Page(['position' => 0]), + new Page(['position' => 1]), + ]); + + static::assertEquals(0, $collection->getFirstChild()->position); + static::assertNull((new Collection())->getFirstChild()); + } + + public function testGetLastChild() + { + $collection = new Collection([ + new Page(['position' => 0]), + new Page(['position' => 1]), + ]); + + static::assertEquals(1, $collection->getLastChild()->position); + static::assertNull((new Collection())->getLastChild()); + } + + public function testGetRange() + { + $collection = new Collection([ + new Page(['position' => 0]), + new Page(['position' => 1]), + new Page(['position' => 2]), + new Page(['position' => 3]), + ]); + + static::assertEquals([2, 3], $collection->getRange(2)->pluck('position')->toArray()); + static::assertEquals([1, 2, 3], $collection->getRange(1, 3)->pluck('position')->toArray()); + } + + public function testGetNeighbors() + { + $collection = new Collection([ + new Page(['position' => 0]), + new Page(['position' => 1]), + new Page(['position' => 2]), + new Page(['position' => 3]), + ]); + + $neighbors = $collection->getNeighbors(1); + + static::assertCount(2, $neighbors); + static::assertEquals([0, 2], $neighbors->pluck('position')->toArray()); + } + + public function testGetPrevSiblings() + { + $collection = new Collection([ + new Page(['position' => 0]), + new Page(['position' => 1]), + new Page(['position' => 2]), + new Page(['position' => 3]), + ]); + + $siblings = $collection->getPrevSiblings(3); + + static::assertCount(3, $siblings); + static::assertEquals([0, 1, 2], $siblings->pluck('position')->toArray()); + } + + public function testGetNextSiblings() + { + $collection = new Collection([ + new Page(['position' => 0]), + new Page(['position' => 1]), + new Page(['position' => 2]), + new Page(['position' => 3]), + ]); + + $siblings = $collection->getNextSiblings(0); + + static::assertCount(3, $siblings); + static::assertEquals([1, 2, 3], $siblings->pluck('position')->toArray()); + } + + public function testGetChildrenOf() + { + $entity = new Page(['position' => 0]); + $childrenRelationIndex = $entity->getChildrenRelationIndex(); + + $collection = new Collection([ + $entity, + new Page(['position' => 1]), + new Page(['position' => 2]) + ]); + + $expected = new Collection([ + new Page(['position' => 0]), + new Page(['position' => 1]), + new Page(['position' => 2]) + ]); + + /** @var Entity $firstEntity */ + $firstEntity = $collection->get(0); + $firstEntity->setRelation($childrenRelationIndex, $expected); + + $actual = $collection->getChildrenOf(0); + + static::assertSame($expected, $actual); + } + + public function testHasChildren() + { + $entity = new Page(['position' => 0]); + $childrenRelationIndex = $entity->getChildrenRelationIndex(); + + $collection = new Collection([ + $entity, + new Page(['position' => 1]), + new Page(['position' => 2]) + ]); + + $children = new Collection([ + new Page(['position' => 0]), + new Page(['position' => 1]), + new Page(['position' => 2]) + ]); + + /** @var Entity $firstEntity */ + $firstEntity = $collection->get(0); + $firstEntity->setRelation($childrenRelationIndex, $children); + + static::assertTrue($collection->hasChildren(0)); + } + + public function testToTree() + { + $root = new Page(['id' => 1]); + $child = new Page(['id' => 2, 'parent_id' => 1]); + $grandChild = new Page(['id' => 3, 'parent_id' => 2]); + + $tree = (new Collection([$root, $child, $grandChild]))->toTree(); + + static::assertCount(1, $tree); + + $children = $tree->get(0)->children; + static::assertCount(1, $children); + static::assertSame($child, $children->get(0)); + + $grandChildren = $children->get(0)->children; + static::assertCount(1, $grandChildren); + static::assertSame($grandChild, $grandChildren->get(0)); + } +} diff --git a/tests/Extensions/StrTests.php b/tests/Extensions/StrTests.php new file mode 100644 index 0000000..eefd788 --- /dev/null +++ b/tests/Extensions/StrTests.php @@ -0,0 +1,20 @@ +create([ + 'migrations-path' => __DIR__, + 'entity-table' => 'entity', + 'closure-table' => 'entity_tree', + 'use-innodb' => $useInnoDb + ]); + + Carbon::setTestNow(); + + $entityMigrationPath = __DIR__ . '/2020_04_03_000000_create_entities_table_migration.php'; + + static::assertFileExists($entityMigrationPath); + + $expectedEntityMigrationPath = sprintf( + '%s/expectedMigration%s.php', + __DIR__, + $useInnoDb ? 'InnoDb' : '' + ); + + static::assertFileEquals($expectedEntityMigrationPath, $entityMigrationPath); + + unlink($entityMigrationPath); + } + + public function useInnoDbDataProvider() + { + return [ + [true, false] + ]; + } +} diff --git a/tests/Generators/ModelTests.php b/tests/Generators/ModelTests.php new file mode 100644 index 0000000..fbbcfac --- /dev/null +++ b/tests/Generators/ModelTests.php @@ -0,0 +1,34 @@ +create([ + 'namespace' => 'Foo', + 'entity' => 'FooBar', + 'entity-table' => 'foo_bar', + 'closure' => 'FooBarClosure', + 'closure-table' => 'foo_bar_tree', + 'models-path' => __DIR__, + ]); + + $entityPath = __DIR__ . '/FooBar.php'; + $closurePath = __DIR__ . '/FooBarClosure.php'; + static::assertFileExists($entityPath); + static::assertFileExists($closurePath); + static::assertFileEquals(__DIR__ . '/expectedEntity.php', $entityPath); + static::assertFileEquals(__DIR__ . '/expectedClosure.php', $closurePath); + + unlink($entityPath); + unlink($closurePath); + } +} diff --git a/tests/Generators/expectedClosure.php b/tests/Generators/expectedClosure.php new file mode 100644 index 0000000..7b4899c --- /dev/null +++ b/tests/Generators/expectedClosure.php @@ -0,0 +1,14 @@ +increments('id'); + $table->integer('parent_id')->unsigned()->nullable(); + $table->integer('position', false, true); + $table->softDeletes(); + + $table->foreign('parent_id') + ->references('id') + ->on('entity') + ->onDelete('set null'); + }); + + Schema::create('entity_tree', function (Blueprint $table) { + $table->increments('closure_id'); + + $table->integer('ancestor', false, true); + $table->integer('descendant', false, true); + $table->integer('depth', false, true); + + $table->foreign('ancestor') + ->references('id') + ->on('entity') + ->onDelete('cascade'); + + $table->foreign('descendant') + ->references('id') + ->on('entity') + ->onDelete('cascade'); + }); + } + + public function down() + { + Schema::dropIfExists('entity_tree'); + Schema::dropIfExists('entity'); + } +} diff --git a/tests/Generators/expectedMigrationInnoDb.php b/tests/Generators/expectedMigrationInnoDb.php new file mode 100644 index 0000000..9d8fb5d --- /dev/null +++ b/tests/Generators/expectedMigrationInnoDb.php @@ -0,0 +1,50 @@ +increments('id'); + $table->integer('parent_id')->unsigned()->nullable(); + $table->integer('position', false, true); + $table->softDeletes(); + + $table->foreign('parent_id') + ->references('id') + ->on('entity') + ->onDelete('set null'); + + $table->engine = 'InnoDB'; + }); + + Schema::create('entity_tree', function (Blueprint $table) { + $table->increments('closure_id'); + + $table->integer('ancestor', false, true); + $table->integer('descendant', false, true); + $table->integer('depth', false, true); + + $table->foreign('ancestor') + ->references('id') + ->on('entity') + ->onDelete('cascade'); + + $table->foreign('descendant') + ->references('id') + ->on('entity') + ->onDelete('cascade'); + + $table->engine = 'InnoDB'; + }); + } + + public function down() + { + Schema::dropIfExists('entity_tree'); + Schema::dropIfExists('entity'); + } +} diff --git a/tests/Models/ClosureTableTestCase.php b/tests/Models/ClosureTableTestCase.php new file mode 100644 index 0000000..7fbe3fb --- /dev/null +++ b/tests/Models/ClosureTableTestCase.php @@ -0,0 +1,73 @@ +ctable = new ClosureTable; + $this->ancestorColumn = $this->ctable->getAncestorColumn(); + $this->descendantColumn = $this->ctable->getDescendantColumn(); + $this->depthColumn = $this->ctable->getDepthColumn(); + } + + public function testAncestorQualifiedKeyName() + { + static::assertEquals( + $this->ctable->getTable() . '.' . $this->ancestorColumn, + $this->ctable->getQualifiedAncestorColumn() + ); + } + + public function testDescendantQualifiedKeyName() + { + static::assertEquals( + $this->ctable->getTable() . '.' . $this->descendantColumn, + $this->ctable->getQualifiedDescendantColumn() + ); + } + + public function testDepthQualifiedKeyName() + { + static::assertEquals( + $this->ctable->getTable() . '.' . $this->depthColumn, + $this->ctable->getQualifiedDepthColumn() + ); + } + + public function testNewNodeShouldBeInsertedIntoClosureTable() + { + $entity = Entity::create(['title' => 'abcde']); + $closure = ClosureTable::whereDescendant($entity->getKey())->first(); + + static::assertNotNull($closure); + static::assertEquals($entity->getKey(), $closure->ancestor); + static::assertEquals(0, $closure->depth); + } +} diff --git a/tests/Models/Entity/AncestorTests.php b/tests/Models/Entity/AncestorTests.php new file mode 100644 index 0000000..c219877 --- /dev/null +++ b/tests/Models/Entity/AncestorTests.php @@ -0,0 +1,88 @@ +getAncestors()); + static::assertCount(0, Entity::find(1)->getAncestors()); + } + + public function testAncestorsScope() + { + $entity = Entity::find(12); + + $ancestors = $entity->ancestors()->get(); + + static::assertCount(3, $ancestors); + static::assertEquals([11, 10, 9], $ancestors->modelKeys()); + } + + public function testAncestorsOfScope() + { + $ancestors = Entity::ancestorsOf(12)->get(); + + static::assertCount(3, $ancestors); + static::assertEquals([11, 10, 9], $ancestors->modelKeys()); + } + + public function testAncestorsWithSelfScope() + { + $entity = Entity::find(12); + + $ancestors = $entity->ancestorsWithSelf()->get(); + + static::assertCount(4, $ancestors); + static::assertEquals([12, 11, 10, 9], $ancestors->modelKeys()); + } + + public function testAncestorsWithSelfOfScope() + { + $ancestors = Entity::ancestorsWithSelfOf(12)->get(); + + static::assertCount(4, $ancestors); + static::assertEquals([12, 11, 10, 9], $ancestors->modelKeys()); + } + + public function testGetAncestorsShouldNotBeEmpty() + { + $entity = Entity::find(12); + + $ancestors = $entity->getAncestors(); + + static::assertInstanceOf(Collection::class, $ancestors); + static::assertCount(3, $ancestors); + static::assertContainsOnlyInstancesOf(Entity::class, $ancestors); + static::assertEquals([11, 10, 9], $ancestors->modelKeys()); + } + + public function testAncestorsWhere() + { + $entity = Entity::find(12); + + $ancestors = $entity->getAncestorsWhere('position', '<', 2); + + static::assertInstanceOf(Collection::class, $ancestors); + static::assertCount(2, $ancestors); + static::assertContainsOnlyInstancesOf(Entity::class, $ancestors); + static::assertEquals([11, 10], $ancestors->modelKeys()); + } + + public function testCountAncestors() + { + static::assertEquals(0, Entity::find(1)->countAncestors()); + static::assertEquals(3, Entity::find(12)->countAncestors()); + } + + public function testHasAncestors() + { + static::assertFalse(Entity::find(1)->hasAncestors()); + static::assertTrue(Entity::find(12)->hasAncestors()); + } +} diff --git a/tests/Models/Entity/ChildManipulationTests.php b/tests/Models/Entity/ChildManipulationTests.php new file mode 100644 index 0000000..2e3b881 --- /dev/null +++ b/tests/Models/Entity/ChildManipulationTests.php @@ -0,0 +1,235 @@ +addChild($child); + + static::assertEquals(14, $child->parent_id); + static::assertEquals(0, $child->position); + static::assertTrue($leaf->isParent()); + } + + public function testAddChild2() + { + $parent = Entity::find(11); + $child = Entity::find(13); + + $parent->addChild($child, 0); + + static::assertEquals(11, $child->parent_id); + static::assertEquals(0, $child->position); + static::assertModelAttribute('position', [ + 10 => 0, + 14 => 1, + 15 => 2, + 11 => 0, + 12 => 1 + ]); + } + + public function testAddChildReordersNodesOnThePreviousLevel() + { + $parent = Entity::find(13); + $child = Entity::find(5); + + $parent->addChild($child); + + static::assertModelAttribute('position', [ + 5 => 0, + // previous level nodes + 6 => 4, + 7 => 5, + 8 => 6, + 9 => 7, + ]); + } + + public function testAddChildShouldReturnChild() + { + $leaf = Entity::find(14); + $child = Entity::find(15); + + $result = $leaf->addChild($child, 0, true); + + static::assertSame($child, $result); + } + + public function testAddChildToTheLastPosition() + { + $parent = Entity::find(9); + $child = Entity::find(12); + + $parent->addChild($child); + + static::assertEquals(9, $child->parent_id); + static::assertEquals(4, $child->position); + static::assertModelAttribute('position', [ + 10 => 0, + 13 => 1, + 14 => 2, + 15 => 3, + 12 => 4, + ]); + } + + public function testAddChildToPosition() + { + $parent = Entity::find(9); + $child = Entity::find(12); + + $parent->addChild($child, 2); + + static::assertEquals(9, $child->parent_id); + static::assertEquals(2, $child->position); + static::assertModelAttribute('position', [ + 10 => 0, + 13 => 1, + 12 => 2, + 14 => 3, + 15 => 4 + ]); + } + + public function testAddChildHavingChildren() + { + $parent = Entity::find(13); + $child = Entity::find(10); + + $parent->addChild($child); + + static::assertEquals(13, $child->parent_id); + static::assertEquals(0, $child->position); + static::assertModelAttribute('position', [ + 13 => 0, + 14 => 1, + 15 => 2, + 10 => 0, + 11 => 0, + 12 => 0 + ]); + } + + public function testAddChildren() + { + $entity = Entity::find(15); + $child1 = new Entity(); + $child2 = new Entity(); + $child3 = new Entity(); + + $result = $entity->addChildren([ + $child1, + $child2, + $child3 + ]); + + static::assertSame($entity, $result); + static::assertEquals(3, $entity->countChildren()); + static::assertEquals(0, $child1->position); + static::assertEquals(1, $child2->position); + static::assertEquals(2, $child3->position); + } + + public function testAddChildrenFromPosition() + { + $entity = Entity::find(9); + $child1 = new Entity(); + $child2 = new Entity(); + + $entity->addChildren([$child1, $child2], 1); + + static::assertEquals(6, $entity->countChildren()); + static::assertEquals(1, $child1->position); + static::assertEquals(2, $child2->position); + static::assertModelAttribute('position', [ + 10 => 0, + 13 => 3, + 14 => 4, + 15 => 5 + ]); + } + + public function testRemoveChild() + { + $entity = Entity::find(9); + + $entity->removeChild(0); + + static::assertNull(Entity::find(10)); + static::assertEquals(3, $entity->countChildren()); + static::assertModelAttribute('position', [ + 13 => 0, + 14 => 1, + 15 => 2 + ]); + } + + public function testRemoveChildHavingChildren() + { + $entity = Entity::find(9); + + $entity->removeChild(0, true); + + static::assertNull(Entity::find(10)); + + $entity11 = Entity::find(11); + $entity12 = Entity::find(12); + + static::assertTrue($entity11->isRoot()); + static::assertFalse($entity12->isRoot()); + } + + public function testRemoveChildren() + { + $entity = Entity::find(9); + $entity->addChild(new Entity()); + + $entity->removeChildren(0, 2); + + static::assertEmpty(Entity::whereIn('id', [10, 13, 14])->get()); + static::assertEquals(2, $entity->countChildren()); + static::assertModelAttribute('position', [ + 15 => 0, + 16 => 1 + ]); + } + + public function testRemoveChildrenToTheEnd() + { + $entity = Entity::find(9); + + $entity->removeChildren(1); + + static::assertEmpty(Entity::whereIn('id', [13, 14, 15])->get()); + static::assertEquals(1, $entity->countChildren()); + + $firstChild = $entity->getFirstChild(); + static::assertEquals(10, $firstChild->getKey()); + static::assertEquals(0, $firstChild->position); + } + + public function testRemoveChildrenHavingChildren() + { + Entity::find(13)->addChildren([new Entity(), new Entity()]); + + $parent = Entity::find(9); + + $parent->removeChildren(0, 1); + + static::assertEmpty(Entity::whereIn('id', [10, 13])->get()); + static::assertEquals(2, $parent->countChildren()); + static::assertModelAttribute('position', [ + 14 => 0, + 15 => 1 + ]); + } +} diff --git a/tests/Models/Entity/ChildQueryTests.php b/tests/Models/Entity/ChildQueryTests.php new file mode 100644 index 0000000..0595111 --- /dev/null +++ b/tests/Models/Entity/ChildQueryTests.php @@ -0,0 +1,120 @@ +getChildren()); + static::assertEquals(0, $entity->countChildren()); + static::assertFalse($entity->hasChildren()); + } + + public function testGetChildren() + { + static::assertCount(4, Entity::find(9)->getChildren()); + } + + public function testCountChildren() + { + static::assertEquals(4, Entity::find(9)->countChildren()); + } + + public function testHasChildren() + { + static::assertFalse(Entity::find(1)->hasChildren()); + static::assertTrue(Entity::find(9)->hasChildren()); + } + + public function testGetChildAt() + { + $child = Entity::find(9)->getChildAt(1); + + static::assertInstanceOf(Entity::class, $child); + static::assertEquals(13, $child->getKey()); + } + + public function testGetFirstChild() + { + $entity = Entity::find(9); + + $child = $entity->getFirstChild(); + + static::assertInstanceOf(Entity::class, $child); + static::assertEquals(10, $child->getKey()); + } + + public function testGetLastChild() + { + $entity = Entity::find(9); + $child = $entity->getLastChild(); + + static::assertInstanceOf(Entity::class, $child); + static::assertEquals(15, $child->getKey()); + } + + public function testGetChildrenRange() + { + $entity = Entity::find(9); + $children = $entity->getChildrenRange(0, 2); + + static::assertInstanceOf(Collection::class, $children); + static::assertCount(3, $children); + static::assertEquals(0, $children[0]->position); + static::assertEquals(1, $children[1]->position); + static::assertEquals(2, $children[2]->position); + + $children = $entity->getChildrenRange(2); + + static::assertCount(2, $children); + static::assertEquals(2, $children[0]->position); + static::assertEquals(3, $children[1]->position); + } + + public function testChildNodeOfScope() + { + $child = Entity::childNodeOf(9)->where('position', '=', 2)->first(); + + static::assertInstanceOf(Entity::class, $child); + static::assertEquals(14, $child->getKey()); + } + + public function testChildOfScope() + { + $child = Entity::childOf(9, 2)->first(); + + static::assertInstanceOf(Entity::class, $child); + static::assertEquals(14, $child->getKey()); + } + + public function testFirstChildOfScope() + { + $child = Entity::firstChildOf(9)->first(); + + static::assertInstanceOf(Entity::class, $child); + static::assertEquals(10, $child->getKey()); + } + + public function testLastChildOfScope() + { + $child = Entity::lastChildOf(9)->first(); + + static::assertInstanceOf(Entity::class, $child); + static::assertEquals(15, $child->getKey()); + } + + public function testChildrenRangeOfScope() + { + $children = Entity::childrenRangeOf(9, 0, 2)->get(); + + static::assertCount(3, $children); + static::assertEquals([10, 13, 14], $children->modelKeys()); + } +} diff --git a/tests/Models/Entity/ConstructionTests.php b/tests/Models/Entity/ConstructionTests.php new file mode 100644 index 0000000..686150a --- /dev/null +++ b/tests/Models/Entity/ConstructionTests.php @@ -0,0 +1,50 @@ +isFillable('position')); + } + + public function testPositionShouldBeCorrect() + { + static::assertNull((new Entity())->position); + static::assertEquals(0, (new Entity(['position' => -1]))->position); + + $entity = new Entity(); + $entity->position = -1; + static::assertEquals(0, $entity->position); + } + + public function testEntityShouldUseDefaultClosureTable() + { + $entity = new CustomEntity(); + $closure = $entity->getClosureTable(); + + static::assertSame(ClosureTable::class, get_class($closure)); + static::assertEquals($entity->getTable() . '_closure', $closure->getTable()); + } + + public function testCreate() + { + $entity = new Page(['title' => 'Item 1']); + + static::assertEquals(null, $entity->position); + static::assertEquals(null, $entity->parent_id); + + $entity->save(); + + static::assertEquals(9, $entity->position); + static::assertEquals(null, $entity->parent_id); + } +} diff --git a/tests/Models/Entity/CustomEntity.php b/tests/Models/Entity/CustomEntity.php new file mode 100644 index 0000000..5e7aaa3 --- /dev/null +++ b/tests/Models/Entity/CustomEntity.php @@ -0,0 +1,19 @@ +closure; + } +} diff --git a/tests/Models/Entity/DescendantTests.php b/tests/Models/Entity/DescendantTests.php new file mode 100644 index 0000000..e71e8df --- /dev/null +++ b/tests/Models/Entity/DescendantTests.php @@ -0,0 +1,82 @@ +getDescendants()); + static::assertCount(0, Entity::find(1)->getDescendants()); + } + + public function testDescendantsScope() + { + $entity = Entity::find(9); + + $descendants = $entity->descendants()->get(); + + static::assertCount(6, $descendants); + static::assertEquals([10, 11, 12, 13, 14, 15], $descendants->modelKeys()); + } + + public function testDescendantsOfScope() + { + $descendants = Entity::descendantsOf(9)->get(); + + static::assertCount(6, $descendants); + static::assertEquals([10, 11, 12, 13, 14, 15], $descendants->modelKeys()); + } + + public function testDescendantsWithSelfScope() + { + $entity = Entity::find(9); + + $descendants = $entity->descendantsWithSelf()->get(); + + static::assertCount(7, $descendants); + static::assertEquals([9, 10, 11, 12, 13, 14, 15], $descendants->modelKeys()); + } + + public function testDescendantsWithSelfOfScope() + { + $descendants = Entity::descendantsWithSelfOf(9)->get(); + + static::assertCount(7, $descendants); + static::assertEquals([9, 10, 11, 12, 13, 14, 15], $descendants->modelKeys()); + } + + public function testGetDescendants() + { + $entity = Entity::find(9); + $descendants = $entity->getDescendants(); + + static::assertInstanceOf(Collection::class, $descendants); + static::assertCount(6, $descendants); + static::assertEquals([10, 11, 12, 13, 14, 15], $descendants->modelKeys()); + } + + public function testGetDescendantsWhere() + { + $descendants = Entity::find(9)->getDescendantsWhere('position', '=', 1); + + static::assertCount(1, $descendants); + static::assertEquals([13], $descendants->modelKeys()); + } + + public function testCountDescendants() + { + static::assertEquals(6, Entity::find(9)->countDescendants()); + static::assertEquals(0, Entity::find(1)->countDescendants()); + } + + public function testHasDescendants() + { + static::assertTrue(Entity::find(9)->hasDescendants()); + static::assertFalse(Entity::find(1)->hasDescendants()); + } +} diff --git a/tests/Models/Entity/MovementTests.php b/tests/Models/Entity/MovementTests.php new file mode 100644 index 0000000..87ae96f --- /dev/null +++ b/tests/Models/Entity/MovementTests.php @@ -0,0 +1,134 @@ +expectException(InvalidArgumentException::class); + + $entity = Entity::find(9); + + $entity->moveTo(0, $entity); + } + + public function testMoveTo() + { + $parent = Entity::find(1); + $child = Entity::find(2); + $result = $child->moveTo(0, $parent); + + static::assertSame($child, $result); + static::assertEquals(0, $result->position); + static::assertEquals(1, $result->parent_id); + static::assertEquals($parent->getKey(), $result->getParent()->getKey()); + } + + public function testInsertedNodeDepth() + { + $entity = Entity::create(['title' => 'abcde']); + $child = Entity::create(['title' => 'abcde']); + $child->moveTo(0, $entity); + + $closure = ClosureTable::whereDescendant($child->getKey()) + ->whereAncestor($entity->getKey())->first(); + + static::assertNotNull($closure); + static::assertEquals(1, $closure->depth); + } + + public function testValidNumberOfRowsInsertedByInsertNode() + { + $ancestor = Entity::create(['title' => 'abcde']); + $descendant = Entity::create(['title' => 'abcde']); + $descendant->moveTo(0, $ancestor); + + $ancestorId = $ancestor->getKey(); + $descendantId = $descendant->getKey(); + $columns = ['ancestor', 'descendant', 'depth']; + $ancestorRows = ClosureTable::where('descendant', '=', $ancestorId)->get($columns); + $descendantRows = ClosureTable::where('descendant', '=', $descendantId)->get($columns); + + static::assertEquals( + [ + 'ancestor' => $ancestorId, + 'descendant' => $ancestorId, + 'depth' => 0 + ], + $ancestorRows->get(0)->toArray() + ); + static::assertEquals( + [ + [ + 'ancestor' => $descendantId, + 'descendant' => $descendantId, + 'depth' => 0 + ], + [ + 'ancestor' => $ancestorId, + 'descendant' => $descendantId, + 'depth' => 1 + ], + ], + $descendantRows->toArray() + ); + } + + public function testMoveNodeToAnotherAncestor() + { + $descendant = Entity::find(1); + $descendant->moveTo(0, 2); + + $ancestors = ClosureTable::whereDescendant(2)->count(); + $descendants = ClosureTable::whereDescendant(1)->count(); + static::assertEquals(1, $ancestors); + static::assertEquals(2, $descendants); + } + + public function testMoveNodeToDeepNesting() + { + $item = Entity::find(1); + $item->moveTo(0, 2); + + $item = Entity::find(2); + $item->moveTo(0, 3); + + $item = Entity::find(3); + $item->moveTo(0, 4); + + $item = Entity::find(4); + $item->moveTo(0, 5); + + $descendantRows = ClosureTable::whereDescendant(1)->count(); + $ancestorRows = ClosureTable::whereDescendant(2)->count(); + + static::assertEquals(4, $ancestorRows); + static::assertEquals(5, $descendantRows); + } + + public function testMoveNodeToBecomeRoot() + { + $item = Entity::find(1); + $item->moveTo(0, 2); + + $item = Entity::find(2); + $item->moveTo(0, 3); + + $item = Entity::find(3); + $item->moveTo(0, 4); + + $item = Entity::find(4); + $item->moveTo(0, 5); + + $item = Entity::find(1); + $item->moveTo(0); + + static::assertEquals(1, ClosureTable::whereDescendant(1)->count()); + } +} diff --git a/tests/Models/Entity/ParentRootTests.php b/tests/Models/Entity/ParentRootTests.php new file mode 100644 index 0000000..4fe71b5 --- /dev/null +++ b/tests/Models/Entity/ParentRootTests.php @@ -0,0 +1,66 @@ +isParent()); + static::assertFalse((new Entity())->isRoot()); + } + + public function testExistingInstance() + { + static::assertTrue(Entity::find(9)->isParent()); + static::assertFalse(Entity::find(1)->isParent()); + static::assertTrue(Entity::find(1)->isRoot()); + static::assertFalse(Entity::find(10)->isRoot()); + } + + public function testGetParent() + { + $parent = Entity::find(10)->getParent(); + + static::assertInstanceOf(Entity::class, $parent); + static::assertEquals(9, $parent->getKey()); + static::assertNull(Entity::find(1)->getParent()); + static::assertNull((new Entity())->getParent()); + } + + public function testGetRoots() + { + $roots = Entity::getRoots(); + + static::assertCount(9, $roots); + + foreach ($roots as $idx => $root) { + static::assertEquals($idx + 1, $roots->get($idx)->getKey()); + } + } + + public function testMakeRoot() + { + $child = Entity::find(13); + + $child->makeRoot(4); + + static::assertTrue($child->isRoot()); + static::assertModelAttribute('position', [ + 10 => 0, + 14 => 1, + 15 => 2, + 5 => 5, + 6 => 6, + 7 => 7, + 8 => 8, + 9 => 9 + ]); + } +} diff --git a/tests/Models/Entity/PositioningTests.php b/tests/Models/Entity/PositioningTests.php new file mode 100644 index 0000000..2d4d95e --- /dev/null +++ b/tests/Models/Entity/PositioningTests.php @@ -0,0 +1,126 @@ +save(); + static::assertEquals(0, $entity1->position); + + $entity2 = new Entity; + $entity2->save(); + static::assertEquals(1, $entity2->position); + + static::assertModelAttribute('position', [16 => 0, 17 => 1]); + } + + public function testSavingLoadedEntityShouldNotTriggerReordering() + { + $entity = new Page(['title' => 'Item 1']); + $entity->save(); + + $id = $entity->getKey(); + $parentId = $entity->parent_id; + $position = $entity->position; + + $sameEntity = Page::find($id); + + // Sibling node that shouldn't move + static::assertEquals(8, Page::find(9)->position); + static::assertEquals( + $position, + $sameEntity->position, + 'Position should be the same after a load' + ); + + static::assertEquals( + $parentId, + $sameEntity->parent_id, + 'Parent should be the same after a load' + ); + + $sameEntity->title = 'New title'; + $sameEntity->save(); + + static::assertEquals( + 8, + Page::find(9)->position, + 'Sibling node should not have been moved' + ); + + static::assertEquals($id, $sameEntity->getKey()); + static::assertEquals($position, $sameEntity->position); + static::assertEquals($parentId, $sameEntity->parent_id); + } + + public function testMoveToTheFirstPosition() + { + $entity = Entity::find(9); + + $entity->position = 0; + $entity->save(); + + static::assertModelAttribute('position', [ + 9 => 0, + 1 => 1, + 2 => 2, + 3 => 3, + 4 => 4, + 5 => 5, + 6 => 6, + 7 => 7, + 8 => 8, + ]); + } + + public function testMoveToTheFifthPosition() + { + $entity = Entity::find(9); + + $entity->position = 5; + $entity->save(); + + static::assertModelAttribute('position', [ + 1 => 0, + 2 => 1, + 3 => 2, + 4 => 3, + 5 => 4, + 9 => 5, + 6 => 6, + 7 => 7, + 8 => 8, + ]); + } + + public function testMoveToPositionWhichIsOutOfTheUpperBound() + { + $entity = Entity::find(1); + + $entity->position = 999; + $entity->save(); + + static::assertModelAttribute('position', [ + 1 => 8, // + 2 => 0, + 3 => 1, + 4 => 2, + 5 => 3, + 6 => 4, + 7 => 5, + 8 => 6, + 9 => 7, + ]); + } +} diff --git a/tests/Models/Entity/SiblingManipulationTests.php b/tests/Models/Entity/SiblingManipulationTests.php new file mode 100644 index 0000000..52137c7 --- /dev/null +++ b/tests/Models/Entity/SiblingManipulationTests.php @@ -0,0 +1,84 @@ +addSibling(new Page(['title' => 'Foo!'])); + + $sibling = $entity->getNextSibling(); + static::assertEquals(4, $sibling->position); + static::assertEquals('Foo!', $sibling->title); + } + + public function testAddSiblingAtPosition() + { + $entity = Entity::find(15); + $sibling = new Page(['title' => 'Foo!']); + + $entity->addSibling($sibling, 1); + + static::assertEquals(16, $sibling->getKey()); + static::assertEquals(1, $sibling->position); + static::assertModelAttribute('position', [ + 10 => 0, + 16 => 1, + 13 => 2, + 14 => 3, + 15 => 4 + ]); + } + + public function testAddSiblings() + { + $entity = Entity::find(15); + $entity->addSiblings([ + new Page(['title' => 'One']), + new Page(['title' => 'Two']), + new Page(['title' => 'Three']), + new Page(['title' => 'Four']), + ]); + + $siblings = $entity->getNextSiblings(); + + static::assertCount(4, $siblings); + static::assertEquals(4, $siblings->get(0)->position); + static::assertEquals(5, $siblings->get(1)->position); + static::assertEquals(6, $siblings->get(2)->position); + static::assertEquals(7, $siblings->get(3)->position); + } + + public function testAddSiblingsFromPosition() + { + $entity = Entity::find(15); + + $entity->addSiblings([ + new Page(['title' => 'One']), + new Page(['title' => 'Two']), + new Page(['title' => 'Three']), + new Page(['title' => 'Four']), + ], 1); + + $siblings = $entity->getSiblingsRange(1, 4); + + static::assertEquals(0, Entity::find(10)->position); + static::assertEquals('One', $siblings->get(0)->title); + static::assertEquals('Two', $siblings->get(1)->title); + static::assertEquals('Three', $siblings->get(2)->title); + static::assertEquals('Four', $siblings->get(3)->title); + static::assertModelAttribute('position', [ + 10 => 0, + 13 => 5, + 14 => 6, + 15 => 7 + ]); + } +} diff --git a/tests/Models/Entity/SiblingQueryTests.php b/tests/Models/Entity/SiblingQueryTests.php new file mode 100644 index 0000000..700b024 --- /dev/null +++ b/tests/Models/Entity/SiblingQueryTests.php @@ -0,0 +1,208 @@ +getSiblings(); + + static::assertInstanceOf(Collection::class, $siblings); + static::assertCount(3, $siblings); + static::assertEquals(10, $siblings->get(0)->getKey()); + static::assertEquals(14, $siblings->get(1)->getKey()); + static::assertEquals(15, $siblings->get(2)->getKey()); + } + + public function testsCountSiblings() + { + static::assertEquals(3, Entity::find(13)->countSiblings()); + } + + public function testsHasSiblings() + { + static::assertTrue(Entity::find(13)->hasSiblings()); + } + + public function testsGetNeighbors() + { + $entity = Entity::find(13); + + $neighbors = $entity->getNeighbors(); + + static::assertCount(2, $neighbors); + static::assertEquals(10, $neighbors->get(0)->getKey()); + static::assertEquals(14, $neighbors->get(1)->getKey()); + } + + public function testsGetSiblingAt() + { + $entity = Entity::find(13); + + $first = $entity->getSiblingAt(0); + $third = $entity->getSiblingAt(2); + + static::assertEquals(10, $first->getKey()); + static::assertEquals(14, $third->getKey()); + } + + public function testGetFirstSibling() + { + static::assertEquals(10, Entity::find(13)->getFirstSibling()->getKey()); + } + + public function testGetLastSibling() + { + static::assertEquals(15, Entity::find(13)->getLastSibling()->getKey()); + } + + public function testGetPrevSibling() + { + static::assertEquals(14, Entity::find(15)->getPrevSibling()->getKey()); + } + + public function testGetPrevSiblings() + { + $entity = Entity::find(15); + + $siblings = $entity->getPrevSiblings(); + + static::assertCount(3, $siblings); + static::assertEquals(10, $siblings->get(0)->getKey()); + static::assertEquals(13, $siblings->get(1)->getKey()); + static::assertEquals(14, $siblings->get(2)->getKey()); + } + + public function testsCountPrevSiblings() + { + static::assertEquals(3, Entity::find(15)->countPrevSiblings()); + static::assertEquals(0, Entity::find(1)->countPrevSiblings()); + } + + public function testsHasPrevSiblings() + { + static::assertTrue(Entity::find(15)->hasPrevSiblings()); + static::assertFalse(Entity::find(1)->hasPrevSiblings()); + } + + public function testGetNextSibling() + { + static::assertEquals(13, Entity::find(10)->getNextSibling()->getKey()); + } + + public function testGetNextSiblings() + { + $entity = Entity::find(10); + + $siblings = $entity->getNextSiblings(); + + static::assertCount(3, $siblings); + static::assertEquals(13, $siblings->get(0)->getKey()); + static::assertEquals(14, $siblings->get(1)->getKey()); + static::assertEquals(15, $siblings->get(2)->getKey()); + } + + public function testCountNextSiblings() + { + static::assertEquals(3, Entity::find(10)->countNextSiblings()); + static::assertEquals(0, Entity::find(15)->countNextSiblings()); + } + + public function testsHasNextSiblings() + { + static::assertTrue(Entity::find(10)->hasNextSiblings()); + static::assertFalse(Entity::find(15)->hasNextSiblings()); + } + + public function testGetSiblingsRange() + { + $entity = Entity::find(15); + + $siblings = $entity->getSiblingsRange(1, 2); + + static::assertCount(2, $siblings); + static::assertEquals(13, $siblings->get(0)->getKey()); + static::assertEquals(14, $siblings->get(1)->getKey()); + } + + public function testGetSiblingsOpenRange() + { + static::assertCount(2, Entity::find(15)->getSiblingsRange(1)); + } + + public function testSiblingOfScope() + { + static::assertEquals([1, 2, 3, 4, 5, 6, 7, 8, 9], Entity::siblingOf(9)->get()->modelKeys()); + static::assertEquals([10, 13, 14, 15], Entity::siblingOf(10)->get()->modelKeys()); + } + + public function testSiblingsOfScope() + { + static::assertEquals([1, 2, 3, 4, 5, 6, 7, 8], Entity::siblingsOf(9)->get()->modelKeys()); + static::assertEquals([10, 14, 15], Entity::siblingsOf(13)->get()->modelKeys()); + } + + public function testNeighborsOfScope() + { + static::assertEquals([7, 9], Entity::neighborsOf(8)->get()->modelKeys()); + static::assertEquals([10, 14], Entity::neighborsOf(13)->get()->modelKeys()); + } + + public function testSiblingOfAtScope() + { + static::assertEquals([2], Entity::siblingOfAt(9, 1)->get()->modelKeys()); + static::assertEquals([14], Entity::siblingOfAt(10, 2)->get()->modelKeys()); + } + + + public function testFirstSiblingOfScope() + { + static::assertEquals([1], Entity::firstSiblingOf(9)->get()->modelKeys()); + static::assertEquals([10], Entity::firstSiblingOf(15)->get()->modelKeys()); + } + + public function testLastSiblingOfScope() + { + static::assertEquals([9], Entity::lastSiblingOf(1)->get()->modelKeys()); + static::assertEquals([15], Entity::lastSiblingOf(10)->get()->modelKeys()); + } + + public function testPrevSiblingOfScope() + { + static::assertEquals([8], Entity::prevSiblingOf(9)->get()->modelKeys()); + static::assertEquals([14], Entity::prevSiblingOf(15)->get()->modelKeys()); + } + + public function testPrevSiblingsOfScope() + { + static::assertEquals([1, 2, 3, 4, 5, 6, 7, 8], Entity::prevSiblingsOf(9)->get()->modelKeys()); + static::assertEquals([10, 13, 14], Entity::prevSiblingsOf(15)->get()->modelKeys()); + } + + public function testNextSiblingOfScope() + { + static::assertEquals([9], Entity::nextSiblingOf(8)->get()->modelKeys()); + static::assertEquals([15], Entity::nextSiblingOf(14)->get()->modelKeys()); + } + + public function testNextSiblingsOfScope() + { + static::assertEquals([2, 3, 4, 5, 6, 7, 8, 9], Entity::nextSiblingsOf(1)->get()->modelKeys()); + static::assertEquals([13, 14, 15], Entity::nextSiblingsOf(10)->get()->modelKeys()); + } + + public function testSiblingsRangeOfScope() + { + static::assertEquals([6, 7, 8, 9], Entity::siblingsRangeOf(1, 5)->get()->modelKeys()); + static::assertEquals([3, 4, 5, 6], Entity::siblingsRangeOf(1, 2, 5)->get()->modelKeys()); + static::assertEquals([13, 14, 15], Entity::siblingsRangeOf(10, 1)->get()->modelKeys()); + static::assertEquals([13, 14, 15], Entity::siblingsRangeOf(10, 1, 3)->get()->modelKeys()); + } +} diff --git a/tests/Models/Entity/TreeTests.php b/tests/Models/Entity/TreeTests.php new file mode 100644 index 0000000..ae09ec4 --- /dev/null +++ b/tests/Models/Entity/TreeTests.php @@ -0,0 +1,165 @@ +deleteSubtree(); + + static::assertEquals(1, Entity::whereBetween('id', [9, 15])->count()); + static::assertEquals(8, Entity::whereBetween('id', [1, 8])->count()); + } + + public function testDeleteSubtreeWithAncestor() + { + $entity = Entity::find(9); + $entity->deleteSubtree(true); + + static::assertEquals(0, Entity::whereBetween('id', [9, 15])->count()); + static::assertEquals(8, Entity::whereBetween('id', [1, 8])->count()); + } + + public function testForceDeleteSubtree() + { + $entity = Entity::find(9); + $entity->deleteSubtree(false, true); + + static::assertEquals(1, Entity::whereBetween('id', [9, 15])->count()); + static::assertEquals(1, ClosureTable::whereBetween('ancestor', [9, 15])->count()); + } + + public function testForceDeleteDeepSubtree() + { + Entity::find(9)->moveTo(0, 8); + Entity::find(8)->moveTo(0, 7); + Entity::find(7)->moveTo(0, 6); + Entity::find(6)->moveTo(0, 5); + Entity::find(5)->moveTo(0, 4); + Entity::find(4)->moveTo(0, 3); + Entity::find(3)->moveTo(0, 2); + Entity::find(2)->moveTo(0, 1); + + Entity::find(1)->deleteSubtree(false, true); + + static::assertEquals(1, Entity::whereBetween('id', [1, 9])->count()); + static::assertEquals(1, ClosureTable::whereBetween('ancestor', [1, 9])->count()); + } + + public function testForceDeleteSubtreeWithSelf() + { + $entity = Entity::find(9); + $entity->deleteSubtree(true, true); + + static::assertEquals(0, Entity::whereBetween('id', [9, 15])->count()); + static::assertEquals(0, ClosureTable::whereBetween('ancestor', [9, 15])->count()); + } + + public function testCreateFromArray() + { + $array = [ + [ + 'id' => 90, + 'title' => 'About', + 'position' => 0, + 'children' => [ + [ + 'id' => 93, + 'title' => 'Testimonials' + ] + ] + ], + [ + 'id' => 91, + 'title' => 'Blog', + 'position' => 1 + ], + [ + 'id' => 92, + 'title' => 'Portfolio', + 'position' => 2 + ], + ]; + + $pages = Page::createFromArray($array); + + static::assertCount(3, $pages); + + $pageZero = $pages->get(0); + static::assertEquals(90, $pageZero->getKey()); + static::assertEquals(91, $pages->get(1)->getKey()); + static::assertEquals(92, $pages->get(2)->getKey()); + static::assertEquals(93, $pageZero->getChildAt(0)->getKey()); + } + + public function testCreateFromArrayBug81() + { + $array = [ + [ + 'title' => 'About', + 'children' => [ + [ + 'title' => 'Testimonials', + 'children' => [ + [ + 'title' => 'child 1', + ], + [ + 'title' => 'child 2', + ], + ] + ] + ] + ], + [ + 'title' => 'Blog', + ], + [ + 'title' => 'Portfolio', + ], + ]; + + $pages = Page::createFromArray($array); + + $about = $pages[0]; + static::assertEquals('About', $about->title); + static::assertEquals(1, $about->countChildren()); + static::assertEquals(16, $about->getKey()); + + $blog = $pages[1]; + static::assertEquals('Blog', $blog->title); + static::assertEquals(0, $blog->countChildren()); + static::assertEquals(20, $blog->getKey()); + + $portfolio = $pages[2]; + static::assertEquals('Portfolio', $portfolio->title); + static::assertEquals(0, $portfolio->countChildren()); + static::assertEquals(21, $portfolio->getKey()); + + $pages = $pages[0]->getChildren(); + + $testimonials = $pages[0]; + static::assertEquals('Testimonials', $testimonials->title); + static::assertEquals(2, $testimonials->countChildren()); + static::assertEquals(17, $testimonials->getKey()); + + $pages = $pages[0]->getChildren(); + + $child1 = $pages[0]; + static::assertEquals('child 1', $child1->title); + static::assertEquals(0, $child1->countChildren()); + static::assertEquals(18, $child1->getKey()); + + $child2 = $pages[1]; + static::assertEquals('child 2', $child2->title); + static::assertEquals(0, $child2->countChildren()); + static::assertEquals(19, $child2->getKey()); + } +} diff --git a/tests/Page.php b/tests/Page.php new file mode 100644 index 0000000..4e83b9a --- /dev/null +++ b/tests/Page.php @@ -0,0 +1,10 @@ +increments('id'); $table->unsignedInteger('parent_id')->nullable(); $table->string('title')->default('The Title'); - $table->text('excerpt'); - $table->longText('body'); + $table->string('excerpt')->default(''); + $table->string('body')->default(''); $table->integer('position', false, true); - $table->integer('real_depth', false, true); $table->softDeletes(); $table->foreign('parent_id')->references('id')->on('entities')->onDelete('set null'); diff --git a/tests/models/Page.php b/tests/models/Page.php deleted file mode 100644 index 019bd5a..0000000 --- a/tests/models/Page.php +++ /dev/null @@ -1,10 +0,0 @@ -files() + ->ignoreVCS(false) + ->in(__DIR__) + ->name('*.php') + ->notName('script-change-testcase-return-type.php') + ->contains('use Orchestra\Testbench\TestCase;'); + +foreach ($finder as $file) { + $path = $file->getRealPath(); + $contents = file_get_contents($path); + $contents = str_replace( + ['public function setUp()', 'public function tearDown()'], + ['public function setUp(): void', 'public function tearDown(): void'], + $contents + ); + + file_put_contents($path, $contents); +} \ No newline at end of file