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/Column
+
Customized by
+
Meaning
+
+
+
parent_id
+
Entity::getParentIdColumn()
+
ID of the node's immediate parent, simplifies queries for immediate parent/child nodes.
+
+
+
position
+
Entity::getPositionColumn()
+
Node position, allows to order nodes of the same depth level
+
+
+
ClosureTable
+
+
+
Attribute/Column
+
Customized by
+
Meaning
+
+
+
id
+
+
+
+
+
ancestor
+
ClosureTable::getAncestorColumn()
+
Parent (self, immediate, distant) node ID
+
+
+
descendant
+
ClosureTable::getDescendantColumn()
+
Child (self, immediate, distant) node ID
+
+
+
depth
+
ClosureTable::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