├── _config.yml ├── CHANGELOG.md ├── .editorconfig ├── LICENSE.md ├── composer.json ├── CONTRIBUTING.md ├── README.md ├── src ├── NestedSetObserver.php └── NestedSetTrait.php └── .php_cs /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-midnight -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All Notable changes to `:package_name` will be documented in this file 4 | 5 | ## NEXT - YYYY-MM-DD 6 | 7 | ### Added 8 | - Nothing 9 | 10 | ### Deprecated 11 | - Nothing 12 | 13 | ### Fixed 14 | - Nothing 15 | 16 | ### Removed 17 | - Nothing 18 | 19 | ### Security 20 | - Nothing 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = tab 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Menno van Ens 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codefocus/nestedset", 3 | "description": "Nested set implementation for Eloquent models in Laravel.", 4 | "keywords": [ 5 | "codefocus", 6 | "nestedset", 7 | "nested set", 8 | "hierarchy", 9 | "hierarchical", 10 | "tree" 11 | ], 12 | "homepage": "https://github.com/codefocus/nestedset", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Menno van Ens", 17 | "email": "info@codefocus.ca", 18 | "homepage": "http://www.codefocus.ca/", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php" : ">=5.4.0" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit" : "4.*", 27 | "scrutinizer/ocular": "~1.1" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Codefocus\\NestedSet\\": "src" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Codefocus\\NestedSet\\Test\\": "tests" 37 | } 38 | }, 39 | "scripts": { 40 | "test": "phpunit" 41 | }, 42 | "extra": { 43 | "branch-alias": { 44 | "dev-master": "0.2.1-dev" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/codefocus/nestedset). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NestedSet 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Software License][ico-license]](LICENSE.md) 5 | [![Build Status][ico-travis]][link-travis] 6 | [![Total Downloads][ico-downloads]][link-downloads] 7 | 8 | Simple to use implementation of the nested set structure, for Eloquent models in Laravel. 9 | 10 | ## CAUTION 11 | 12 | **This package is currently INCOMPLETE and deployment in a production environment is NOT RECOMMENDED** 13 | 14 | To learn more about the nested set structure, please refer to "[Efficient tree retrieval in Laravel using the nested set structure](http://www.codefocus.ca/blog/efficient-tree-retrieval-in-laravel-using-the-nested-set)" on [codefocus.ca](http://www.codefocus.ca/). 15 | 16 | ## Table of contents 17 | 18 | * [Install](#install) 19 | * [Configuration](#configuration) 20 | * [Enabling the nested set functionality in your model](#enabling-the-nested-set-functionality-in-your-model) 21 | * [Database columns](#database-columns) 22 | * [Database indexes](#database-indexes) 23 | * [Usage](#usage) 24 | * [Building a tree](#building-a-tree) 25 | * [Building a new tree from an existing parent-child based data structure](#building-a-new-tree-from-an-existing-parent-child-based-data-structure) 26 | * [Rebuilding the tree under an existing node](#rebuilding-the-tree-under-an-existing-node) 27 | * [Adding a node](#adding-a-node) 28 | * [Moving a node](#moving-a-node) 29 | * [Removing a node](#removing-a-node) 30 | * [Testing](#testing) 31 | * [Contributing](#contributing) 32 | * [Security](#security) 33 | * [Credits](#credits) 34 | 35 | ## Install 36 | 37 | Via Composer 38 | 39 | ``` bash 40 | $ composer require codefocus/nestedset 41 | ``` 42 | 43 | ## Configuration 44 | 45 | ### Enabling the nested set functionality in your model 46 | 47 | To implement the NestedSetTrait, simply `use` it in your model: 48 | 49 | ``` php 50 | class YourModel extends \Illuminate\Database\Eloquent\Model { 51 | 52 | use \Codefocus\NestedSet\NestedSetTrait; 53 | ... 54 | 55 | } 56 | ``` 57 | 58 | ### Database columns 59 | 60 | The Trait expects database columns to be present for (at least) your Model's `left`, `right` and `parent_id` fields. 61 | The names of these fields can be configured per Model, 62 | by setting the following protected variables in the Model that uses it: 63 | 64 | ``` php 65 | protected $nestedSetColumns = [ 66 | // Which column to use for the "left" value. 67 | // Default: left 68 | 'left' => 'left', 69 | 70 | // Which column to use for the "right" value. 71 | // Default: right 72 | 'right' => 'right', 73 | 74 | // Which column to point to the parent's PK. 75 | // Null is allowed. This will remove the ability to rebuild the tree. 76 | // Default: parent_id 77 | 'parent' => 'parent_id', 78 | 79 | // Which column to use for the node's "depth", or level in the tree. 80 | // Null is allowed. 81 | // ! When restricting the tree by depth, each node's depth will be 82 | // calculated automatically. This is not recommended for large trees. 83 | // Default: null 84 | 'depth' => null, 85 | 86 | // When a table can hold multiple trees, we need to specify which field 87 | // uniquely identifies which tree we are operating on. 88 | // E.g. in the case of comments, that could be "thread_id" or "post_id". 89 | // Null is allowed. NestedSetTrait will assume there is only one tree. 90 | // Default: null 91 | 'group' => null, 92 | ]; 93 | ``` 94 | 95 | ### Database indexes 96 | 97 | Indexes are highly recommended on these fields (or the ones configured in `$nestedSetColumns`): 98 | 99 | - `left`, `right`, `group`, `depth` 100 | - `left`, `group`, `depth` 101 | - `parent_id` 102 | 103 | If you are not using `depth` and `group`, these indexes will suffice: 104 | 105 | - `left`, `right` 106 | - `parent_id` 107 | 108 | ## Usage 109 | 110 | ### Building a tree 111 | 112 | #### Building a new tree from an existing parent-child based data structure 113 | 114 | **@TODO: incomplete** 115 | 116 | Use your data's existing parent → child hierarchy to construct a new tree 117 | (or multiple trees, if you have configured the `$nestedSetColumns['group']` column in your model). 118 | 119 | This may take a while, depending on the size of your data set! 120 | 121 | ``` php 122 | YourModel::buildNewTree(); 123 | ``` 124 | 125 | #### Rebuilding the tree under an existing node 126 | 127 | **@TODO: incomplete** 128 | 129 | Use your data's existing parent → child hierarchy to (re)construct (part of the) tree, from the current node downward. 130 | 131 | ``` php 132 | $yourModelInstance->buildTree(); 133 | ``` 134 | 135 | #### Adding a node 136 | 137 | Adding a node to the tree requires literally no work. 138 | Just save a model instance as usual, and the Trait will automagically adjust the tree structure. 139 | 140 | ``` php 141 | $yourModelInstance->save(); 142 | ``` 143 | 144 | #### Moving a node 145 | 146 | **@TODO: incomplete** 147 | 148 | Moving a node from one parent to another (or no parent) is handled in the same way. 149 | When the Trait sees that a model instance's `parent_id` (or the column name configured in `$nestedSetColumns['parent']`) value has changed, the tree structure is adjusted accordingly. 150 | 151 | ``` php 152 | $yourModelInstance->parent_id = $newParent->id; 153 | $yourModelInstance->save(); 154 | ``` 155 | 156 | #### Removing a node 157 | 158 | **@TODO: incomplete** 159 | 160 | Deleting a node from the tree is also automated by the Trait. 161 | When you delete a model instance as usual, the Trait will adjust the tree structure. 162 | 163 | ``` php 164 | $yourModelInstance->delete(); 165 | ``` 166 | 167 | ## Testing 168 | 169 | ``` bash 170 | $ composer test 171 | ``` 172 | 173 | ## Contributing 174 | 175 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 176 | 177 | ## Security 178 | 179 | If you discover any security related issues, please email info@codefocus.ca instead of using the issue tracker. 180 | 181 | ## Credits 182 | 183 | - [Menno van Ens][link-author] 184 | - [All Contributors][link-contributors] 185 | 186 | ## License 187 | 188 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 189 | 190 | [ico-version]: https://img.shields.io/packagist/v/codefocus/nestedset.svg?style=flat-square 191 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 192 | [ico-travis]: https://img.shields.io/travis/codefocus/nestedset/master.svg?style=flat-square 193 | [ico-downloads]: https://img.shields.io/packagist/dt/codefocus/nestedset.svg?style=flat-square 194 | 195 | [link-packagist]: https://packagist.org/packages/codefocus/nestedset 196 | [link-travis]: https://travis-ci.org/codefocus/nestedset 197 | [link-downloads]: https://packagist.org/packages/codefocus/nestedset 198 | [link-author]: https://github.com/codefocus 199 | [link-contributors]: ../../contributors 200 | -------------------------------------------------------------------------------- /src/NestedSetObserver.php: -------------------------------------------------------------------------------- 1 | getKeyName(); 20 | $leftColumn = $model->getLeftColumn(); 21 | $rightColumn = $model->getRightColumn(); 22 | $parentColumn = $model->getParentColumn(); 23 | $depthColumn = $model->getDepthColumn(); 24 | $groupColumn = $model->getGroupColumn(); 25 | 26 | if (empty($model->{$parentColumn})) { 27 | // No parent referenced. 28 | // We're creating a node at the root level of the tree. 29 | if (!is_null($groupColumn)) { 30 | // Group column is configured. 31 | // Get the max "right" value in the group to which this node belongs. 32 | $currentMaxRight = $model::where($groupColumn, '=', $model->{$groupColumn}) 33 | ->max($rightColumn); 34 | } else { 35 | // Group column is not configured. 36 | // Get the max "right" value across the whole table. 37 | $currentMaxRight = $model::max($rightColumn); 38 | } 39 | // Ensure we have a positive number. 40 | $currentMaxRight = max(0, $currentMaxRight); 41 | 42 | // Define the node's new "left" and "right". 43 | $model->{$leftColumn} = $currentMaxRight + 1; 44 | $model->{$rightColumn} = $currentMaxRight + 2; 45 | 46 | // If a depth column is configured, set that to 0, 47 | // indicating we're at the root level. 48 | if (!empty($depthColumn)) { 49 | $model->{$depthColumn} = 0; 50 | } 51 | } else { 52 | // A parent node is referenced by this node. 53 | // We're creating a child. 54 | 55 | // Get the parent and its "left" and "right" values. 56 | $parent = $model::findOrFail( 57 | $model->{$parentColumn}, 58 | array_filter([ 59 | $pkColumn, 60 | $leftColumn, 61 | $rightColumn, 62 | $depthColumn, 63 | $groupColumn, 64 | ]) 65 | ); 66 | 67 | // Define the node's new "left" and "right". 68 | // We're inserting ourselves at the rightmost side of our parent container. 69 | $model->{$leftColumn} = $parent->{$rightColumn}; 70 | $model->{$rightColumn} = $parent->{$rightColumn} + 1; 71 | 72 | // Make space. 73 | if (!is_null($groupColumn)) { 74 | // Group column is configured. 75 | // Only move nodes in the same group as our parent. 76 | $model::where($groupColumn, '=', $model->{$groupColumn}) 77 | ->where($rightColumn, '>=', $parent->{$rightColumn}) 78 | ->increment($rightColumn, 2); 79 | $model::where($groupColumn, '=', $model->{$groupColumn}) 80 | ->where($leftColumn, '>=', $parent->{$rightColumn}) 81 | ->increment($leftColumn, 2); 82 | } else { 83 | // Group column is not configured. 84 | // Move nodes across the whole table. 85 | $model::where($rightColumn, '>=', $parent->{$rightColumn}) 86 | ->increment($rightColumn, 2); 87 | $model::where($leftColumn, '>=', $parent->{$rightColumn}) 88 | ->increment($leftColumn, 2); 89 | } 90 | 91 | if (!is_null($groupColumn)) { 92 | // Group column is configured. 93 | // Ensure we live in the same group as our parent. 94 | $model->{$groupColumn} = $parent->{$groupColumn}; 95 | } 96 | 97 | if (!empty($depthColumn)) { 98 | // Depth column is configured. 99 | // Set our depth one level deeper than our parent. 100 | $model->{$depthColumn} = $parent->{$depthColumn} + 1; 101 | } 102 | } 103 | } 104 | 105 | public function updating(Model $model) 106 | { 107 | // An node is being updated. 108 | // Rebuild the tree if the parent was changed. 109 | 110 | // Get column names. 111 | // Missing required columns will throw an exception. 112 | $pkColumn = $model->getKeyName(); 113 | $leftColumn = $model->getLeftColumn(); 114 | $rightColumn = $model->getRightColumn(); 115 | $parentColumn = $model->getParentColumn(); 116 | $depthColumn = $model->getDepthColumn(); 117 | $groupColumn = $model->getGroupColumn(); 118 | 119 | $originalParentId = $model->getOriginal($parentColumn); 120 | if ($originalParentId !== $model->{$parentColumn}) { 121 | $model->syncOriginalAttribute($parentColumn); 122 | 123 | echo 'parent changed from "' . $originalParentId . '" to "' . $model->{$parentColumn} . '"'; 124 | 125 | if (!is_null($originalParentId)) { 126 | // Load original parent 127 | $originalParent = $model::findOrFail( 128 | $originalParentId, 129 | array_filter([ 130 | $pkColumn, 131 | $leftColumn, 132 | $rightColumn, 133 | $depthColumn, 134 | $groupColumn, 135 | ]) 136 | ); 137 | } 138 | if (!is_null($model->{$parentColumn})) { 139 | // Load new parent 140 | $newParent = $model::findOrFail( 141 | $model->{$parentColumn}, 142 | array_filter([ 143 | $pkColumn, 144 | $leftColumn, 145 | $rightColumn, 146 | $depthColumn, 147 | $groupColumn, 148 | ]) 149 | ); 150 | if ($newParent->{$leftColumn} > $model->{$leftColumn} and $newParent->{$leftColumn} < $model->{$rightColumn}) { 151 | // This change would create a circular tree. 152 | throw new \Exception('Changing this node\'s parent to ' . $model->{$parentColumn} . ' creates a circular reference'); 153 | } 154 | } 155 | 156 | //$originalParent = $model->where 157 | 158 | // @TODO: Move this to the Trait. Override save(). 159 | 160 | dd($modelId); 161 | 162 | /* 163 | SELECT id FROM node 164 | WHERE node.left BETWEEN 1 AND 10 165 | AND node.id = 8 166 | */ 167 | 168 | // Rebuild the entire tree. 169 | // @TODO: Be smarter about this method. 170 | dd($model); 171 | 172 | $rootNode = $model::where($parentColumn, '=', null)->first(); 173 | $rootNode->buildTree(); 174 | } 175 | 176 | return true; 177 | } 178 | 179 | public function deleting(Model $model) 180 | { 181 | 182 | // Get column names. 183 | // Missing required columns will throw an exception. 184 | $pkColumn = $model->getKeyName(); 185 | $leftColumn = $model->getLeftColumn(); 186 | $rightColumn = $model->getRightColumn(); 187 | $parentColumn = $model->getParentColumn(); 188 | 189 | // A node is being deleted. 190 | // Rebuild the entire tree. 191 | echo 'node deleted'; 192 | // @TODO: Be smarter about this method. 193 | dd($model); 194 | $rootNode = $model::where($parentColumn, '=', null)->first(); 195 | $rootNode->buildTree(); 196 | 197 | return true; 198 | } 199 | } // NestedSetObserver 200 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 5 | ->setRules([ 6 | 'align_multiline_comment' => ['comment_type' => 'phpdocs_like'], 7 | 'array_indentation' => true, 8 | 'array_syntax' => ['syntax' => 'short'], 9 | 'backtick_to_shell_exec' => true, 10 | 'binary_operator_spaces' => ['align_double_arrow' => false, 'align_equals' => false], 11 | 'blank_line_after_namespace' => true, 12 | 'blank_line_after_opening_tag' => true, 13 | 'blank_line_before_return' => true, 14 | 'braces' => ['allow_single_line_closure' => false, 'position_after_anonymous_constructs' => 'same', 'position_after_control_structures' => 'same', 'position_after_functions_and_oop_constructs' => 'next'], 15 | 'cast_spaces' => ['space' => 'single'], 16 | 'class_attributes_separation' => true, 17 | 'class_definition' => ['multiLineExtendsEachSingleLine' => false, 'singleItemSingleLine' => true, 'singleLine' => true], 18 | 'combine_consecutive_issets' => true, 19 | 'combine_consecutive_unsets' => true, 20 | 'comment_to_phpdoc' => true, 21 | 'compact_nullable_typehint' => true, 22 | 'concat_space' => ['spacing' => 'one'], 23 | 'declare_equal_normalize' => ['space' => 'single'], 24 | 'dir_constant' => true, 25 | 'elseif' => true, 26 | 'encoding' => true, 27 | 'escape_implicit_backslashes' => ['single_quoted' => true], 28 | 'explicit_indirect_variable' => true, 29 | 'explicit_string_variable' => true, 30 | 'full_opening_tag' => true, 31 | 'fully_qualified_strict_types' => true, 32 | 'function_declaration' => ['closure_function_spacing' => 'one'], 33 | 'function_to_constant' => true, 34 | 'function_typehint_space' => true, 35 | 'general_phpdoc_annotation_remove' => true, 36 | 'heredoc_to_nowdoc' => true, 37 | 'include' => true, 38 | 'increment_style' => ['style' => 'pre'], 39 | 'indentation_type' => true, 40 | 'line_ending' => true, 41 | 'linebreak_after_opening_tag' => true, 42 | 'list_syntax' => ['syntax' => 'short'], 43 | 'lowercase_cast' => true, 44 | 'lowercase_constants' => true, 45 | 'lowercase_keywords' => true, 46 | 'magic_constant_casing' => true, 47 | 'mb_str_functions' => true, 48 | 'method_argument_space' => ['ensure_fully_multiline' => true, 'keep_multiple_spaces_after_comma' => false], 49 | 'method_chaining_indentation' => true, 50 | 'modernize_types_casting' => true, 51 | 'multiline_comment_opening_closing' => true, 52 | 'multiline_whitespace_before_semicolons' => ['strategy' => 'no_multi_line'], 53 | 'native_function_casing' => true, 54 | 'new_with_braces' => true, 55 | 'no_alias_functions' => true, 56 | 'no_alternative_syntax' => true, 57 | 'no_blank_lines_after_class_opening' => true, 58 | 'no_blank_lines_after_phpdoc' => true, 59 | // 'no_blank_lines_before_namespace' => true, 60 | 'no_break_comment' => true, 61 | 'no_closing_tag' => true, 62 | 'no_empty_comment' => true, 63 | 'no_empty_phpdoc' => true, 64 | 'no_empty_statement' => true, 65 | 'no_extra_blank_lines' => true, 66 | 'no_homoglyph_names' => true, 67 | 'no_leading_import_slash' => true, 68 | 'no_leading_namespace_whitespace' => true, 69 | 'no_mixed_echo_print' => ['use' => 'echo'], 70 | 'no_multiline_whitespace_around_double_arrow' => true, 71 | 'no_null_property_initialization' => true, 72 | 'no_short_bool_cast' => true, 73 | 'no_short_echo_tag' => true, 74 | 'no_singleline_whitespace_before_semicolons' => true, 75 | 'no_spaces_after_function_name' => true, 76 | 'no_spaces_around_offset' => true, 77 | 'no_spaces_inside_parenthesis' => true, 78 | 'no_superfluous_elseif' => true, 79 | 'no_trailing_comma_in_list_call' => true, 80 | 'no_trailing_comma_in_singleline_array' => true, 81 | 'no_trailing_whitespace' => true, 82 | 'no_trailing_whitespace_in_comment' => true, 83 | 'no_unneeded_control_parentheses' => true, 84 | 'no_unneeded_curly_braces' => true, 85 | 'no_unneeded_final_method' => true, 86 | 'no_unused_imports' => true, 87 | 'no_useless_else' => true, 88 | 'no_useless_return' => true, 89 | 'no_whitespace_before_comma_in_array' => true, 90 | 'no_whitespace_in_blank_line' => true, 91 | 'non_printable_character' => true, 92 | 'normalize_index_brace' => true, 93 | 'object_operator_without_whitespace' => true, 94 | 'ordered_imports' => true, 95 | 'phpdoc_align' => true, 96 | 'phpdoc_indent' => true, 97 | 'phpdoc_inline_tag' => true, 98 | 'phpdoc_no_access' => true, 99 | 'phpdoc_no_alias_tag' => true, 100 | 'phpdoc_no_empty_return' => true, 101 | 'phpdoc_no_package' => true, 102 | 'phpdoc_no_useless_inheritdoc' => true, 103 | 'phpdoc_order' => true, 104 | 'phpdoc_return_self_reference' => true, 105 | 'phpdoc_scalar' => true, 106 | 'phpdoc_separation' => true, 107 | 'phpdoc_single_line_var_spacing' => true, 108 | 'phpdoc_summary' => true, 109 | 'phpdoc_to_comment' => true, 110 | 'phpdoc_trim' => true, 111 | 'phpdoc_types' => true, 112 | 'phpdoc_var_without_name' => true, 113 | 'pow_to_exponentiation' => true, 114 | 'random_api_migration' => true, 115 | 'return_type_declaration' => ['space_before' => 'none'], 116 | 'self_accessor' => true, 117 | 'semicolon_after_instruction' => true, 118 | 'short_scalar_cast' => true, 119 | 'simplified_null_return' => true, 120 | 'single_blank_line_at_eof' => true, 121 | 'single_blank_line_before_namespace' => true, 122 | 'single_class_element_per_statement' => true, 123 | 'single_import_per_statement' => true, 124 | 'single_line_after_imports' => true, 125 | 'single_line_comment_style' => true, 126 | 'single_quote' => true, 127 | 'standardize_increment' => true, 128 | 'standardize_not_equals' => true, 129 | 'string_line_ending' => true, 130 | 'switch_case_semicolon_to_colon' => true, 131 | 'switch_case_space' => true, 132 | 'ternary_operator_spaces' => true, 133 | 'ternary_to_null_coalescing' => true, 134 | 'trailing_comma_in_multiline_array' => true, 135 | 'trim_array_spaces' => true, 136 | 'unary_operator_spaces' => true, 137 | 'visibility_required' => true, 138 | 'whitespace_after_comma_in_array' => true, 139 | ]) 140 | ->setFinder(PhpCsFixer\Finder::create() 141 | ->exclude('vendor') 142 | ->in(__DIR__) 143 | ) 144 | ; 145 | 146 | /* 147 | This document has been generated with 148 | https://mlocati.github.io/php-cs-fixer-configurator/ 149 | you can change this configuration by importing this YAML code: 150 | 151 | fixers: 152 | align_multiline_comment: 153 | comment_type: phpdocs_like 154 | array_indentation: true 155 | array_syntax: 156 | syntax: short 157 | backtick_to_shell_exec: true 158 | binary_operator_spaces: 159 | align_double_arrow: false 160 | align_equals: false 161 | blank_line_after_namespace: true 162 | blank_line_after_opening_tag: true 163 | blank_line_before_return: true 164 | braces: 165 | allow_single_line_closure: false 166 | position_after_anonymous_constructs: same 167 | position_after_control_structures: same 168 | position_after_functions_and_oop_constructs: next 169 | cast_spaces: 170 | space: single 171 | class_attributes_separation: true 172 | class_definition: 173 | multiLineExtendsEachSingleLine: false 174 | singleItemSingleLine: true 175 | singleLine: true 176 | combine_consecutive_issets: true 177 | combine_consecutive_unsets: true 178 | comment_to_phpdoc: true 179 | compact_nullable_typehint: true 180 | concat_space: 181 | spacing: one 182 | declare_equal_normalize: 183 | space: single 184 | dir_constant: true 185 | elseif: true 186 | encoding: true 187 | escape_implicit_backslashes: 188 | single_quoted: true 189 | explicit_indirect_variable: true 190 | explicit_string_variable: true 191 | full_opening_tag: true 192 | fully_qualified_strict_types: true 193 | function_declaration: 194 | closure_function_spacing: one 195 | function_to_constant: true 196 | function_typehint_space: true 197 | general_phpdoc_annotation_remove: true 198 | heredoc_to_nowdoc: true 199 | include: true 200 | increment_style: 201 | style: pre 202 | indentation_type: true 203 | line_ending: true 204 | linebreak_after_opening_tag: true 205 | list_syntax: 206 | syntax: short 207 | lowercase_cast: true 208 | lowercase_constants: true 209 | lowercase_keywords: true 210 | magic_constant_casing: true 211 | mb_str_functions: true 212 | method_argument_space: 213 | ensure_fully_multiline: true 214 | keep_multiple_spaces_after_comma: false 215 | method_chaining_indentation: true 216 | modernize_types_casting: true 217 | multiline_comment_opening_closing: true 218 | multiline_whitespace_before_semicolons: 219 | strategy: no_multi_line 220 | native_function_casing: true 221 | new_with_braces: true 222 | no_alias_functions: true 223 | no_alternative_syntax: true 224 | no_blank_lines_after_class_opening: true 225 | no_blank_lines_after_phpdoc: true 226 | no_blank_lines_before_namespace: true 227 | no_break_comment: true 228 | no_closing_tag: true 229 | no_empty_comment: true 230 | no_empty_phpdoc: true 231 | no_empty_statement: true 232 | no_extra_blank_lines: true 233 | no_homoglyph_names: true 234 | no_leading_import_slash: true 235 | no_leading_namespace_whitespace: true 236 | no_mixed_echo_print: 237 | use: echo 238 | no_multiline_whitespace_around_double_arrow: true 239 | no_null_property_initialization: true 240 | no_short_bool_cast: true 241 | no_short_echo_tag: true 242 | no_singleline_whitespace_before_semicolons: true 243 | no_spaces_after_function_name: true 244 | no_spaces_around_offset: true 245 | no_spaces_inside_parenthesis: true 246 | no_superfluous_elseif: true 247 | no_trailing_comma_in_list_call: true 248 | no_trailing_comma_in_singleline_array: true 249 | no_trailing_whitespace: true 250 | no_trailing_whitespace_in_comment: true 251 | no_unneeded_control_parentheses: true 252 | no_unneeded_curly_braces: true 253 | no_unneeded_final_method: true 254 | no_unused_imports: true 255 | no_useless_else: true 256 | no_useless_return: true 257 | no_whitespace_before_comma_in_array: true 258 | no_whitespace_in_blank_line: true 259 | non_printable_character: true 260 | normalize_index_brace: true 261 | object_operator_without_whitespace: true 262 | ordered_imports: true 263 | phpdoc_align: true 264 | phpdoc_indent: true 265 | phpdoc_inline_tag: true 266 | phpdoc_no_access: true 267 | phpdoc_no_alias_tag: true 268 | phpdoc_no_empty_return: true 269 | phpdoc_no_package: true 270 | phpdoc_no_useless_inheritdoc: true 271 | phpdoc_order: true 272 | phpdoc_return_self_reference: true 273 | phpdoc_scalar: true 274 | phpdoc_separation: true 275 | phpdoc_single_line_var_spacing: true 276 | phpdoc_summary: true 277 | phpdoc_to_comment: true 278 | phpdoc_trim: true 279 | phpdoc_types: true 280 | phpdoc_var_without_name: true 281 | pow_to_exponentiation: true 282 | random_api_migration: true 283 | return_type_declaration: 284 | space_before: none 285 | self_accessor: true 286 | semicolon_after_instruction: true 287 | short_scalar_cast: true 288 | simplified_null_return: true 289 | single_blank_line_at_eof: true 290 | single_blank_line_before_namespace: true 291 | single_class_element_per_statement: true 292 | single_import_per_statement: true 293 | single_line_after_imports: true 294 | single_line_comment_style: true 295 | single_quote: true 296 | standardize_increment: true 297 | standardize_not_equals: true 298 | string_line_ending: true 299 | switch_case_semicolon_to_colon: true 300 | switch_case_space: true 301 | ternary_operator_spaces: true 302 | ternary_to_null_coalescing: true 303 | trailing_comma_in_multiline_array: true 304 | trim_array_spaces: true 305 | unary_operator_spaces: true 306 | visibility_required: true 307 | whitespace_after_comma_in_array: true 308 | risky: true 309 | 310 | */ 311 | -------------------------------------------------------------------------------- /src/NestedSetTrait.php: -------------------------------------------------------------------------------- 1 | 'left', 19 | 'right' => 'right', 20 | 'parent' => 'parent_id', 21 | 'depth' => null, 22 | 'group' => null, 23 | ]; 24 | 25 | private $requiredColumns = [ 26 | 'left', 27 | 'right', 28 | 'parent', 29 | ]; 30 | 31 | /** 32 | * Observe the Model. 33 | */ 34 | public static function bootNestedSetTrait() 35 | { 36 | static::observe(NestedSetObserver::class); 37 | } 38 | 39 | /** 40 | * Get the name of the "left" column. 41 | * 42 | * @return string 43 | */ 44 | public function getLeftColumn() 45 | { 46 | return $this->getColumnName('left'); 47 | } 48 | 49 | /** 50 | * Get the qualified name of the "left" column. 51 | * 52 | * @return string 53 | */ 54 | public function getQualifiedLeftColumn() 55 | { 56 | return $this->getQualifiedColumnName('left'); 57 | } 58 | 59 | /** 60 | * Get the name of the "right" column. 61 | * 62 | * @return string 63 | */ 64 | public function getRightColumn() 65 | { 66 | return $this->getColumnName('right'); 67 | } 68 | 69 | /** 70 | * Get the qualified name of the "right" column. 71 | * 72 | * @return string 73 | */ 74 | public function getQualifiedRightColumn() 75 | { 76 | return $this->getQualifiedColumnName('right'); 77 | } 78 | 79 | /** 80 | * Get the name of the "parent" column. 81 | * 82 | * @return string 83 | */ 84 | public function getParentColumn() 85 | { 86 | return $this->getColumnName('parent'); 87 | } 88 | 89 | /** 90 | * Get the qualified name of the "parent" column. 91 | * 92 | * @return string 93 | */ 94 | public function getQualifiedParentColumn() 95 | { 96 | return $this->getQualifiedColumnName('parent'); 97 | } 98 | 99 | /** 100 | * Get the name of the "depth" column. 101 | * 102 | * @return string 103 | */ 104 | public function getDepthColumn() 105 | { 106 | return $this->getColumnName('depth'); 107 | } 108 | 109 | /** 110 | * Get the qualified name of the "depth" column. 111 | * 112 | * @return string 113 | */ 114 | public function getQualifiedDepthColumn() 115 | { 116 | return $this->getQualifiedColumnName('depth'); 117 | } 118 | 119 | /** 120 | * Get the name of the "group" column. 121 | * 122 | * @return string 123 | */ 124 | public function getGroupColumn() 125 | { 126 | return $this->getColumnName('group'); 127 | } 128 | 129 | /** 130 | * Get the qualified name of the "group" column. 131 | * 132 | * @return string 133 | */ 134 | public function getQualifiedGroupColumn() 135 | { 136 | return $this->getQualifiedColumnName('group'); 137 | } 138 | 139 | /** 140 | * Get the name of one of the configurable columns. 141 | * 142 | * @return string 143 | */ 144 | private function getColumnName($column) 145 | { 146 | if (isset($this->nestedSetColumns[$column])) { 147 | if (empty($this->nestedSetColumns[$column]) and in_array($column, $this->requiredColumns)) { 148 | // Throw an exception if a required column is empty. 149 | throw new \UnexpectedValueException('"' . $column . '" column cannot be empty in ' . get_class($model)); 150 | } 151 | 152 | return $this->nestedSetColumns[$column]; 153 | } 154 | 155 | return $this->defaultColumns[$column]; 156 | } 157 | 158 | /** 159 | * Get the qualified name of one of the configurable columns. 160 | * 161 | * @return string 162 | */ 163 | private function getQualifiedColumnName($column) 164 | { 165 | $columnName = $this->getColumnName($column); 166 | if (empty($columnName)) { 167 | return; 168 | } 169 | 170 | return $this->getTable() . '.' . $columnName; 171 | } 172 | 173 | /** 174 | * Ensure depth is available in the query result, 175 | * whether through the depth column or by calculating. 176 | * 177 | * @param Builder $builder 178 | * 179 | * @return Builder 180 | */ 181 | public function scopeWithDepth(Builder $builder) 182 | { 183 | $depthColumn = $this->getDepthColumn(); 184 | if (is_null($depthColumn)) { 185 | return $this->scopeWithCalculatedDepth($builder); 186 | } 187 | 188 | return $builder; 189 | } 190 | 191 | /** 192 | * Add a calculated depth value to each query result, 193 | * if the depth column is not configured. 194 | * 195 | * @param Builder $builder 196 | * 197 | * @return Builder 198 | */ 199 | public function scopeWithCalculatedDepth(Builder $builder) 200 | { 201 | $tableName = $this->getTable(); 202 | $query = $builder->getQuery(); 203 | $grammar = $query->getGrammar(); 204 | // Build subquery 205 | $subquery = \DB::table($tableName . ' AS ns_d') 206 | ->selectRaw('COUNT(ns_d.left)') 207 | ->whereRaw($grammar->wrap($tableName) . '.left BETWEEN ns_d.left AND ns_d.right') 208 | ->toSql(); 209 | 210 | $depthColumn = '(' . $subquery . ') AS depth'; 211 | // Get requested columns and add "*" if none are specified yet, 212 | // so we don't just select our subquery. 213 | $query = $builder->getQuery(); 214 | if (is_null($query->columns)) { 215 | $query->columns = ['*']; 216 | } 217 | // Add depth subquery as a column 218 | return $query->selectRaw($depthColumn); 219 | } 220 | 221 | public function scopeLimitDepth(Builder $query, $maximumDepth, $minimumDepth = 0) 222 | { 223 | } 224 | 225 | /** 226 | * scopeForGroup function. 227 | * 228 | * @param Builder $query 229 | * @param mixed $groupId 230 | * 231 | * @return Builder 232 | */ 233 | public function scopeForGroup(Builder $query, $groupId) 234 | { 235 | $groupColumn = $this->getGroupColumn(); 236 | if (is_null($groupColumn)) { 237 | // Depth specified but no depth column configured 238 | throw new \UnexpectedValueException('Depth specified, but no depth column configured.'); 239 | } 240 | echo $groupId; 241 | $query = $query->where($groupColumn, '=', $groupId); 242 | } 243 | 244 | /** 245 | * Build the complete tree structure into existing data. 246 | * This is a slow recursive process and should be used with caution. 247 | * 248 | * @static 249 | */ 250 | public static function buildNewTree() 251 | { 252 | } 253 | 254 | /** 255 | * (re)build the tree structure below the current node. 256 | * This is a slow recursive process and should be used with caution. 257 | * 258 | * @static 259 | */ 260 | public function buildTree($left = 0) 261 | { 262 | } 263 | 264 | /** 265 | * buildTree function. 266 | * 267 | * @param int $left (default: 0) 268 | */ 269 | public function OLD_buildTree($left = 0) 270 | { 271 | // the right value of this Model is the left value + 1 272 | $right = $left + 1; 273 | // get all children of this Model 274 | $children = $this->children; 275 | 276 | // Recursively build the tree below this Model. 277 | foreach ($children as $child) { 278 | $right = $child->buildTree($right); 279 | } 280 | 281 | // We have the left value, and now that we have 282 | // processed this Model's children, we also know 283 | // the right value. 284 | try { 285 | $this->left = $left; 286 | $this->right = $right; 287 | $this->save(); 288 | } catch (\Exception $e) { 289 | // Ignore an exception here. 290 | // The "left" and "right" fields are not yet present in the database. 291 | unset($this->left, $this->right); 292 | } 293 | 294 | // Return the right value of this Model + 1 295 | return ++$right; 296 | } 297 | 298 | // function rebuildTree 299 | 300 | /** 301 | * Get the model. 302 | * 303 | * @return \Illuminate\Database\Eloquent\Model 304 | */ 305 | public function getModel() 306 | { 307 | return $this; 308 | } 309 | 310 | /** 311 | * Relationship to parent. 312 | * 313 | * @return BelongsTo 314 | */ 315 | public function parent() 316 | { 317 | return $this->belongsTo(get_class($this), $this->getParentColumn()); 318 | } 319 | 320 | /** 321 | * Relationship to children. 322 | * 323 | * @return HasMany 324 | */ 325 | public function children() 326 | { 327 | return $this->hasMany(get_class($this), $this->getParentColumn()); 328 | } 329 | 330 | /** 331 | * descendants function. 332 | * 333 | * @param int $depth (default: null) 334 | * 335 | * @return Builder 336 | */ 337 | public function descendants($depth = null) 338 | { 339 | $leftColumn = $this->getLeftColumn(); 340 | $rightColumn = $this->getRightColumn(); 341 | $groupColumn = $this->getGroupColumn(); 342 | 343 | $query = $this->whereBetween($leftColumn, [$this->{$leftColumn} + 1, $this->{$rightColumn} - 1]); 344 | if (!is_null($groupColumn)) { 345 | $query = $query->forGroup($this->{$groupColumn}); 346 | } 347 | 348 | if ($depth) { 349 | $depthColumn = $this->getDepthColumn(); 350 | if (is_null($depthColumn)) { 351 | // Depth specified but no depth column configured 352 | throw new \UnexpectedValueException('Depth specified, but no depth column configured.'); 353 | } 354 | // Limit depth 355 | $query = $query->where($depthColumn, '<=', max($this->{$depthColumn} + $depth)); 356 | } 357 | 358 | return $query; 359 | } 360 | 361 | /** 362 | * Descendants attribute. 363 | * attribute alias for the descendants() function. 364 | * This returns the query results instead of the Builder. 365 | * 366 | * @return Collection 367 | */ 368 | public function getDescendantsAttribute() 369 | { 370 | return $this->descendants()->get(); 371 | } 372 | 373 | /** 374 | * ancestors function. 375 | * 376 | * @param int $depth (default: null) 377 | * 378 | * @return Builder 379 | */ 380 | public function ancestors($depth = null) 381 | { 382 | $leftColumn = $this->getLeftColumn(); 383 | $rightColumn = $this->getRightColumn(); 384 | $groupColumn = $this->getGroupColumn(); 385 | 386 | $query = $this 387 | ->where($leftColumn, '<', $this->{$leftColumn}) 388 | ->where($rightColumn, '>', $this->{$rightColumn}); 389 | 390 | if (!is_null($groupColumn)) { 391 | $query = $query->forGroup($this->{$groupColumn}); 392 | } 393 | 394 | if ($depth) { 395 | $depthColumn = $this->getDepthColumn(); 396 | if (is_null($depthColumn)) { 397 | // Depth specified but no depth column configured 398 | throw new \UnexpectedValueException('Depth specified, but no depth column configured.'); 399 | } 400 | // Limit depth, only if the depth specified does not take us to the root level. 401 | $minDepth = max(0, $this->{$depthColumn} - $depth); 402 | if ($minDepth > 0) { 403 | $query = $query->where($depthColumn, '>=', $minDepth); 404 | } 405 | } 406 | 407 | return $query; 408 | } 409 | 410 | /** 411 | * Ancestors attribute. 412 | * attribute alias for the ancestors() function. 413 | * This returns the query results instead of the Builder. 414 | * 415 | * @return Collection 416 | */ 417 | public function getAncestorsAttribute() 418 | { 419 | return $this->ancestors()->get(); 420 | } 421 | 422 | /** 423 | * descendantCount attribute. 424 | * Returns the number of descendants for this node. 425 | * 426 | * @return int 427 | */ 428 | public function getDescendantCountAttribute() 429 | { 430 | $leftColumn = $this->getLeftColumn(); 431 | $rightColumn = $this->getRightColumn(); 432 | 433 | return floor(($this->{$rightColumn} - $this->{$leftColumn} - 1) / 2); 434 | } 435 | 436 | public function scopeTree(Builder $query, $depth = null) 437 | { 438 | $parentColumn = $this->getParentColumn(); 439 | $leftColumn = $this->getQualifiedLeftColumn(); 440 | 441 | if ($depth) { 442 | // If depth is specified, ensure it's included in the query. 443 | // If a depth field is not configured, it is calculated. 444 | $query->withDepth(); 445 | $query->limitDepth($depth); 446 | } 447 | 448 | $query->orderBy($leftColumn); 449 | 450 | //dd($query->toSql()); 451 | 452 | /* 453 | // If we're using parent_id, use that to get the toplevel nodes. 454 | if (!is_null($parentColumn)) { 455 | $query->whereNull($parentColumn); 456 | } 457 | 458 | // If we're using depth, use that to get the toplevel nodes. 459 | if (!is_null($depthColumn)) { 460 | return $query->where($depthColumn, '=', 0); 461 | } 462 | */ 463 | 464 | return $query; 465 | 466 | // Get ALL nodes and order by left. 467 | 468 | /* 469 | if (!is_null($depthColumn)) { 470 | return static::where($depthColumn, '=', 0); 471 | } 472 | */ 473 | 474 | return static::whereBetween($leftColumn, [$this->{$leftColumn} + 1, $this->{$rightColumn} - 1]); 475 | 476 | dd('Building tree'); 477 | } 478 | } 479 | --------------------------------------------------------------------------------