├── .gitignore ├── INSTALL.markdown ├── LICENSE ├── README.markdown ├── lib └── DoctrineExtensions │ └── NestedSet │ ├── Config.php │ ├── Manager.php │ ├── MultipleRootNode.php │ ├── Node.php │ └── NodeWrapper.php └── tests ├── .gitignore ├── DoctrineExtensions └── NestedSet │ └── Tests │ ├── ConfigTest.php │ ├── DatabaseTest.php │ ├── ManagerTest.php │ ├── Mocks │ ├── ManagerMock.php │ ├── NodeMock.php │ ├── NonNodeMock.php │ ├── RelatedObj.php │ └── SingleRootNodeMock.php │ ├── NodeWrapperTest.php │ └── SingleRootNodeWrapperTest.php ├── README.markdown ├── autoload.php ├── install_vendors.sh ├── phpunit.xml.dist └── update_vendors.sh /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | tests/DoctrineExtensions/NestedSet/Proxies/ 3 | -------------------------------------------------------------------------------- /INSTALL.markdown: -------------------------------------------------------------------------------- 1 | Installing NestedSet for Doctrine2 2 | ================================== 3 | 4 | In the vendor directory of your project, use git to clone the NestedSet 5 | extension: 6 | 7 | $ cd vendor 8 | $ git clone git://github.com/blt04/doctrine2-nestedset.git 9 | 10 | Now add the `DoctrineExtensions\NestedSet` namespace to your class autoloader. 11 | For example, if using Doctrine's autoloader: 12 | 13 | $loader = new Doctrine\Common\ClassLoader("DoctrineExtensions\\NestedSet", "vendor/doctrine2-nestedset/lib"); 14 | $loader->register(); 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | 504 | 505 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Doctrine2 NestedSet 2 | =================== 3 | 4 | This Doctrine2 extension implements the nested set model (modified pre-order 5 | tree traversal algorithm) for Doctrine2. This allows storing hierarchical 6 | data, a collection of data where each item has a single parent and zero or 7 | more children, in the flat tables of a relational database. For more 8 | information on the nested set model, see: 9 | 10 | [https://en.wikipedia.org/wiki/Nested_set_model](https://en.wikipedia.org/wiki/Nested_set_model) 11 | 12 | 13 | ## Introduction 14 | 15 | Nested Set is a solution for storing hierarchical data that provides very fast 16 | read access. However, updating nested set trees is more costly. Therefore this 17 | solution is best suited for hierarchies that are much more frequently read 18 | than written to. And because of the nature of the web, this is the case for 19 | most web applications. 20 | 21 | 22 | ## Setting Up 23 | 24 | To set up your model as a Nested Set, your entity classes must implement the 25 | `DoctrineExtensions\NestedSet\Node` interface. Each entity class must contain 26 | mapped fields for holding the Nested Set left and right values. 27 | 28 | Here's an example using annotation mapping: 29 | 30 | namespace Entity; 31 | 32 | use DoctrineExtensions\NestedSet\Node; 33 | 34 | /** 35 | * @Entity 36 | */ 37 | class Category implements Node 38 | { 39 | /** 40 | * @Id @Column(type="integer") 41 | * @GeneratedValue 42 | */ 43 | private $id; 44 | 45 | /** 46 | * @Column(type="integer") 47 | */ 48 | private $lft; 49 | 50 | /** 51 | * @Column(type="integer") 52 | */ 53 | private $rgt; 54 | 55 | /** 56 | * @Column(type="string", length="16") 57 | */ 58 | private $name; 59 | 60 | 61 | public function getId() { return $this->id; } 62 | 63 | public function getLeftValue() { return $this->lft; } 64 | public function setLeftValue($lft) { $this->lft = $lft; } 65 | 66 | public function getRightValue() { return $this->rgt; } 67 | public function setRightValue($rgt) { $this->rgt = $rgt; } 68 | 69 | public function getName() { return $this->name; } 70 | public function setName($name) { $this->name = $name; } 71 | 72 | public function __toString() { return $this->name; } 73 | } 74 | 75 | Generally you do not need to, and should not, interact with the left and right 76 | fields. These are used internally to manage the tree structure. 77 | 78 | 79 | ## Multiple Trees 80 | 81 | The nested set implementation can be configured to allow your table to have 82 | multiple root nodes, and therefore multiple trees within the same table. This 83 | is done by implementing the `DoctrineExtensions\NestedSet\MultipleRootNode` 84 | interface (instead of `DoctrineExtensions\NestedSet\Node`) and mapping a root 85 | field. 86 | 87 | Extending our annotation example: 88 | 89 | /** 90 | * @Column(type="integer") 91 | */ 92 | private $root; 93 | 94 | public function getRootValue() { return $this->root; } 95 | public function setRootValue($root) { $this->root = $root; } 96 | 97 | Like the left and right fields, you generally do not need to interact with the 98 | root value. 99 | 100 | 101 | ## Working with Trees 102 | 103 | After you successfully set up your model as a nested set you can start working 104 | with it. Working with Doctrine2's nested set implementation is all about two 105 | classes: Manager and NodeWrapper. NodeWrapper wraps your entity classes 106 | giving you access to the underlying tree structure. Manager provides methods 107 | for creating new trees and fetching existing trees. 108 | 109 | To fetch an entire tree from the database: 110 | 111 | $config = new Config($em, 'Entity\Category'); 112 | $nsm = new Manager($config); 113 | $rootNode = $nsm->fetchTree(1); 114 | 115 | In this example, `$rootNode` is an instance of `NodeWrapper` wrapping your 116 | model's root node. To get access to your model object: 117 | 118 | $modelObject = $rootNode->getNode(); 119 | 120 | 121 | ### Creating a Root Node 122 | 123 | $config = new Config($em, 'Entity\Category'); 124 | $nsm = new Manager($config); 125 | 126 | $category = new Category(); 127 | $category->setName('Root Category 1'); 128 | 129 | $rootNode = $nsm->createRoot($category); 130 | 131 | 132 | ### Inserting a Node 133 | 134 | $child1 = new Category(); 135 | $child1->setName('Child Category 1'); 136 | 137 | $child2 = new Category(); 138 | $child2->setName('Child Category 2'); 139 | 140 | $rootNode->addChild($child1); 141 | $rootNode->addChild($child2); 142 | 143 | 144 | ### Deleting a Node 145 | 146 | You must always delete a node using the `NodeWrapper::delete()` method instead 147 | of EntityManager's delete method. `NodeWrapper::delete()` takes care of 148 | updating the tree when deleting nodes: 149 | 150 | $category = $em->getRepository('Entity\Category')->findOneByName('Child Category 1'); 151 | $node = $nsm->wrapNode($category); 152 | $node->delete(); 153 | 154 | Deleting a node will also delete all descendants of that node. So make sure 155 | you move them elsewhere before you delete the node if you don't want to delete 156 | them. 157 | 158 | 159 | ### Moving a Node 160 | 161 | Moving a node is simple. NodeWrapper offers several methods for moving nodes 162 | around between trees: 163 | 164 | * moveAsLastChildOf($other) 165 | * moveAsFirstChildOf($other) 166 | * moveAsPrevSiblingOf($other) 167 | * moveAsNextSiblingOf($other) 168 | 169 | 170 | ### Examining a Node 171 | 172 | You can examine the nodes and what type of node they are by using some of the 173 | following functions: 174 | 175 | $isLeaf = $node->isLeaf(); 176 | $isRoot = $node->isRoot(); 177 | 178 | 179 | ### Examining and Retrieving Siblings 180 | 181 | You can easily check if a node has any next or previous siblings by using the 182 | following methods: 183 | 184 | $hasNextSib = $node->hasNextSibling(); 185 | $hasPrevSib = $node->hasPrevSibling(); 186 | 187 | You can also retrieve the next or previous siblings if they exist with the 188 | following methods: 189 | 190 | $nextSib = $node->getNextSibling(); 191 | $prevSib = $node->getPrevSibling(); 192 | 193 | If you want to retrieve an array of all the siblings you can simply use the 194 | `getSiblings()` method: 195 | 196 | $siblings = $node->getSiblings(); 197 | 198 | 199 | ### Examining and Retrieving Descendants 200 | 201 | You can check if a node has a parent or children by using the following 202 | methods: 203 | 204 | $hasChildren = $node->hasChildren(); 205 | $hasParent = $node->hasParent(); 206 | 207 | You can retrieve a nodes first and last child by using the following methods: 208 | 209 | $firstChild = $node->getFirstChild(); 210 | $lastChild = $node->getLastChild(); 211 | 212 | Or if you want to retrieve the parent of a node: 213 | 214 | $parent = $node->getParent(); 215 | 216 | You can get the children of a node by using the following method: 217 | 218 | $children = $node->getChildren(); 219 | 220 | > The `getChildren()` method returns only the direct descendants. If you want 221 | > all descendants, use the `getDescendants()` method. 222 | 223 | You can get the descendants or ancestors of a node by using the following 224 | methods: 225 | 226 | $descendants = $node->getDescendants(); 227 | $ancestors = $node->getAncestors(); 228 | 229 | Sometimes you may just want to get the number of children or descendants. You 230 | can use the following methods to accomplish this: 231 | 232 | $numChildren = $node->getNumberChildren(); 233 | $numDescendants = $node->getNumberDescendants(); 234 | 235 | The `getDescendants()` method accepts a parameter that you can use to specify 236 | the depth of the resulting branch. For example `getDescendants(1)` retrieves 237 | only the direct descendants (the descendants that are 1 level below, that's 238 | the same as `getChildren()`). 239 | 240 | 241 | ### Rendering a Simple Tree 242 | 243 | $tree = $nsm->fetchTreeAsArray(1); 244 | 245 | foreach ($tree as $node) { 246 | echo str_repeat('  ', $node->getLevel()) . $node . "
"; 247 | } 248 | 249 | 250 | ## Advanced Usage 251 | 252 | The previous sections have explained the basic usage of Doctrine's nested set 253 | implementation. This section will go one step further. 254 | 255 | 256 | ### Fetching a Tree with Relations 257 | 258 | If you're a demanding software developer this question may already have come 259 | into your mind: "How do I fetch a tree/branch with related data?". Simple 260 | example: You want to display a tree of categories, but you also want to 261 | display some related data of each category, let's say some details of the 262 | hottest product in that category. Fetching the tree as seen in the previous 263 | sections and simply accessing the relations while iterating over the tree is 264 | possible but produces a lot of unnecessary database queries. Luckily, 265 | Manager and some flexibility in the nested set implementation have come 266 | to your rescue. The nested set implementation uses `QueryBuilder` objects for 267 | all it's database work. By giving you access to the base query builder of the 268 | nested set implementation you can unleash the full power of `QueryBuilder` 269 | while using your nested set. 270 | 271 | $qb = $em->createQueryBuilder(); 272 | $qb->select('c.name, p.name, m.name') 273 | ->from('Category', 'c') 274 | ->leftJoin('c.HottestProduct', 'p') 275 | ->leftJoin('p.Manufacturer', 'm'); 276 | 277 | Now we need to set the above query as the base query for the tree: 278 | 279 | $nsm->getConfiguration()->setBaseQueryBuilder($qb); 280 | $tree = $nsm->fetchTree(1); 281 | 282 | There it is, the tree with all the related data you need, all in one query. 283 | 284 | > If you don't set your own base query then one will be automatically created 285 | > for you internally. 286 | 287 | When you are done it is a good idea to reset the base query back to normal: 288 | 289 | $nsm->getConfiguration()->resetBaseQueryBuilder(); 290 | 291 | 292 | ### Transactions 293 | 294 | When modifying a tree using methods from `NodeWrapper`, each method is 295 | executed immediately. This differs from working with normal Doctrine2 296 | entities where changes are queued via the EntityManager and not executed until 297 | `flush` is called. 298 | 299 | If you are making multiple changes, it is recommended to wrap these changes in 300 | a transaction: 301 | 302 | $em->getConnection()->beginTransaction(); 303 | try { 304 | 305 | $root = $nsm->createRoot(new Category('Root')); 306 | $root->addChild(new Category('Child 1')); 307 | $root->addChild(new Category('Child 2')); 308 | 309 | $em->getConnection()->commit(); 310 | } catch (Exception $e) { 311 | $em->close(); 312 | $em->getConnection()->rollback(); 313 | throw $e; 314 | } 315 | 316 | 317 | ### Customizing left, right and root fields 318 | 319 | NestedSet requires you include left, right and root fields in your entity 320 | class. By default, NestedSet expects these fields to be named lft, rgt and 321 | root respectively. You can customize the names of these fields using via the 322 | manager configuration: 323 | 324 | $config = new Config($em, 'Entity\Category'); 325 | $config->setLeftFieldName('nsLeft'); 326 | $config->setRightFieldName('nsRight'); 327 | $config->setRootFieldName('nsRoot'); 328 | $nsm = new Manager($config); 329 | 330 | 331 | ## Conclusion 332 | 333 | NestedSet makes managing hierarchical data in Doctrine2 quick and easy. 334 | -------------------------------------------------------------------------------- /lib/DoctrineExtensions/NestedSet/Config.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class Config 31 | { 32 | private 33 | $em, 34 | $classname, 35 | $classMetadata, 36 | $leftFieldName, 37 | $rightFieldName, 38 | $rootFieldName, 39 | $baseQueryBuilder, 40 | $queryHintName, 41 | $queryHintValue, 42 | $hydrateLevel, 43 | $hydrateOutlineNumber, 44 | $hasManyRoots; 45 | 46 | /** 47 | * Constructor. 48 | * 49 | * @param EntityManager $em 50 | * @param mixed $clazz a class name or ClassMetadata object representing 51 | * the entity class associated with this configuration 52 | */ 53 | public function __construct(EntityManager $em, $clazz=null) 54 | { 55 | $this->em = $em; 56 | 57 | // Set defaults 58 | $this->hasManyRoots = false; 59 | $this->setLeftFieldName('lft'); 60 | $this->setRightFieldName('rgt'); 61 | $this->setRootFieldName('root'); 62 | $this->setHydrateLevel(true); 63 | $this->setHydrateOutlineNumber(true); 64 | 65 | if($clazz) 66 | { 67 | $this->setClass($clazz); 68 | } 69 | } 70 | 71 | 72 | /** 73 | * Sets the class associated with this configuration 74 | * 75 | * @param mixed $clazz a class name or ClassMetadata object representing 76 | * the entity class associated with this configuration 77 | * 78 | * @return Config $this for fluent API 79 | */ 80 | public function setClass($clazz) 81 | { 82 | if($clazz instanceof ClassMetadata) 83 | { 84 | $classMetadata = $clazz; 85 | $classname = $clazz->getReflectionClass()->getName(); 86 | } 87 | else if (class_exists($clazz)) 88 | { 89 | $classname = $clazz; 90 | $classMetadata = $this->getEntityManager()->getClassMetadata($clazz); 91 | } else 92 | { 93 | $parts = explode(':', $clazz); 94 | $alias = array_shift($parts); 95 | $rest = implode('\\', $parts); 96 | 97 | try 98 | { 99 | $namespace = $this->getEntityManager()->getConfiguration()->getEntityNamespace($alias); 100 | } 101 | catch (\Doctrine\ORM\ORMException $e) { 102 | throw new \InvalidArgumentException("Can't find class: $clazz'"); 103 | } 104 | 105 | $classname = $namespace.'\\'.$rest; 106 | $classMetadata = $this->getEntityManager()->getClassMetadata($classname); 107 | } 108 | 109 | $reflectionClass = $classMetadata->getReflectionClass(); 110 | if(!$reflectionClass->implementsInterface('DoctrineExtensions\NestedSet\Node')) 111 | { 112 | throw new \InvalidArgumentException('Class must implement Node interface: ' . $classname); 113 | } 114 | 115 | $this->hasManyRoots = $reflectionClass->implementsInterface('DoctrineExtensions\NestedSet\MultipleRootNode'); 116 | $this->classMetadata = $classMetadata; 117 | $this->classname = $classname; 118 | 119 | return $this; 120 | } 121 | 122 | 123 | /** 124 | * gets the entity class name associated with this configuration 125 | * 126 | * @return string 127 | */ 128 | public function getClassname() 129 | { 130 | return $this->classname; 131 | } 132 | 133 | 134 | /** 135 | * gets the class metadata associated with this configuration 136 | * 137 | * @return ClassMetadata 138 | */ 139 | public function getClassMetadata() 140 | { 141 | return $this->classMetadata; 142 | } 143 | 144 | 145 | /** 146 | * Returns the Doctrine entity manager associated with this Manager 147 | * 148 | * @return Doctrine\ORM\EntityManager 149 | */ 150 | public function getEntityManager() 151 | { 152 | return $this->em; 153 | } 154 | 155 | 156 | /** 157 | * gets the left field name 158 | * 159 | * @return string 160 | */ 161 | public function getLeftFieldName() 162 | { 163 | return $this->leftFieldName; 164 | } 165 | 166 | /** 167 | * sets the left field name 168 | * 169 | * @param string $fieldName 170 | * 171 | * @return Config $this for fluent API 172 | */ 173 | public function setLeftFieldName($fieldName) 174 | { 175 | $this->leftFieldName = $fieldName; 176 | return $this; 177 | } 178 | 179 | 180 | /** 181 | * gets the right field name 182 | * 183 | * @return string 184 | */ 185 | public function getRightFieldName() 186 | { 187 | return $this->rightFieldName; 188 | } 189 | 190 | /** 191 | * sets the right field name 192 | * 193 | * @param string $fieldName 194 | * 195 | * @return Config $this for fluent API 196 | */ 197 | public function setRightFieldName($fieldName) 198 | { 199 | $this->rightFieldName = $fieldName; 200 | return $this; 201 | } 202 | 203 | 204 | /** 205 | * gets the root field name 206 | * 207 | * @return string 208 | */ 209 | public function getRootFieldName() 210 | { 211 | return $this->rootFieldName; 212 | } 213 | 214 | /** 215 | * sets the root field name 216 | * 217 | * @param string $fieldName 218 | * 219 | * @return Config $this for fluent API 220 | */ 221 | public function setRootFieldName($fieldName) 222 | { 223 | $this->rootFieldName = $fieldName; 224 | return $this; 225 | } 226 | 227 | 228 | /** 229 | * returns true if the root field is supported 230 | * 231 | * @return bool 232 | */ 233 | public function hasManyRoots() 234 | { 235 | return $this->hasManyRoots; 236 | } 237 | 238 | /** 239 | * gets the base query builder 240 | * 241 | * @return QueryBuilder 242 | */ 243 | public function getBaseQueryBuilder() 244 | { 245 | if(!$this->baseQueryBuilder) 246 | { 247 | $this->baseQueryBuilder = $this->getDefaultQueryBuilder(); 248 | } 249 | 250 | return clone $this->baseQueryBuilder; 251 | } 252 | 253 | 254 | /** 255 | * sets the base query builder 256 | * 257 | * @param Query $baseQueryBuilder or null to reset the base query builder 258 | */ 259 | public function setBaseQueryBuilder(QueryBuilder $baseQueryBuilder=null) 260 | { 261 | if($baseQueryBuilder === null) 262 | { 263 | $this->baseQueryBuilder = $this->getDefaultQueryBuilder(); 264 | } 265 | else 266 | { 267 | $this->baseQueryBuilder = $baseQueryBuilder; 268 | } 269 | } 270 | 271 | 272 | /** 273 | * rests the base query builder back to the default 274 | */ 275 | public function resetBaseQueryBuilder() 276 | { 277 | $this->setBaseQueryBuilder(null); 278 | } 279 | 280 | 281 | /** 282 | * gets the default query builder 283 | * 284 | * @return QueryBuilder 285 | */ 286 | public function getDefaultQueryBuilder() 287 | { 288 | $em = $this->getEntityManager(); 289 | return $em->createQueryBuilder() 290 | ->select('n') 291 | ->from($this->getClassname(), 'n'); 292 | } 293 | 294 | 295 | public function getQueryBuilderAlias() 296 | { 297 | return $this->getBaseQueryBuilder()->getRootAlias(); 298 | } 299 | 300 | public function setQueryHint($name, $value) 301 | { 302 | $this->queryHintName = $name; 303 | $this->queryHintValue = $value; 304 | } 305 | 306 | public function getQueryHintName() 307 | { 308 | return $this->queryHintName; 309 | } 310 | 311 | public function getQueryHintValue() 312 | { 313 | return $this->queryHintValue; 314 | } 315 | 316 | /** 317 | * Checks Configuration to see if Query Hint has been set 318 | * @return bool 319 | */ 320 | public function isQueryHintSet() 321 | { 322 | if (!$this->GetQueryHintName() || !$this->GetQueryHintValue()){ 323 | return false; 324 | } 325 | return true; 326 | } 327 | 328 | /** 329 | * Returns true if the level should be hydrated when fetching trees 330 | * 331 | * @return bool 332 | */ 333 | public function getHydrateLevel() 334 | { 335 | return $this->hydrateLevel; 336 | } 337 | 338 | /** 339 | * Sets whether or not to hydrate the level field when fetching trees 340 | * 341 | * @param bool $b 342 | * 343 | * @return Config $this for fluent API 344 | */ 345 | public function setHydrateLevel($b) 346 | { 347 | $this->hydrateLevel = (bool)$b; 348 | return $this; 349 | } 350 | 351 | 352 | /* 353 | * Returns true if the outline number should be hydrated when fetching 354 | * trees 355 | * 356 | * @return bool 357 | */ 358 | public function getHydrateOutlineNumber() 359 | { 360 | return $this->hydrateOutlineNumber; 361 | } 362 | 363 | 364 | /** 365 | * Sets whether or not to hydrate the outline number when fetching trees 366 | * 367 | * @param bool $b 368 | * 369 | * @return Config $this for fluent API 370 | */ 371 | public function setHydrateOutlineNumber($b) 372 | { 373 | $this->hydrateOutlineNumber = (bool)$b; 374 | return $this; 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /lib/DoctrineExtensions/NestedSet/Manager.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | class Manager 27 | { 28 | /** @var Config */ 29 | protected $config; 30 | 31 | /** @var array */ 32 | protected $wrappers; 33 | 34 | 35 | /** 36 | * Initializes a new NestedSet Manager. 37 | * 38 | * @param string|Doctrine\ORM\Mapping\ClassMetadata $clazz the fully qualified entity class name 39 | * or a ClassMetadata object representing the class of nodes to be managed 40 | * by this manager 41 | * @param Doctrine\ORM\EntityManager $em The EntityManager to use. 42 | */ 43 | public function __construct(Config $config) 44 | { 45 | $this->config = $config; 46 | $this->wrappers = array(); 47 | } 48 | 49 | 50 | /** 51 | * Fetches the complete tree, returning the root node of the tree 52 | * 53 | * @param mixed $rootId the root id of the tree (or null if model doesn't 54 | * support multiple trees 55 | * @param int $depth the depth to retrieve or null for unlimited 56 | * 57 | * @return NodeWrapper $root 58 | */ 59 | public function fetchTree($rootId=null, $depth=null) 60 | { 61 | $wrappers = $this->fetchTreeAsArray($rootId, $depth); 62 | 63 | return (!is_array($wrappers) || empty($wrappers)) ? null : $wrappers[0]; 64 | } 65 | 66 | 67 | /** 68 | * Fetches the complete tree, returning a flat array of node wrappers with 69 | * parent, children, ancestors and descendants pre-populated. 70 | * 71 | * @param mixed $rootId the root id of the tree (or null if model doesn't 72 | * support multiple trees 73 | * @param int $depth the depth to retrieve or null for unlimited 74 | * 75 | * @return array 76 | */ 77 | public function fetchTreeAsArray($rootId=null, $depth=null) 78 | { 79 | $config = $this->getConfiguration(); 80 | $lftField = $config->getLeftFieldName(); 81 | $rgtField = $config->getRightFieldName(); 82 | $rootField = $config->getRootFieldName(); 83 | $hasManyRoots = $config->hasManyRoots(); 84 | 85 | if($rootId === null && $rootField !== null) 86 | { 87 | throw new \InvalidArgumentException('Must provide root id'); 88 | } 89 | 90 | if($depth === 0) 91 | { 92 | return array(); 93 | } 94 | 95 | $qb = $config->getBaseQueryBuilder(); 96 | $alias = $config->getQueryBuilderAlias(); 97 | 98 | $qb->andWhere("$alias.$lftField >= :lowerbound") 99 | ->setParameter('lowerbound', 1) 100 | ->orderBy("$alias.$lftField", "ASC"); 101 | 102 | if($hasManyRoots) 103 | { 104 | $qb->andWhere("$alias.$rootField = :rootid") 105 | ->setParameter('rootid', $rootId); 106 | } 107 | 108 | $q = $qb->getQuery(); 109 | if ($this->config->isQueryHintSet()){ 110 | $q = $this->addHintToQuery($q); 111 | } 112 | $nodes = $q->execute(); 113 | 114 | if(empty($nodes)) 115 | { 116 | return array(); 117 | } 118 | 119 | // TODO: Filter depth using a cross join instead of this 120 | if($depth !== null) 121 | { 122 | $nodes = $this->filterNodeDepth($nodes, $depth); 123 | } 124 | 125 | $wrappers = array(); 126 | foreach($nodes as $node) 127 | { 128 | $wrappers[] = $this->wrapNode($node); 129 | } 130 | 131 | $this->buildTree($wrappers); 132 | 133 | return $wrappers; 134 | } 135 | 136 | 137 | /** 138 | * Fetches a branch of a tree, returning the starting node of the branch. 139 | * All children and descendants are pre-populated. 140 | * 141 | * @param mixed $pk the primary key used to locate the node to traverse 142 | * the tree from 143 | * @param int $depth the depth to retrieve or null for unlimited 144 | * 145 | * @return NodeWrapper $branch 146 | */ 147 | public function fetchBranch($pk, $depth=null) 148 | { 149 | $wrappers = $this->fetchBranchAsArray($pk, $depth); 150 | 151 | return (!is_array($wrappers) || empty($wrappers)) ? null : $wrappers[0]; 152 | } 153 | 154 | 155 | /** 156 | * Fetches a branch of a tree, returning a flat array of node wrappers with 157 | * parent, children, ancestors and descendants pre-populated. 158 | * 159 | * @param mixed $pk the primary key used to locate the node to traverse 160 | * the tree from 161 | * @param int $depth the depth to retrieve or null for unlimited 162 | * 163 | * @return array 164 | */ 165 | public function fetchBranchAsArray($pk, $depth=null) 166 | { 167 | $config = $this->getConfiguration(); 168 | $lftField = $config->getLeftFieldName(); 169 | $rgtField = $config->getRightFieldName(); 170 | $rootField = $config->getRootFieldName(); 171 | $hasManyRoots = $config->hasManyRoots(); 172 | 173 | if($depth === 0) 174 | { 175 | return array(); 176 | } 177 | 178 | $node = $this->getEntityManager()->find($this->getConfiguration()->getClassname(), $pk); 179 | 180 | if(!$node) 181 | { 182 | return array(); 183 | } 184 | 185 | $qb = $config->getBaseQueryBuilder(); 186 | $alias = $config->getQueryBuilderAlias(); 187 | 188 | $qb->andWhere("$alias.$lftField >= :lowerbound") 189 | ->setParameter('lowerbound', $node->getLeftValue()) 190 | ->andWhere("$alias.$rgtField <= :upperbound") 191 | ->setParameter('upperbound', $node->getRightValue()) 192 | ->orderBy("$alias.$lftField", "ASC"); 193 | 194 | // TODO: Add support for depth via a cross join 195 | 196 | if($hasManyRoots) 197 | { 198 | $qb->andWhere("$alias.$rootField = :rootid") 199 | ->setParameter('rootid', $node->getRootValue()); 200 | } 201 | 202 | $q = $qb->getQuery(); 203 | if ($this->config->isQueryHintSet()){ 204 | $q = $this->addHintToQuery($q); 205 | } 206 | $nodes = $q->execute(); 207 | 208 | // @codeCoverageIgnoreStart 209 | if(empty($nodes)) 210 | { 211 | return null; 212 | } 213 | // @codeCoverageIgnoreEnd 214 | 215 | // TODO: Filter depth using a cross join instead of this 216 | if($depth !== null) 217 | { 218 | $nodes = $this->filterNodeDepth($nodes, $depth); 219 | } 220 | 221 | $wrappers = array(); 222 | foreach($nodes as $node) 223 | { 224 | $wrappers[] = $this->wrapNode($node); 225 | } 226 | 227 | $this->buildTree($wrappers); 228 | 229 | return $wrappers; 230 | } 231 | 232 | 233 | /** 234 | * Creates a new root node 235 | * 236 | * @param Node 237 | * 238 | * @return NodeWrapper 239 | */ 240 | public function createRoot(Node $node) 241 | { 242 | if($node instanceof NodeWrapper) 243 | { 244 | throw new \InvalidArgumentException('Can\'t create a root node from a NodeWrapper node'); 245 | } 246 | 247 | $node->setLeftValue(1); 248 | $node->setRightValue(2); 249 | 250 | if($this->getConfiguration()->hasManyRoots()) 251 | { 252 | $rootValue = $node->getId(); 253 | if($rootValue === null) 254 | { 255 | // Set a temporary value in case wrapped node requires root value to be set 256 | $node->setRootValue(0); 257 | $this->getEntityManager()->persist($node); 258 | $this->getEntityManager()->flush(); 259 | $rootValue = $node->getId(); 260 | } 261 | 262 | if($rootValue === null) 263 | { 264 | // @codeCoverageIgnoreStart 265 | throw new \RuntimeException('Node must have an identifier available via getId()'); 266 | // @codeCoverageIgnoreEnd 267 | } 268 | 269 | $node->setRootValue($rootValue); 270 | } 271 | 272 | 273 | $this->getEntityManager()->persist($node); 274 | $this->getEntityManager()->flush(); 275 | 276 | return $this->wrapNode($node); 277 | } 278 | 279 | 280 | /** 281 | * wraps the node using the NodeWrapper class 282 | * 283 | * @param Node $node 284 | * 285 | * @return NodeWrapper 286 | */ 287 | public function wrapNode(Node $node) 288 | { 289 | if($node instanceof NodeWrapper) 290 | { 291 | throw new \InvalidArgumentException('Can\'t wrap a NodeWrapper node'); 292 | } 293 | 294 | $oid = spl_object_hash($node); 295 | if(!isset($this->wrappers[$oid]) || $this->wrappers[$oid]->getNode() !== $node) 296 | { 297 | $this->wrappers[$oid] = new NodeWrapper($node, $this); 298 | } 299 | 300 | return $this->wrappers[$oid]; 301 | } 302 | 303 | 304 | /** 305 | * Resets the manager. Clears NodeWrapper caches. 306 | */ 307 | public function reset() 308 | { 309 | $this->wrappers = array(); 310 | } 311 | 312 | 313 | /** 314 | * Returns the Doctrine entity manager associated with this Manager 315 | * 316 | * @return Doctrine\ORM\EntityManager 317 | */ 318 | public function getEntityManager() 319 | { 320 | return $this->getConfiguration()->getEntityManager(); 321 | } 322 | 323 | 324 | /** 325 | * gets configuration 326 | * 327 | * @return Config 328 | */ 329 | public function getConfiguration() 330 | { 331 | return $this->config; 332 | } 333 | 334 | 335 | 336 | // 337 | // Methods marked internal should not be used outside of the 338 | // NestedSet namespace 339 | // 340 | 341 | 342 | /** 343 | * Internal 344 | * Updates the left values of managed nodes 345 | * 346 | * @param int $first first left value to shift 347 | * @param int $last last left value to shift, or 0 348 | * @param int $delta offset to shift by 349 | * @param mixed $rootVal the root value of entities to act upon 350 | * 351 | */ 352 | public function updateLeftValues($first, $last, $delta, $rootVal=null) 353 | { 354 | $hasManyRoots = $this->getConfiguration()->hasManyRoots(); 355 | 356 | foreach($this->wrappers as $wrapper) 357 | { 358 | if(!$hasManyRoots || ($wrapper->getRootValue() == $rootVal)) 359 | { 360 | if($wrapper->getLeftValue() >= $first && ($last === 0 || $wrapper->getLeftValue() <= $last)) 361 | { 362 | $wrapper->setLeftValue($wrapper->getLeftValue() + $delta); 363 | $wrapper->invalidate(); 364 | } 365 | } 366 | } 367 | } 368 | 369 | 370 | /** 371 | * Internal 372 | * Updates the right values of managed nodes 373 | * 374 | * @param int $first first right value to shift 375 | * @param int $last last right value to shift, or 0 376 | * @param int $delta offset to shift by 377 | * @param mixed $rootVal the root value of entities to act upon 378 | * 379 | */ 380 | public function updateRightValues($first, $last, $delta, $rootVal=null) 381 | { 382 | $hasManyRoots = $this->getConfiguration()->hasManyRoots(); 383 | 384 | foreach($this->wrappers as $wrapper) 385 | { 386 | if(!$hasManyRoots || ($wrapper->getRootValue() == $rootVal)) 387 | { 388 | if($wrapper->getRightValue() >= $first && ($last === 0 || $wrapper->getRightValue() <= $last)) 389 | { 390 | $wrapper->setRightValue($wrapper->getRightValue() + $delta); 391 | $wrapper->invalidate(); 392 | } 393 | } 394 | } 395 | } 396 | 397 | 398 | /** 399 | * Internal 400 | * Updates the left, right and root values of managed nodes 401 | * 402 | * @param int $first lowerbound (lft/rgt) of nodes to update 403 | * @param int $last upperbound (lft/rgt) of nodes to update, or 0 404 | * @param int $delta delta to add to lft/rgt values (can be negative) 405 | * @param mixed $oldRoot the old root value of entities to act upon 406 | * @param mixed $newRoot the new root value to set (or null to not change root) 407 | */ 408 | public function updateValues($first, $last, $delta, $oldRoot=null, $newRoot=null) 409 | { 410 | if(!$this->wrappers) 411 | { 412 | return; 413 | } 414 | 415 | $hasManyRoots = $this->getConfiguration()->hasManyRoots(); 416 | 417 | foreach($this->wrappers as $wrapper) 418 | { 419 | if(!$hasManyRoots || ($wrapper->getRootValue() == $oldRoot)) 420 | { 421 | if($wrapper->getLeftValue() >= $first && ($last === 0 || $wrapper->getRightValue() <= $last)) 422 | { 423 | if($delta !== 0) 424 | { 425 | $wrapper->setLeftValue($wrapper->getLeftValue() + $delta); 426 | $wrapper->setRightValue($wrapper->getRightValue() + $delta); 427 | } 428 | if($hasManyRoots && $newRoot !== null) 429 | { 430 | $wrapper->setRootValue($newRoot); 431 | } 432 | } 433 | } 434 | } 435 | } 436 | 437 | 438 | /** 439 | * Internal 440 | * Removes managed nodes 441 | * 442 | * @param int $left 443 | * @param int $right 444 | * @param mixed $root 445 | */ 446 | public function removeNodes($left, $right, $root=null) 447 | { 448 | $hasManyRoots = $this->getConfiguration()->hasManyRoots(); 449 | 450 | $removed = array(); 451 | foreach($this->wrappers as $oid => $wrapper) 452 | { 453 | if(!$hasManyRoots || ($wrapper->getRootValue() == $root)) 454 | { 455 | if($wrapper->getLeftValue() >= $left && $wrapper->getRightValue() <= $right) 456 | { 457 | $removed[$oid] = $wrapper; 458 | } 459 | } 460 | } 461 | 462 | foreach($removed as $key => $wrapper) 463 | { 464 | unset($this->wrappers[$key]); 465 | $wrapper->setLeftValue(0); 466 | $wrapper->setRightValue(0); 467 | if($hasManyRoots) 468 | { 469 | $wrapper->setRootValue(0); 470 | } 471 | $this->getEntityManager()->detach($wrapper->getNode()); 472 | } 473 | } 474 | 475 | 476 | /** 477 | * Internal 478 | * Filters an array of nodes by depth 479 | * 480 | * @param array array of Node instances 481 | * @param int $depth the depth to filter to 482 | * 483 | * @return array array of Node instances 484 | */ 485 | public function filterNodeDepth($nodes, $depth) 486 | { 487 | if(empty($nodes) || $depth === 0) 488 | { 489 | return array(); 490 | } 491 | 492 | $newNodes = array(); 493 | $stack = array(); 494 | $level = 0; 495 | 496 | foreach($nodes as $node) 497 | { 498 | $parent = end($stack); 499 | while($parent && $node->getLeftValue() > $parent->getRightValue()) 500 | { 501 | array_pop($stack); 502 | $parent = end($stack); 503 | $level--; 504 | } 505 | 506 | if($level < $depth) 507 | { 508 | $newNodes[] = $node; 509 | } 510 | 511 | if(($node->getRightValue() - $node->getLeftValue()) > 1) 512 | { 513 | array_push($stack, $node); 514 | $level++; 515 | } 516 | } 517 | 518 | return $newNodes; 519 | } 520 | 521 | 522 | 523 | 524 | protected function buildTree($wrappers) 525 | { 526 | // @codeCoverageIgnoreStart 527 | if(empty($wrappers)) 528 | { 529 | return; 530 | } 531 | // @codeCoverageIgnoreEnd 532 | 533 | // We are rebuilding the tree, so we invalidate all NodeWrappers 534 | // in case they have cached children/metadata/etc. 535 | foreach($wrappers as $wrapper) 536 | { 537 | $wrapper->invalidate(); 538 | } 539 | 540 | $config = $this->getConfiguration(); 541 | 542 | $rootNode = $wrappers[0]; 543 | $stack = array(); 544 | 545 | $level = 0; 546 | if($config->getHydrateLevel()) 547 | { 548 | $level = $wrappers[0]->getLevel(); 549 | } 550 | 551 | $outlineNumbers = array(0); 552 | if($config->getHydrateOutlineNumber()) 553 | { 554 | $outlineNumbers = explode('.', $wrappers[0]->getOutlineNumber('.', true)); 555 | $outlineNumbers[count($outlineNumbers)-1]--; 556 | } 557 | 558 | foreach($wrappers as $wrapper) 559 | { 560 | $parent = end($stack); 561 | while($parent && $wrapper->getLeftValue() > $parent->getRightValue()) 562 | { 563 | array_pop($stack); 564 | $parent = end($stack); 565 | $level--; 566 | array_pop($outlineNumbers); 567 | } 568 | 569 | $outlineNumbers[count($outlineNumbers)-1]++; 570 | 571 | if($parent && $wrapper !== $rootNode) 572 | { 573 | $wrapper->internalSetParent($parent); 574 | $parent->internalAddChild($wrapper); 575 | $wrapper->internalSetAncestors($stack); 576 | if($config->getHydrateLevel()) 577 | { 578 | $wrapper->internalSetLevel($level); 579 | } 580 | if($config->getHydrateOutlineNumber()) 581 | { 582 | $wrapper->internalSetOutlineNumbers($outlineNumbers); 583 | } 584 | foreach($stack as $anc) 585 | { 586 | $anc->internalAddDescendant($wrapper); 587 | } 588 | } 589 | 590 | if($wrapper->hasChildren()) 591 | { 592 | array_push($stack, $wrapper); 593 | $level++; 594 | array_push($outlineNumbers, 0); 595 | } 596 | } 597 | } 598 | 599 | /** 600 | * Adds a Query hint to a Query Object 601 | * @param \Doctrine\ORM\Query $query 602 | * @return \Doctrine\ORM\Query 603 | */ 604 | public function addHintToQuery(\Doctrine\ORM\Query $query) 605 | { 606 | return $query->setHint($this->getConfiguration()->GetQueryHintName(), $this->getConfiguration()->GetQueryHintValue()); 607 | } 608 | 609 | 610 | } 611 | -------------------------------------------------------------------------------- /lib/DoctrineExtensions/NestedSet/MultipleRootNode.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | interface MultipleRootNode extends Node 33 | { 34 | /** 35 | * gets Node's root value 36 | * 37 | * @return mixed 38 | */ 39 | public function getRootValue(); 40 | 41 | /** 42 | * sets Node's root value 43 | * 44 | * @param mixed $root 45 | */ 46 | public function setRootValue($root); 47 | } 48 | -------------------------------------------------------------------------------- /lib/DoctrineExtensions/NestedSet/Node.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | interface Node 33 | { 34 | 35 | /** 36 | * gets a unique identifier for this node 37 | * 38 | * @return mixed 39 | */ 40 | public function getId(); 41 | 42 | /** 43 | * gets a string representation of the Node 44 | * 45 | * @return string 46 | */ 47 | public function __toString(); 48 | 49 | /** 50 | * gets Node's left value 51 | * 52 | * @return int 53 | */ 54 | public function getLeftValue(); 55 | 56 | /** 57 | * sets Node's left value 58 | * 59 | * @param int $lft 60 | */ 61 | public function setLeftValue($lft); 62 | 63 | /** 64 | * gets Node's right value 65 | * 66 | * @return int 67 | */ 68 | public function getRightValue(); 69 | 70 | /** 71 | * sets Node's right value 72 | * 73 | * @param int $rgt 74 | */ 75 | public function setRightValue($rgt); 76 | } 77 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | cov/ 2 | -------------------------------------------------------------------------------- /tests/DoctrineExtensions/NestedSet/Tests/ConfigTest.php: -------------------------------------------------------------------------------- 1 | config = new Config($this->getEntityManager()); 33 | } 34 | 35 | /** 36 | * @covers DoctrineExtensions\NestedSet\Config::__construct 37 | */ 38 | public function testConstructor() 39 | { 40 | $this->assertInstanceOf('DoctrineExtensions\NestedSet\Config', $this->config, '->__construct() works with default parameters'); 41 | 42 | $clazz = 'DoctrineExtensions\NestedSet\Tests\Mocks\NodeMock'; 43 | $this->assertInstanceOf('DoctrineExtensions\NestedSet\Config', new Config($this->getEntityManager(), $clazz), '->construct() works with a classname'); 44 | 45 | $metadata = $this->getEntityManager()->getClassMetadata($clazz); 46 | $this->assertInstanceOf('DoctrineExtensions\NestedSet\Config', new Config($this->getEntityManager(), $metadata), '->construct() works with metadata'); 47 | 48 | $this->assertEquals('lft', $this->config->getLeftFieldName(), '->__construct() sets default left field name'); 49 | $this->assertEquals('rgt', $this->config->getRightFieldName(), '->__construct() sets default right field name'); 50 | $this->assertEquals('root', $this->config->getRootFieldName(), '->__construct() sets default root field name'); 51 | $this->assertFalse($this->config->hasManyRoots(), '->__construct sets default hasManyRoots'); 52 | $this->assertTrue($this->config->getHydrateLevel(), '->__construct() sets hydrate level to true'); 53 | $this->assertTrue($this->config->getHydrateOutlineNumber(), '->__construct() sets default hydrate outline number'); 54 | } 55 | 56 | 57 | /** 58 | * @covers DoctrineExtensions\NestedSet\Config::setClass 59 | * @expectedException InvalidArgumentException 60 | */ 61 | public function testSetUnknownClass() 62 | { 63 | $this->config->setClass('BogusClass'); 64 | } 65 | 66 | 67 | /** 68 | * @covers DoctrineExtensions\NestedSet\Config::setClass 69 | * @expectedException Doctrine\ORM\Mapping\MappingException 70 | */ 71 | public function testSetNonEntityClass() 72 | { 73 | $this->config->setClass('DoctrineExtensions\NestedSet\Tests\ConfigTest'); 74 | } 75 | 76 | 77 | /** 78 | * @covers DoctrineExtensions\NestedSet\Config::setClass 79 | * @expectedException InvalidArgumentException 80 | */ 81 | public function testSetNonNodeClass() 82 | { 83 | $this->config->setClass('DoctrineExtensions\NestedSet\Tests\Mocks\NonNodeMock'); 84 | } 85 | 86 | /** 87 | * @covers DoctrineExtensions\NestedSet\Config::setClass 88 | * @covers DoctrineExtensions\NestedSet\Config::getClassname 89 | * @covers DoctrineExtensions\NestedSet\Config::getClassMetadata 90 | */ 91 | public function testSetAliasedClass() 92 | { 93 | $namespace = 'DoctrineExtensions\NestedSet\Tests\Mocks'; 94 | 95 | $this->getEntityManager()->getConfiguration()->addEntityNamespace('D2NS', $namespace); 96 | $this->config->setClass('D2NS:NodeMock'); 97 | $clazz = $namespace.'\\'.'NodeMock'; 98 | $metadata = $this->getEntityManager()->getClassMetadata($clazz); 99 | 100 | $this->assertEquals($clazz, $this->config->getClassname(), '->setClass() accepts a repository alias'); 101 | $this->assertSame($metadata, $this->config->getClassMetadata(), '->getClassMetadata() works'); 102 | 103 | $this->assertSame($this->config, $this->config->setClass($clazz), '->setClass() returns $this for fluent API'); 104 | $this->assertEquals($clazz, $this->config->getClassname(), '->getClassname() works'); 105 | 106 | } 107 | 108 | /** 109 | * @covers DoctrineExtensions\NestedSet\Config::setClass 110 | * @covers DoctrineExtensions\NestedSet\Config::getClassname 111 | * @covers DoctrineExtensions\NestedSet\Config::getClassMetadata 112 | */ 113 | public function testSetClass() 114 | { 115 | $clazz = 'DoctrineExtensions\NestedSet\Tests\Mocks\NodeMock'; 116 | $metadata = $this->getEntityManager()->getClassMetadata($clazz); 117 | $this->config->setClass($metadata); 118 | $this->assertEquals($clazz, $this->config->getClassname(), '->setClass() accepts a metadata object'); 119 | $this->assertSame($metadata, $this->config->getClassMetadata(), '->getClassMetadata() works'); 120 | 121 | $this->assertSame($this->config, $this->config->setClass($clazz), '->setClass() returns $this for fluent API'); 122 | $this->assertEquals($clazz, $this->config->getClassname(), '->getClassname() works'); 123 | } 124 | 125 | 126 | /** 127 | * @covers DoctrineExtensions\NestedSet\Config::getEntityManager 128 | */ 129 | public function testGetEntityManager() 130 | { 131 | $this->assertInstanceOf('Doctrine\ORM\EntityManager', $this->config->getEntityManager(), '->getEntityManager() returns EntityManager'); 132 | } 133 | 134 | 135 | /** 136 | * @covers DoctrineExtensions\NestedSet\Config::getLeftFieldName 137 | * @covers DoctrineExtensions\NestedSet\Config::setLeftFieldName 138 | */ 139 | public function testSetLeftFieldName() 140 | { 141 | $this->assertSame($this->config, $this->config->setLeftFieldName('foo'), '->setLeftFieldName() returns $this for fluent API'); 142 | $this->assertEquals('foo', $this->config->getLeftFieldName(), '->getLeftFieldName() works'); 143 | } 144 | 145 | 146 | /** 147 | * @covers DoctrineExtensions\NestedSet\Config::getRightFieldName 148 | * @covers DoctrineExtensions\NestedSet\Config::setRightFieldName 149 | */ 150 | public function testSetRightFieldName() 151 | { 152 | $this->assertSame($this->config, $this->config->setRightFieldName('foo'), '->setRightFieldName() returns $this for fluent API'); 153 | $this->assertEquals('foo', $this->config->getRightFieldName(), '->getRightFieldName() works'); 154 | } 155 | 156 | 157 | /** 158 | * @covers DoctrineExtensions\NestedSet\Config::getRootFieldName 159 | * @covers DoctrineExtensions\NestedSet\Config::setRootFieldName 160 | */ 161 | public function testSetRootFieldName() 162 | { 163 | $this->assertSame($this->config, $this->config->setRootFieldName('foo'), '->setRootFieldName() returns $this for fluent API'); 164 | $this->assertEquals('foo', $this->config->getRootFieldName(), '->getRootFieldName() works'); 165 | } 166 | 167 | 168 | /** 169 | * @covers DoctrineExtensions\NestedSet\Config::hasManyRoots 170 | */ 171 | public function testIsSingleRoot() 172 | { 173 | $this->config->setClass('DoctrineExtensions\NestedSet\Tests\Mocks\NodeMock'); 174 | $this->assertTrue($this->config->hasManyRoots(), '->hasManyRoots() returns true for MutlipleRootNode node'); 175 | 176 | $this->config->setClass('DoctrineExtensions\NestedSet\Tests\Mocks\SingleRootNodeMock'); 177 | $this->assertFalse($this->config->hasManyRoots(), '->hasManyRoots() returns false for Node node'); 178 | } 179 | 180 | 181 | /** 182 | * @covers DoctrineExtensions\NestedSet\Config::getBaseQueryBuilder 183 | * @covers DoctrineExtensions\NestedSet\Config::setBaseQueryBuilder 184 | */ 185 | public function testSetBaseQueryBuilder() 186 | { 187 | $defaultQb = $this->config->getDefaultQueryBuilder(); 188 | $this->assertEquals($defaultQb, $this->config->getBaseQueryBuilder(), '->getBaseQueryBuilder() returns default QueryBuilder if none is set'); 189 | 190 | $qb = $this->getEntityManager()->createQueryBuilder() 191 | ->select('y') 192 | ->from('DoctrineExtensions\NestedSet\Tests\Mocks\NodeMock', 'y'); 193 | $this->config->setBaseQueryBuilder($qb); 194 | $this->assertEquals($qb, $this->config->getBaseQueryBuilder(), '->setBaseQueryBuilder() sets a QueryBuilder object'); 195 | $this->assertNotSame($qb, $this->config->getBaseQueryBuilder(), '->getBaseQueryBuilder() returns a clone of the base query builder object'); 196 | 197 | $this->config->setBaseQueryBuilder(); 198 | $this->assertEquals($defaultQb, $this->config->getBaseQueryBuilder(), '->setBaseQueryBuilder() reverts to default QueryBuilder when nothing is set'); 199 | } 200 | 201 | 202 | /** 203 | * @covers DoctrineExtensions\NestedSet\Config::resetBaseQueryBuilder 204 | * @covers DoctrineExtensions\NestedSet\Config::setBaseQueryBuilder 205 | */ 206 | public function testResetBaseQueryBuilder() 207 | { 208 | $defaultQb = $this->config->getDefaultQueryBuilder(); 209 | 210 | $qb = $this->getEntityManager()->createQueryBuilder() 211 | ->select('y') 212 | ->from('DoctrineExtensions\NestedSet\Tests\Mocks\NodeMock', 'y'); 213 | $this->config->setBaseQueryBuilder($qb); 214 | 215 | $this->config->resetBaseQueryBuilder(); 216 | $this->assertEquals($defaultQb, $this->config->getBaseQueryBuilder(), '->resetBaseQueryBuilder() reverts to default QueryBuilder'); 217 | } 218 | 219 | 220 | /** 221 | * @covers DoctrineExtensions\NestedSet\Config::getDefaultQueryBuilder 222 | */ 223 | public function testGetDefaultQueryBuilder() 224 | { 225 | $qb = $this->config->getDefaultQueryBuilder(); 226 | $this->assertInstanceOf('Doctrine\ORM\QueryBuilder', $qb, '->getDefaultQueryBuilder() returns QueryBuilder object'); 227 | } 228 | 229 | 230 | /** 231 | * @covers DoctrineExtensions\NestedSet\Config::getQueryBuilderAlias 232 | */ 233 | public function testGetQueryBuilderAlias() 234 | { 235 | $this->assertEquals('n', $this->config->getQueryBuilderAlias()); 236 | } 237 | 238 | 239 | /** 240 | * @covers DoctrineExtensions\NestedSet\Config::getHydrateLevel 241 | * @covers DoctrineExtensions\NestedSet\Config::setHydrateLevel 242 | */ 243 | public function testSetHydrateLevel() 244 | { 245 | $this->assertSame($this->config, $this->config->setHydrateLevel(false), '->setHydrateLevel() returns $this for fluent API'); 246 | $this->assertFalse($this->config->getHydrateLevel(), '->getHydrateLevel() works'); 247 | } 248 | 249 | 250 | /* 251 | * @covers DoctrineExtensions\NestedSet\Config::getHydrateOutlineNumber 252 | * @covers DoctrineExtensions\NestedSet\Config::setHydrateOutlineNumber 253 | */ 254 | public function testSetHydrateOutlineNumber() 255 | { 256 | $this->assertSame($this->config, $this->config->setHydrateOutlineNumber(false), '->setHydrateOutlineNumber() returns $this for fluent API'); 257 | $this->assertFalse($this->config->getHydrateOutlineNumber(), '->getHydrateOutlineNumber() works'); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /tests/DoctrineExtensions/NestedSet/Tests/DatabaseTest.php: -------------------------------------------------------------------------------- 1 | em) 38 | { 39 | $this->em = $this->_createEntityManager(); 40 | } 41 | 42 | return $this->em; 43 | } 44 | 45 | /** 46 | * Creates an entity manager to use for all tests 47 | * 48 | * @return Doctrine\ORM\EntityManager 49 | */ 50 | protected function _createEntityManager() 51 | { 52 | $conn = \Doctrine\DBAL\DriverManager::getConnection(array( 53 | 'driver' => 'pdo_sqlite', 54 | 'memory' => true 55 | )); 56 | 57 | $config = new \Doctrine\ORM\Configuration(); 58 | $config->setProxyDir(__DIR__ . '/../Proxies'); 59 | $config->setProxyNamespace('DoctrineExtensions\NestedSet\Tests\Proxies'); 60 | $config->setMetadataDriverImpl(\Doctrine\ORM\Mapping\Driver\AnnotationDriver::create()); 61 | $config->setAutoGenerateProxyClasses(true); 62 | 63 | return \Doctrine\ORM\EntityManager::create($conn, $config); 64 | } 65 | 66 | 67 | /** 68 | * Loads the schema for the given classes 69 | * 70 | * @param array $classes 71 | * 72 | */ 73 | protected function loadSchema($classes) 74 | { 75 | $schemaTool = new \Doctrine\ORM\Tools\SchemaTool($this->getEntityManager()); 76 | $schemaTool->createSchema($classes); 77 | } 78 | 79 | 80 | protected function enableSqlLogger() 81 | { 82 | if($this->logger) 83 | { 84 | $this->logger->enabled = true; 85 | } 86 | else 87 | { 88 | $this->getSqlLogger(); 89 | } 90 | } 91 | 92 | protected function disableSqlLogger() 93 | { 94 | if($this->logger) 95 | { 96 | $this->logger->enabled = false; 97 | } 98 | } 99 | 100 | 101 | protected function getSqlLogger() 102 | { 103 | if(!$this->logger) 104 | { 105 | $this->logger = new DebugStack(); 106 | $this->getEntityManager()->getConnection()->getConfiguration()->setSQLLogger($this->logger); 107 | } 108 | 109 | return $this->logger; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/DoctrineExtensions/NestedSet/Tests/ManagerTest.php: -------------------------------------------------------------------------------- 1 | nsm = new ManagerMock($this->getEntityManager(), 'DoctrineExtensions\NestedSet\Tests\Mocks\NodeMock'); 38 | } 39 | 40 | public function setUpDb($em=null) 41 | { 42 | if($em === null) 43 | { 44 | $em = $this->getEntityManager(); 45 | } 46 | 47 | $this->loadSchema(array($em->getClassMetadata('DoctrineExtensions\NestedSet\Tests\Mocks\NodeMock'))); 48 | } 49 | 50 | protected function loadData() 51 | { 52 | $this->setUpDb(); 53 | $em = $this->getEntityManager(); 54 | 55 | $this->nodes = array( 56 | new NodeMock(1, '1', 1, 10), # 0 57 | new NodeMock(2, '1.1', 2, 7), # 1 58 | new NodeMock(3, '1.1.1', 3, 4), # 2 59 | new NodeMock(4, '1.1.2', 5, 6), # 3 60 | new NodeMock(5, '1.2', 8, 9), # 4 61 | ); 62 | 63 | $this->nodes2 = array( 64 | new NodeMock(11, '1', 1, 12, 2), # 0 65 | new NodeMock(12, '1.1', 2, 7, 2), # 1 66 | new NodeMock(13, '1.1.1', 3, 4, 2), # 2 67 | new NodeMock(14, '1.1.2', 5, 6, 2), # 3 68 | new NodeMock(15, '1.2', 8, 9, 2), # 4 69 | new NodeMock(16, '1.3', 10, 11, 2), # 5 70 | ); 71 | 72 | 73 | foreach($this->nodes as $node) 74 | { 75 | $em->persist($node); 76 | } 77 | 78 | $this->wrappers2 = array(); 79 | foreach($this->nodes2 as $node) 80 | { 81 | $em->persist($node); 82 | } 83 | 84 | $em->flush(); 85 | } 86 | 87 | 88 | /** 89 | * @covers DoctrineExtensions\NestedSet\Manager::__construct 90 | */ 91 | public function testConstructor() 92 | { 93 | $this->assertInstanceOf('DoctrineExtensions\NestedSet\Manager', $this->nsm); 94 | } 95 | 96 | 97 | /** 98 | * @covers DoctrineExtensions\NestedSet\Manager::fetchTree 99 | * @covers DoctrineExtensions\NestedSet\Manager::fetchTreeAsArray 100 | * @expectedException InvalidArgumentException 101 | */ 102 | public function testFetchTreeNoRootId() 103 | { 104 | $this->nsm->fetchTree(); 105 | } 106 | 107 | /** 108 | * @covers DoctrineExtensions\NestedSet\Manager::fetchTree 109 | * @covers DoctrineExtensions\NestedSet\Manager::fetchTreeAsArray 110 | * @covers DoctrineExtensions\NestedSet\Manager::buildTree 111 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::internalSetParent 112 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::internalSetAncestors 113 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::internalAddDescendant 114 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::internalAddChild 115 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::internalSetLevel 116 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::internalSetOutlineNumbers 117 | */ 118 | public function testFetchTree() 119 | { 120 | $this->loadData(); 121 | $nodes = $this->nodes; 122 | 123 | $this->assertNull($this->nsm->fetchTree(10), '->fetchTree() returns null when no nodes exist'); 124 | 125 | $root = $this->nsm->fetchTree(1); 126 | $this->assertInstanceOf('DoctrineExtensions\NestedSet\NodeWrapper', $root, '->fetchTree() returns a NodeWrapper object'); 127 | 128 | // 129 | // NOTE: Testing private variables 130 | // 131 | 132 | $root_parent = $this->readAttribute($root, 'parent'); 133 | $root_children = $this->readAttribute($root, 'children'); 134 | $root_ancestors = $this->readAttribute($root, 'ancestors'); 135 | $root_descendants = $this->readAttribute($root, 'descendants'); 136 | 137 | $this->assertEquals($nodes[0]->getId(), $root->getId(), '->fetchTree() root id is correct'); 138 | $this->assertAttributeEquals(0, 'level', $root, '->fetchTree() root level is correct'); 139 | $this->assertAttributeEquals(array(1), 'outlineNumbers', $root, '->fetchTree() root outlineNumbers is correct'); 140 | $this->assertEmpty($root_ancestors, '->fetchTree() root ancestors is empty'); 141 | $this->assertNull($root_parent, '->fetchTree() root parent is null'); 142 | $this->assertEquals( 143 | array($nodes[1]->getId(), $nodes[4]->getId()), 144 | array_map(function($e) {return $e->getNode()->getId();}, $root_children), 145 | '->fetchTree() root children populated' 146 | ); 147 | $this->assertEquals( 148 | array($nodes[1]->getId(), $nodes[2]->getId(), $nodes[3]->getId(), $nodes[4]->getId()), 149 | array_map(function($e) {return $e->getNode()->getId();}, $root_descendants), 150 | '->fetchTree() root descendants populated' 151 | ); 152 | $this->assertAttributeEquals(1, 'level', $root_children[0], '->fetchTree() root children level is correct'); 153 | $this->assertAttributeEquals(array(1,1), 'outlineNumbers', $root_children[0], '->fetchTree() root children outlineNumbers is correct'); 154 | 155 | 156 | $node1_parent = $this->readAttribute($root_children[0], 'parent'); 157 | $node1_children = $this->readAttribute($root_children[0], 'children'); 158 | $node1_ancestors = $this->readAttribute($root_children[0], 'ancestors'); 159 | $node1_descendants = $this->readAttribute($root_children[0], 'descendants'); 160 | 161 | $this->assertEquals($nodes[0]->getId(), $node1_parent->getNode()->getId(), '->fetchTree() first child parent is correct'); 162 | $this->assertEquals( 163 | array($nodes[0]->getId()), 164 | array_map(function($e) {return $e->getNode()->getId();}, $node1_ancestors), 165 | '->fetchTree() first child ancestors is correct' 166 | ); 167 | $this->assertEquals( 168 | array($nodes[2]->getId(), $nodes[3]->getId()), 169 | array_map(function($e) {return $e->getNode()->getId();}, $node1_children), 170 | '->fetchTree() first child children populated' 171 | ); 172 | $this->assertEquals( 173 | array($nodes[2]->getId(), $nodes[3]->getId()), 174 | array_map(function($e) {return $e->getNode()->getId();}, $node1_descendants), 175 | '->fetchTree() first child descendants populated' 176 | ); 177 | $this->assertAttributeEquals(2, 'level', $node1_children[0], '->fetchTree() node 1 children level is correct'); 178 | $this->assertAttributeEquals(array(1,1,2), 'outlineNumbers', $node1_children[1], '->fetchTree() node 1 children outlineNumbers is correct'); 179 | 180 | 181 | $node3_parent = $this->readAttribute($root_descendants[2], 'parent'); 182 | $node3_children = $this->readAttribute($root_descendants[2], 'children'); 183 | $node3_ancestors = $this->readAttribute($root_descendants[2], 'ancestors'); 184 | $node3_descendants = $this->readAttribute($root_descendants[2], 'descendants'); 185 | $this->assertEquals($nodes[1]->getId(), $node3_parent->getNode()->getId(), '->fetchTree() leaf parent is correct'); 186 | $this->assertEquals( 187 | array($nodes[0]->getId(), $nodes[1]->getId()), 188 | array_map(function($e) {return $e->getNode()->getId();}, $node3_ancestors), 189 | '->fetchTree() leaf ancestors is correct' 190 | ); 191 | $this->assertEmpty($node3_children, '->fetchTree() leaf children is empty'); 192 | $this->assertEmpty($node3_descendants, '->fetchTree() leaf descendants is empty'); 193 | } 194 | 195 | 196 | /** 197 | * @covers DoctrineExtensions\NestedSet\Manager::fetchTree 198 | * @covers DoctrineExtensions\NestedSet\Manager::fetchTreeAsArray 199 | * @covers DoctrineExtensions\NestedSet\Manager::buildTree 200 | * @covers DoctrineExtensions\NestedSet\Manager::filterNodeDepth 201 | */ 202 | public function testFetchTreeDepth() 203 | { 204 | $this->loadData(); 205 | $nodes = $this->nodes; 206 | 207 | $this->assertNull($this->nsm->fetchTree(1, 0)); 208 | 209 | $root = $this->nsm->fetchTree(1, 2); 210 | $this->assertInstanceOf('DoctrineExtensions\NestedSet\NodeWrapper', $root, '->fetchTree() returns a NodeWrapper object'); 211 | 212 | // 213 | // NOTE: Testing private variables 214 | // 215 | $node1_children = $this->readAttribute($root->getFirstChild(), 'children'); 216 | $node1_descendants = $this->readAttribute($root->getFirstChild(), 'descendants'); 217 | $this->assertEmpty($node1_children, '->fetchTree() empty children with depth filtered'); 218 | $this->assertEmpty($node1_descendants, '->fetchTree() empty descendants with depth filtered'); 219 | } 220 | 221 | 222 | /** 223 | * @covers DoctrineExtensions\NestedSet\Manager::fetchBranch 224 | * @covers DoctrineExtensions\NestedSet\Manager::fetchBranchAsArray 225 | * @covers DoctrineExtensions\NestedSet\Manager::buildTree 226 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::internalSetParent 227 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::internalSetAncestors 228 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::internalAddDescendant 229 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::internalAddChild 230 | */ 231 | public function testFetchBranch() 232 | { 233 | $this->loadData(); 234 | $nodes = $this->nodes; 235 | 236 | $this->assertNull($this->nsm->fetchBranch(-10), '->fetchBranch() returns null when branch node doesn\'t exist'); 237 | 238 | $root = $this->nsm->fetchBranch(2); 239 | $this->assertInstanceOf('DoctrineExtensions\NestedSet\NodeWrapper', $root, '->fetchBranch() returns a NodeWrapper object'); 240 | 241 | // 242 | // NOTE: Testing private variables 243 | // 244 | 245 | $root_parent = $this->readAttribute($root, 'parent'); 246 | $root_children = $this->readAttribute($root, 'children'); 247 | $root_ancestors = $this->readAttribute($root, 'ancestors'); 248 | $root_descendants = $this->readAttribute($root, 'descendants'); 249 | 250 | $this->assertEquals($nodes[1]->getId(), $root->getId(), '->fetchBranch() start id is correct'); 251 | $this->assertAttributeEquals(1, 'level', $root, '->fetchBranch() start level is correct'); 252 | $this->assertAttributeEquals(array(1,1), 'outlineNumbers', $root, '->fetchBranch() start outlineNumbers is correct'); 253 | $this->assertNull($root_parent, '->fetchBranch() start parent is null'); 254 | $this->assertEquals( 255 | array($nodes[2]->getId(), $nodes[3]->getId()), 256 | array_map(function($e) {return $e->getNode()->getId();}, $root_children), 257 | '->fetchBranch() start children populated' 258 | ); 259 | $this->assertEquals( 260 | array($nodes[2]->getId(), $nodes[3]->getId()), 261 | array_map(function($e) {return $e->getNode()->getId();}, $root_descendants), 262 | '->fetchBranch() start descendants populated' 263 | ); 264 | $this->assertAttributeEquals(2, 'level', $root_children[0], '->fetchBranch() child level is correct'); 265 | $this->assertAttributeEquals(array(1,1,2), 'outlineNumbers', $root_children[1], '->fetchBranch() child outlineNumbers is correct'); 266 | 267 | 268 | $node2_parent = $this->readAttribute($root_children[0], 'parent'); 269 | $node2_children = $this->readAttribute($root_children[0], 'children'); 270 | $node2_ancestors = $this->readAttribute($root_children[0], 'ancestors'); 271 | $node2_descendants = $this->readAttribute($root_children[0], 'descendants'); 272 | 273 | $this->assertEquals($nodes[1]->getId(), $node2_parent->getNode()->getId(), '->fetchBranch() first child parent is correct'); 274 | $this->assertEquals( 275 | array($nodes[1]->getId()), 276 | array_map(function($e) {return $e->getNode()->getId();}, $node2_ancestors), 277 | '->fetchBranch() first child ancestors is correct' 278 | ); 279 | $this->assertEmpty($node2_children, '->fetchBranch() first child children populated'); 280 | $this->assertEmpty($node2_descendants, '->fetchBranch() first child descendants populated'); 281 | } 282 | 283 | 284 | /** 285 | * @covers DoctrineExtensions\NestedSet\Manager::fetchBranch 286 | * @covers DoctrineExtensions\NestedSet\Manager::fetchBranchAsArray 287 | * @covers DoctrineExtensions\NestedSet\Manager::buildTree 288 | * @covers DoctrineExtensions\NestedSet\Manager::filterNodeDepth 289 | */ 290 | public function testFetchBranchDepth() 291 | { 292 | $this->loadData(); 293 | $nodes = $this->nodes; 294 | 295 | $this->assertNull($this->nsm->fetchBranch(2, 0)); 296 | 297 | $root = $this->nsm->fetchBranch(2, 1); 298 | $this->assertInstanceOf('DoctrineExtensions\NestedSet\NodeWrapper', $root, '->fetchTree() returns a NodeWrapper object'); 299 | 300 | // 301 | // NOTE: Testing private variables 302 | // 303 | $node1_children = $this->readAttribute($root, 'children'); 304 | $node1_descendants = $this->readAttribute($root, 'descendants'); 305 | $this->assertEmpty($node1_children, '->fetchTree() empty children with depth filtered'); 306 | $this->assertEmpty($node1_descendants, '->fetchTree() empty descendants with depth filtered'); 307 | } 308 | 309 | 310 | /** 311 | * @covers DoctrineExtensions\NestedSet\Manager::fetchTree 312 | * @covers DoctrineExtensions\NestedSet\Manager::fetchTreeAsArray 313 | * @covers DoctrineExtensions\NestedSet\Manager::buildTree 314 | */ 315 | public function testFetchTreeDuplicate() 316 | { 317 | $this->loadData(); 318 | $nodes = $this->nodes; 319 | 320 | $root1 = $this->nsm->fetchTree(1); 321 | $this->assertEquals(2, count($root1->getChildren()), '1st root has correct number of children'); 322 | 323 | $root2 = $this->nsm->fetchTree(1); 324 | $this->assertEquals(2, count($root2->getChildren()), '1st root has correct number of children after 2nd fetchTree()'); 325 | } 326 | 327 | 328 | /** 329 | * @covers DoctrineExtensions\NestedSet\Manager::fetchBranch 330 | * @covers DoctrineExtensions\NestedSet\Manager::fetchBranchAsArray 331 | */ 332 | public function testFetchWithQueryBuilder() 333 | { 334 | $em = $this->getEntityManager(); 335 | $logger = $this->getSqlLogger(); 336 | $this->loadSchema(array($em->getClassMetadata('DoctrineExtensions\NestedSet\Tests\Mocks\RelatedObj'))); 337 | $this->loadData(); 338 | 339 | foreach($this->nodes as $node) 340 | { 341 | $node->setRelatedObj(new RelatedObj()); 342 | } 343 | $em->flush(); 344 | 345 | $em->clear(); 346 | $logger->queries = array(); 347 | 348 | $qb = $this->getEntityManager()->createQueryBuilder() 349 | ->select('n, r') 350 | ->from('DoctrineExtensions\NestedSet\Tests\Mocks\NodeMock', 'n') 351 | ->innerJoin('n.related', 'r'); 352 | $this->nsm->getConfiguration()->setBaseQueryBuilder($qb); 353 | 354 | $root = $this->nsm->fetchTree(1); 355 | 356 | $beforeCount = count($logger->queries); 357 | $relatedId = $root->getNode()->getRelatedObj()->getId(); 358 | $afterCount = count($logger->queries); 359 | 360 | $this->assertEquals($beforeCount, $afterCount, '->fetchTree() uses custom base QueryBuilder'); 361 | } 362 | 363 | 364 | /** 365 | * @covers DoctrineExtensions\NestedSet\Manager::createRoot 366 | */ 367 | public function testCreateRoot() 368 | { 369 | $this->setUpDb(); 370 | 371 | $node = new NodeMock(21, '1'); 372 | $wrapper = $this->nsm->createRoot($node); 373 | $this->assertInstanceOf('DoctrineExtensions\NestedSet\NodeWrapper', $wrapper, '->createRoot() returns a NodeWrapper()'); 374 | $this->assertEquals(1, $wrapper->getLeftValue(), '->createRoot() sets left value'); 375 | $this->assertEquals(2, $wrapper->getRightValue(), '->createRoot() sets right value'); 376 | $this->assertEquals(21, $wrapper->getRootValue(), '->createRoot() sets root value'); 377 | } 378 | 379 | 380 | /** 381 | * @covers DoctrineExtensions\NestedSet\Manager::createRoot 382 | * @expectedException InvalidArgumentException 383 | */ 384 | public function testCreateRoot_CantPassNodeWrapper() 385 | { 386 | $node = new NodeMock(21, '1'); 387 | $wrapper = $this->nsm->wrapNode($node); 388 | $this->nsm->createRoot($wrapper); 389 | } 390 | 391 | 392 | /** 393 | * @covers DoctrineExtensions\NestedSet\Manager::createRoot 394 | */ 395 | public function testCreateRoot_NoId() 396 | { 397 | $this->setUpDb(); 398 | 399 | $node = new NodeMock(null, '1'); 400 | $wrapper = $this->nsm->createRoot($node); 401 | $this->assertInstanceOf('DoctrineExtensions\NestedSet\NodeWrapper', $wrapper, '->createRoot() returns a NodeWrapper()'); 402 | $this->assertEquals($wrapper->getId(), $wrapper->getRootValue(), '->createRoot() sets root value'); 403 | } 404 | 405 | 406 | /** 407 | * @covers DoctrineExtensions\NestedSet\Manager::wrapNode 408 | */ 409 | public function testWrapNode() 410 | { 411 | $node = new NodeMock(1, '1'); 412 | $wrapper = $this->nsm->wrapNode($node); 413 | $this->assertInstanceOf('DoctrineExtensions\NestedSet\NodeWrapper', $wrapper, '->wrapNode returns NodeWrapper object'); 414 | } 415 | 416 | 417 | /** 418 | * @covers DoctrineExtensions\NestedSet\Manager::wrapNode 419 | * @expectedException InvalidArgumentException 420 | */ 421 | public function testWrapNode_CantWrapNodeWrapper() 422 | { 423 | $node = new NodeMock(1, '1'); 424 | $wrapper = new NodeWrapper($node, $this->nsm); 425 | $this->nsm->wrapNode($wrapper); 426 | } 427 | 428 | 429 | /** 430 | * @covers DoctrineExtensions\NestedSet\Manager::reset 431 | */ 432 | public function testReset() 433 | { 434 | $node = new NodeMock(1, '1'); 435 | $wrapper1 = $this->nsm->wrapNode($node); 436 | $wrapper2 = $this->nsm->wrapNode($node); 437 | $this->assertSame($wrapper1, $wrapper2, '->wrapNode() returns cached NodeWrapper instance'); 438 | 439 | $this->nsm->reset(); 440 | $wrapper3 = $this->nsm->wrapNode($node); 441 | $this->assertNotSame($wrapper1, $wrapper3, '->reset() clears NodeWrapper cache'); 442 | } 443 | 444 | 445 | /** 446 | * @covers DoctrineExtensions\NestedSet\Manager::getEntityManager 447 | */ 448 | public function testGetEntityManager() 449 | { 450 | $this->assertInstanceOf('Doctrine\ORM\EntityManager', $this->nsm->getEntityManager(), '->getEntityManager() returns instance of EntityManager'); 451 | } 452 | 453 | 454 | /** 455 | * @covers DoctrineExtensions\NestedSet\Manager::getConfiguration 456 | */ 457 | public function testGetConfiguration() 458 | { 459 | $this->assertInstanceOf('DoctrineExtensions\NestedSet\Config', $this->nsm->getConfiguration(), '->getConfiguration() works'); 460 | } 461 | 462 | 463 | /** 464 | * @covers DoctrineExtensions\NestedSet\Manager::updateLeftValues 465 | */ 466 | public function testUpdateLeftValues() 467 | { 468 | $wrappers = array( 469 | $this->nsm->wrapNode(new NodeMock(1, '1', 1, 6)), 470 | $this->nsm->wrapNode(new NodeMock(2, '1.1', 2, 3)), 471 | $this->nsm->wrapNode(new NodeMock(3, '1.2', 4, 5)), 472 | ); 473 | 474 | $this->nsm->updateLeftValues(2, 0, 2, 1); 475 | 476 | $this->assertEquals(4, $wrappers[1]->getLeftValue(), '->updateLeftValues() updates left value'); 477 | } 478 | 479 | 480 | /** 481 | * @covers DoctrineExtensions\NestedSet\Manager::updateRightValues 482 | */ 483 | public function testUpdateRightValues() 484 | { 485 | $wrappers = array( 486 | $this->nsm->wrapNode(new NodeMock(1, '1', 1, 6)), 487 | $this->nsm->wrapNode(new NodeMock(2, '1.1', 2, 3)), 488 | $this->nsm->wrapNode(new NodeMock(3, '1.2', 4, 5)), 489 | ); 490 | 491 | $this->nsm->updateRightValues(2, 0, 2, 1); 492 | 493 | $this->assertEquals(5, $wrappers[1]->getRightValue(), '->updateRightValues() updates right value'); 494 | } 495 | 496 | 497 | /** 498 | * @covers DoctrineExtensions\NestedSet\Manager::updateValues 499 | */ 500 | public function testUpdateValues() 501 | { 502 | // Make sure updateValues can be called with no registered wrappers 503 | $this->nsm->updateValues(1, 0, 2, 1, 15); 504 | 505 | $wrappers = array( 506 | $this->nsm->wrapNode(new NodeMock(1, '1', 1, 6)), 507 | $this->nsm->wrapNode(new NodeMock(2, '1.1', 2, 3)), 508 | $this->nsm->wrapNode(new NodeMock(3, '1.2', 4, 5)), 509 | ); 510 | 511 | $this->nsm->updateValues(1, 0, 2, 1, 15); 512 | 513 | $this->assertEquals(4, $wrappers[1]->getLeftValue(), '->updateValues() updates left value'); 514 | $this->assertEquals(5, $wrappers[1]->getRightValue(), '->updateValues() updates right value'); 515 | $this->assertEquals(15, $wrappers[1]->getRootValue(), '->updateValues() updates root value'); 516 | } 517 | 518 | 519 | /** 520 | * @covers DoctrineExtensions\NestedSet\Manager::removeNodes 521 | */ 522 | public function testRemoveNodes() 523 | { 524 | $wrappers = array( 525 | $this->nsm->wrapNode(new NodeMock(1, '1', 1, 6)), 526 | $this->nsm->wrapNode(new NodeMock(2, '1.1', 2, 3)), 527 | $this->nsm->wrapNode(new NodeMock(3, '1.2', 4, 5)), 528 | ); 529 | 530 | $this->nsm->removeNodes(2,3,1); 531 | $this->assertFalse($this->nsm->wrapperExists($wrappers[1]->getId()), '->removeNodes() removes node from manager'); 532 | $this->assertFalse($this->nsm->getEntityManager()->contains($wrappers[2]->getNode()), '->removeNodes() removes node from entity manager'); 533 | } 534 | 535 | 536 | /** 537 | * @covers DoctrineExtensions\NestedSet\Manager::filterNodeDepth 538 | */ 539 | public function testFilterNodeDepth_Empty() 540 | { 541 | $this->assertEmpty($this->nsm->filterNodeDepth(array(), 1), '->filterNodeDepth() returns an empty array when given an empty array'); 542 | $this->assertEmpty($this->nsm->filterNodeDepth($this->nodes, 0), '->filterNodeDepth() returns an empty array for depth=0'); 543 | } 544 | } 545 | -------------------------------------------------------------------------------- /tests/DoctrineExtensions/NestedSet/Tests/Mocks/ManagerMock.php: -------------------------------------------------------------------------------- 1 | wrappers); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/DoctrineExtensions/NestedSet/Tests/Mocks/NodeMock.php: -------------------------------------------------------------------------------- 1 | id = $id; 65 | $this->lft = $lft; 66 | $this->rgt = $rgt; 67 | $this->root = $root; 68 | $this->name = $name; 69 | } 70 | 71 | public function getId() { return $this->id; } 72 | 73 | public function getLeftValue() { return $this->lft; } 74 | public function setLeftValue($lft) { $this->lft = $lft; } 75 | 76 | public function getRightValue() { return $this->rgt; } 77 | public function setRightValue($rgt) { $this->rgt = $rgt; } 78 | 79 | public function getRootValue() { return $this->root; } 80 | public function setRootValue($root) { $this->root = $root; } 81 | 82 | public function getName() { return $this->name; } 83 | public function setName($name) { $this->name = $name; } 84 | 85 | public function __toString() { return $this->name; } 86 | 87 | public function getRelatedObj() { return $this->related; } 88 | public function setRelatedObj($related) { $this->related = $related; } 89 | } 90 | -------------------------------------------------------------------------------- /tests/DoctrineExtensions/NestedSet/Tests/Mocks/NonNodeMock.php: -------------------------------------------------------------------------------- 1 | node = $node; 43 | } 44 | 45 | public function getId() 46 | { 47 | return $this->id; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/DoctrineExtensions/NestedSet/Tests/Mocks/SingleRootNodeMock.php: -------------------------------------------------------------------------------- 1 | id = $id; 52 | $this->lft = $lft; 53 | $this->rgt = $rgt; 54 | $this->name = $name; 55 | } 56 | 57 | public function getId() { return $this->id; } 58 | 59 | public function getLeftValue() { return $this->lft; } 60 | public function setLeftValue($lft) { $this->lft = $lft; } 61 | 62 | public function getRightValue() { return $this->rgt; } 63 | public function setRightValue($rgt) { $this->rgt = $rgt; } 64 | 65 | public function getRootValue() { return null; } 66 | public function setRootValue($root) { } 67 | 68 | public function getName() { return $this->name; } 69 | public function setName($name) { $this->name = $name; } 70 | 71 | public function __toString() { return $this->name; } 72 | } 73 | -------------------------------------------------------------------------------- /tests/DoctrineExtensions/NestedSet/Tests/NodeWrapperTest.php: -------------------------------------------------------------------------------- 1 | getEntityManager(); 39 | $this->loadSchema(array($em->getClassMetadata('DoctrineExtensions\NestedSet\Tests\Mocks\NodeMock'))); 40 | 41 | $this->nsm = new ManagerMock($em, 'DoctrineExtensions\NestedSet\Tests\Mocks\NodeMock'); 42 | 43 | $this->nodes = array( 44 | new NodeMock(1, '1', 1, 10), # 0 45 | new NodeMock(2, '1.1', 2, 7), # 1 46 | new NodeMock(3, '1.1.1', 3, 4), # 2 47 | new NodeMock(4, '1.1.2', 5, 6), # 3 48 | new NodeMock(5, '1.2', 8, 9), # 4 49 | ); 50 | 51 | $this->nodes2 = array( 52 | new NodeMock(11, '1', 1, 12, 2), # 0 53 | new NodeMock(12, '1.1', 2, 7, 2), # 1 54 | new NodeMock(13, '1.1.1', 3, 4, 2), # 2 55 | new NodeMock(14, '1.1.2', 5, 6, 2), # 3 56 | new NodeMock(15, '1.2', 8, 9, 2), # 4 57 | new NodeMock(16, '1.3', 10, 11, 2), # 5 58 | ); 59 | 60 | 61 | $this->wrappers = array(); 62 | foreach($this->nodes as $node) 63 | { 64 | $em->persist($node); 65 | $this->wrappers[] = $this->nsm->wrapNode($node); 66 | } 67 | 68 | $this->wrappers2 = array(); 69 | foreach($this->nodes2 as $node) 70 | { 71 | $em->persist($node); 72 | $this->wrappers2[] = $this->nsm->wrapNode($node); 73 | } 74 | 75 | $em->flush(); 76 | } 77 | 78 | 79 | /** 80 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::__construct 81 | */ 82 | public function testConstructor() 83 | { 84 | $this->assertType('DoctrineExtensions\NestedSet\NodeWrapper', new NodeWrapper($this->nodes[2], $this->nsm), '->__construct() works'); 85 | } 86 | 87 | 88 | /** 89 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::__construct 90 | * @expectedException InvalidArgumentException 91 | */ 92 | public function testConstructorWithNodeWrapper() 93 | { 94 | new NodeWrapper($this->wrappers[0], $this->nsm); 95 | } 96 | 97 | 98 | /** 99 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getNode 100 | */ 101 | public function testGetNode() 102 | { 103 | $this->assertSame($this->nodes[0], $this->wrappers[0]->getNode(), '->getNode() returns wrapped node'); 104 | } 105 | 106 | 107 | /** 108 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::hasPrevSibling 109 | */ 110 | public function testHasPrevSibling() 111 | { 112 | $this->assertTrue($this->wrappers[3]->hasPrevSibling(), '->hasPrevSibling() returns true when previous sibling exists'); 113 | $this->assertFalse($this->wrappers[2]->hasPrevSibling(), '->hasPrevSibling() returns false when previous sibling doesn\'t exist'); 114 | $this->assertFalse($this->wrappers[0]->hasPrevSibling(), '->hasPrevSibling() returns false for root node'); 115 | } 116 | 117 | 118 | /** 119 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::hasNextSibling 120 | */ 121 | public function testHasNextSibling() 122 | { 123 | $this->assertTrue($this->wrappers[2]->hasNextSibling(), '->hasNextSibling() returns true when next sibling exists'); 124 | $this->assertFalse($this->wrappers[3]->hasNextSibling(), '->hasNextSibling() returns false when next sibling doesn\'t exist'); 125 | $this->assertFalse($this->wrappers[0]->hasNextSibling(), '->hasNextSibling() returns false for root node'); 126 | } 127 | 128 | 129 | /** 130 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::hasChildren 131 | */ 132 | public function testHasChildren() 133 | { 134 | $this->assertTrue($this->wrappers[0]->hasChildren()); 135 | $this->assertFalse($this->wrappers[4]->hasChildren()); 136 | } 137 | 138 | 139 | /** 140 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::hasParent 141 | */ 142 | public function testHasParent() 143 | { 144 | $this->assertFalse($this->wrappers[0]->hasParent()); 145 | $this->assertTrue($this->wrappers[4]->hasParent()); 146 | } 147 | 148 | 149 | /** 150 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::isRoot 151 | */ 152 | public function testIsRoot() 153 | { 154 | $this->assertTrue($this->wrappers[0]->isRoot()); 155 | $this->assertFalse($this->wrappers[1]->isRoot()); 156 | } 157 | 158 | 159 | /** 160 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::isLeaf 161 | */ 162 | public function testIsLeaf() 163 | { 164 | $this->assertFalse($this->wrappers[0]->isLeaf()); 165 | $this->assertTrue($this->wrappers[2]->isLeaf()); 166 | $this->assertTrue($this->wrappers[4]->isLeaf()); 167 | } 168 | 169 | 170 | /** 171 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::isValidNode 172 | */ 173 | public function testIsValidNode() 174 | { 175 | $this->assertTrue($this->wrappers[0]->isValidNode(), '->isValidNode() returns true for valid nodes'); 176 | 177 | $this->wrappers[0]->setLeftValue(12); 178 | $this->assertFalse($this->wrappers[0]->isValidNode(), '->isValidNode() returns false for invalid nodes'); 179 | } 180 | 181 | 182 | /** 183 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::invalidate 184 | */ 185 | public function testInvalidate() 186 | { 187 | $this->wrappers[0]->invalidate(); 188 | } 189 | 190 | 191 | /** 192 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::isDescendantOf 193 | */ 194 | public function testIsDescendantOf() 195 | { 196 | $this->assertTrue($this->wrappers[3]->isDescendantOf($this->wrappers[0])); 197 | $this->assertTrue($this->wrappers[2]->isDescendantOf($this->nodes[1])); 198 | $this->assertFalse($this->wrappers[1]->isDescendantOf($this->wrappers[4])); 199 | $this->assertFalse($this->wrappers[1]->isDescendantOf($this->nodes[2])); 200 | } 201 | 202 | 203 | /* 204 | public function testIsSiblingOf() 205 | { 206 | $this->assertTrue($this->wrappers[3]->isSiblingOf($this->wrappers[2])); 207 | $this->assertTrue($this->wrappers[4]->isSiblingOf($this->nodes[1])); 208 | $this->assertFalse($this->wrappers[1]->isSiblingOf($this->wrappers[0])); 209 | $this->assertFalse($this->wrappers[1]->isSiblingOf($this->nodes[2])); 210 | } 211 | */ 212 | 213 | 214 | /** 215 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::isAncestorOf 216 | */ 217 | public function testIsAncestorOf() 218 | { 219 | $this->assertTrue($this->wrappers[0]->isAncestorOf($this->wrappers[1])); 220 | $this->assertTrue($this->wrappers[1]->isAncestorOf($this->nodes[2])); 221 | $this->assertFalse($this->wrappers[4]->isAncestorOf($this->wrappers[1])); 222 | $this->assertFalse($this->wrappers[3]->isAncestorOf($this->wrappers[0])); 223 | } 224 | 225 | 226 | /** 227 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::isEqualTo 228 | */ 229 | public function testIsEqualTo() 230 | { 231 | $this->assertTrue($this->wrappers[0]->isEqualTo($this->nodes[0]), '->isEqualTo() returns true for equal nodes'); 232 | $this->assertFalse($this->wrappers[2]->isEqualTo($this->wrappers[3]), '->isEqualTo() returns false for non-equal nodes'); 233 | } 234 | 235 | 236 | /** 237 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getParent 238 | */ 239 | public function testGetParent() 240 | { 241 | $this->assertEquals($this->nodes[0]->getId(), $this->wrappers[1]->getParent()->getNode()->getId()); 242 | $this->assertNull($this->wrappers[0]->getParent()); 243 | 244 | $this->wrappers[3]->getAncestors(); 245 | $this->assertEquals($this->nodes[1]->getId(), $this->wrappers[3]->getParent()->getNode()->getId()); 246 | } 247 | 248 | 249 | /** 250 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getAncestors 251 | */ 252 | public function testGetAncestors() 253 | { 254 | $a = $this->wrappers[3]->getAncestors(); 255 | $this->assertEquals( 256 | array($this->nodes[0]->getId(), $this->nodes[1]->getId()), 257 | array_map(function($node) {return $node->getNode()->getId();}, $a) 258 | ); 259 | 260 | $this->assertEmpty($this->wrappers[0]->getAncestors()); 261 | } 262 | 263 | 264 | /** 265 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getPath 266 | */ 267 | public function testGetPath() 268 | { 269 | $this->assertEquals('1 > 1.1', $this->wrappers[3]->getPath(), '->getPath() works with default parameters'); 270 | 271 | $this->assertEquals('1 | 1.1', $this->wrappers[3]->getPath(' | '), '->getPath() supports custom separator'); 272 | 273 | $this->assertEquals('1 > 1.1 > 1.1.2', $this->wrappers[3]->getPath(' > ', true), '->getPath() can include self'); 274 | 275 | $this->assertEquals('', $this->wrappers[0]->getPath(), '->getPath() returns empty string for root node'); 276 | } 277 | 278 | 279 | /** 280 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getOutlineNumber 281 | */ 282 | public function testGetOutlineNumber() 283 | { 284 | $this->assertEquals('1.1.1', $this->wrappers[2]->getOutlineNumber(), '->getOutlineNumber() works for 1.1.1'); 285 | $this->assertEquals('1.1.2', $this->wrappers[3]->getOutlineNumber(), '->getOutlineNumber() works for 1.1.2'); 286 | $this->assertEquals('', $this->wrappers[0]->getOutlineNumber('.',false), '->getOutlineNumber() works for root'); 287 | $this->assertEquals('2', $this->wrappers[4]->getOutlineNumber('.',false), '->getOutlineNumber() works with includeNode=false'); 288 | $this->assertEquals('1-1', $this->wrappers[1]->getOutlineNumber('-'), '->getOutlineNumber() supports separator'); 289 | } 290 | 291 | 292 | /** 293 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getDescendants 294 | * @covers DoctrineExtensions\NestedSet\Manager::filterNodeDepth 295 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getLeftFieldName 296 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getRightFieldName 297 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getRootFieldName 298 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::hasManyRoots 299 | */ 300 | public function testGetDescendants() 301 | { 302 | $d = $this->wrappers[1]->getDescendants(); 303 | $this->assertEquals( 304 | array($this->nodes[2]->getId(), $this->nodes[3]->getId()), 305 | array_map(function($node) {return $node->getNode()->getId();}, $d), 306 | '->getDescendants() depth=unlimited' 307 | ); 308 | 309 | $this->assertEmpty($this->wrappers[0]->getDescendants(0), '->getDescendants() depth=0'); 310 | 311 | $d = $this->wrappers[0]->getDescendants(1); 312 | $this->assertEquals( 313 | array($this->nodes[1]->getId(), $this->nodes[4]->getId()), 314 | array_map(function($node) {return $node->getNode()->getId();}, $d), 315 | '->getDescendants() depth=1' 316 | ); 317 | 318 | $d = $this->wrappers[0]->getDescendants(2); 319 | $this->assertEquals( 320 | array($this->nodes[1]->getId(), $this->nodes[2]->getId(), $this->nodes[3]->getId(), $this->nodes[4]->getId()), 321 | array_map(function($node) {return $node->getNode()->getId();}, $d), 322 | '->getDescendants() depth=2' 323 | ); 324 | } 325 | 326 | 327 | /** 328 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getDescendants 329 | */ 330 | public function testGetDescendants_None() 331 | { 332 | $d = $this->wrappers[2]->getDescendants(); 333 | $this->assertEmpty($d, '->getDescendants() works with no children'); 334 | } 335 | 336 | 337 | /** 338 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getPrevSibling 339 | */ 340 | public function testGetPrevSibling_UnloadedTree() 341 | { 342 | $this->assertEquals($this->nodes[2]->getId(), $this->wrappers[3]->getPrevSibling()->getNode()->getId(), '->getPrevSibling() returns previous sibling'); 343 | 344 | $this->assertNull($this->wrappers[2]->getPrevSibling(), '->getPrevSibling() returns null for first sibling'); 345 | } 346 | 347 | 348 | /** 349 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getPrevSibling 350 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::internalGetChildren 351 | */ 352 | public function testGetPrevSibling_LoadedTree() 353 | { 354 | $this->wrappers[3]->getParent(); 355 | $this->assertEquals($this->nodes[2]->getId(), $this->wrappers[3]->getPrevSibling()->getNode()->getId(), '->getPrevSibling() returns previous sibling with parent loaded'); 356 | 357 | $this->wrappers[2]->getParent(); 358 | $this->wrappers[1]->getChildren(); 359 | 360 | $this->assertEquals($this->nodes[2]->getId(), $this->wrappers[3]->getPrevSibling()->getNode()->getId(), '->getPrevSibling() returns previous sibling'); 361 | 362 | $this->assertNull($this->wrappers[2]->getPrevSibling(), '->getPrevSibling() returns null for first sibling'); 363 | } 364 | 365 | 366 | /** 367 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getNextSibling 368 | */ 369 | public function testGetNextSibling() 370 | { 371 | $this->assertEquals($this->nodes[3]->getId(), $this->wrappers[2]->getNextSibling()->getNode()->getId(), '->getNextSibling() returns next sibling'); 372 | 373 | $this->assertNull($this->wrappers[3]->getNextSibling(), '->getNextSibling() returns null for last sibling'); 374 | } 375 | 376 | 377 | /** 378 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getNextSibling 379 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::internalGetChildren 380 | */ 381 | public function testGetNextSibling_LoadedTree() 382 | { 383 | $this->wrappers[2]->getParent(); 384 | $this->assertEquals($this->nodes[3]->getId(), $this->wrappers[2]->getNextSibling()->getNode()->getId(), '->getNextSibling() returns previous sibling with parent loaded'); 385 | 386 | $this->wrappers[3]->getParent(); 387 | $this->wrappers[1]->getChildren(); 388 | 389 | $this->assertEquals($this->nodes[3]->getId(), $this->wrappers[2]->getNextSibling()->getNode()->getId(), '->getNextSibling() returns next sibling'); 390 | 391 | $this->assertNull($this->wrappers[3]->getNextSibling(), '->getNextSibling() returns null for last sibling'); 392 | } 393 | 394 | 395 | /** 396 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getSiblings 397 | */ 398 | public function testGetSiblings() 399 | { 400 | $siblings = $this->wrappers[2]->getSiblings(); 401 | $this->assertEquals( 402 | array($this->nodes[3]->getId()), 403 | array_map(function($node) {return $node->getNode()->getId();}, $siblings), 404 | '->getSiblings() excludes current node by default' 405 | ); 406 | 407 | $siblings = $this->wrappers[2]->getSiblings(true); 408 | $this->assertEquals( 409 | array($this->nodes[2]->getId(), $this->nodes[3]->getId()), 410 | array_map(function($node) {return $node->getNode()->getId();}, $siblings), 411 | '->getSiblings() includes current node' 412 | ); 413 | } 414 | 415 | /** 416 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getFirstChild 417 | */ 418 | public function testGetFirstChild() 419 | { 420 | $this->assertNull($this->wrappers[2]->getFirstChild(), '->getFirstChild() returns null for leaf node'); 421 | 422 | $this->assertEquals($this->nodes[2]->getId(), $this->wrappers[1]->getFirstChild()->getNode()->getId(), '->getFirstChild() queries for child'); 423 | 424 | $this->wrappers[0]->getChildren(); 425 | $this->assertEquals($this->nodes[1]->getId(), $this->wrappers[0]->getFirstChild()->getNode()->getId(), '->getFirstChild() works when children exist'); 426 | } 427 | 428 | 429 | /** 430 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getLastChild 431 | */ 432 | public function testGetLastChild() 433 | { 434 | $this->assertNull($this->wrappers[2]->getLastChild(), '->getLastChild() returns null for leaf node'); 435 | 436 | $this->assertEquals($this->nodes[3]->getId(), $this->wrappers[1]->getLastChild()->getNode()->getId(), '->getLastChild() queries for child'); 437 | 438 | $this->wrappers[0]->getChildren(); 439 | $this->assertEquals($this->nodes[4]->getId(), $this->wrappers[0]->getLastChild()->getNode()->getId(), '->getLastChild() works when children exist'); 440 | } 441 | 442 | 443 | /** 444 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getChildren 445 | */ 446 | public function testGetChildren() 447 | { 448 | $c = $this->wrappers[1]->getChildren(); 449 | $this->assertEquals( 450 | array($this->nodes[2]->getId(), $this->nodes[3]->getId()), 451 | array_map(function($node) {return $node->getNode()->getId();}, $c) 452 | ); 453 | 454 | $c = $this->wrappers[0]->getChildren(); 455 | $this->assertEquals( 456 | array($this->nodes[1]->getId(), $this->nodes[4]->getId()), 457 | array_map(function($node) {return $node->getNode()->getId();}, $c) 458 | ); 459 | 460 | $this->assertEmpty($this->wrappers[2]->getChildren(), 'no children'); 461 | } 462 | 463 | 464 | /** 465 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getNumberChildren 466 | */ 467 | public function testGetNumberChildren() 468 | { 469 | $this->assertEquals(2, $this->wrappers[1]->getNumberChildren()); 470 | $this->assertEquals(0, $this->wrappers[3]->getNumberChildren()); 471 | } 472 | 473 | 474 | /** 475 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getNumberDescendants 476 | */ 477 | public function testGetNumberDescendants() 478 | { 479 | $this->assertEquals(4, $this->wrappers[0]->getNumberDescendants()); 480 | $this->assertEquals(0, $this->wrappers[2]->getNumberDescendants()); 481 | } 482 | 483 | 484 | /** 485 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getLevel 486 | */ 487 | public function testGetLevel() 488 | { 489 | $this->assertEquals(0, $this->wrappers[0]->getLevel(), '->getLevel() works for root node'); 490 | $this->assertEquals(2, $this->wrappers[3]->getLevel(), '->getLevel() works for leaf node'); 491 | } 492 | 493 | 494 | /** 495 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsParentOf 496 | */ 497 | public function testInsertAsParentOf() 498 | { 499 | $newWrapper = $this->nsm->wrapNode(new NodeMock(6, '1.1', 0, 0, 0)); 500 | 501 | $newWrapper->insertAsParentOf($this->wrappers[4]); 502 | $this->assertEquals(8, $newWrapper->getLeftValue(), '->insertAsParentOf() updates new node\'s left value'); 503 | $this->assertEquals(11, $newWrapper->getRightValue(), '->insertAsParentOf() updates new node\'s right value'); 504 | $this->assertEquals(1, $newWrapper->getRootValue(), '->insertAsParentOf() updates new node\'s root value'); 505 | $this->assertEquals(9, $this->wrappers[4]->getLeftValue(), '->insertAsParentOf() updates next node\'s left value'); 506 | $this->assertEquals(10, $this->wrappers[4]->getRightValue(), '->insertAsParentOf() updates next node\'s right value'); 507 | $this->assertEquals(12, $this->wrappers[0]->getRightValue(), '->insertAsParentOf() updates parent node\'s right value'); 508 | } 509 | 510 | 511 | /** 512 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsParentOf 513 | * @expectedException InvalidArgumentException 514 | */ 515 | public function testInsertAsParentOf_CantInsertSelf() 516 | { 517 | $this->wrappers[1]->insertAsParentOf($this->wrappers[1]); 518 | } 519 | 520 | 521 | /** 522 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsParentOf 523 | * @expectedException InvalidArgumentException 524 | */ 525 | public function testInsertAsParentOf_CantInsertValidNode() 526 | { 527 | $newWrapper = $this->nsm->wrapNode(new NodeMock(6, '1.1', 0, 0, 0)); 528 | $this->wrappers[1]->insertAsParentOf($newWrapper); 529 | } 530 | 531 | 532 | /** 533 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsParentOf 534 | * @expectedException InvalidArgumentException 535 | */ 536 | public function testInsertAsParentOf_CantInsertRoot() 537 | { 538 | $newWrapper = $this->nsm->wrapNode(new NodeMock(6, '1.1', 0, 0, 0)); 539 | $newWrapper->insertAsParentOf($this->wrappers[0]); 540 | } 541 | 542 | 543 | 544 | /** 545 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsPrevSiblingOf 546 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 547 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertNode 548 | */ 549 | public function testInsertAsPrevSiblingOf() 550 | { 551 | $newNode = new NodeWrapper(new NodeMock(21, '1.1.1(.5)'), $this->nsm); 552 | 553 | $newNode->insertAsPrevSiblingOf($this->wrappers[3]); 554 | $this->assertEquals(5, $newNode->getLeftValue(), '->insertAsPrevSiblingOf() updates new node\'s left value'); 555 | $this->assertEquals(6, $newNode->getRightValue(), '->insertAsPrevSiblingOf() updates new node\'s right value'); 556 | $this->assertEquals(3, $this->wrappers[2]->getLeftValue(), '->insertAsPrevSiblingOf updates prev node\'s left value'); 557 | $this->assertEquals(4, $this->wrappers[2]->getRightValue(), '->insertAsPrevSiblingOf updates prev node\'s right value'); 558 | $this->assertEquals(7, $this->wrappers[3]->getLeftValue(), '->insertAsPrevSiblingOf updates next node\'s left value'); 559 | $this->assertEquals(8, $this->wrappers[3]->getRightValue(), '->insertAsPrevSiblingOf updates next node\'s right value'); 560 | } 561 | 562 | 563 | /** 564 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsPrevSiblingOf 565 | * @expectedException InvalidArgumentException 566 | */ 567 | public function testInsertAsPrevSiblingOf_CantInsertSelf() 568 | { 569 | $newNode = new NodeWrapper(new NodeMock(21, '1.1.1(.5)'), $this->nsm); 570 | $newNode->insertAsPrevSiblingOf($newNode); 571 | } 572 | 573 | 574 | /** 575 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsNextSiblingOf 576 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 577 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertNode 578 | */ 579 | public function testInsertAsNextSiblingOf() 580 | { 581 | $newNode = new NodeWrapper(new NodeMock(21, '1.1.1(.5)'), $this->nsm); 582 | 583 | $newNode->insertAsNextSiblingOf($this->wrappers[2]); 584 | $this->assertEquals(5, $newNode->getLeftValue(), '->insertAsNextSiblingOf() updates new node\'s left value'); 585 | $this->assertEquals(6, $newNode->getRightValue(), '->insertAsNextSiblingOf() updates new node\'s right value'); 586 | $this->assertEquals(3, $this->wrappers[2]->getLeftValue(), '->insertAsNextSiblingOf updates prev node\'s left value'); 587 | $this->assertEquals(4, $this->wrappers[2]->getRightValue(), '->insertAsNextSiblingOf updates prev node\'s right value'); 588 | $this->assertEquals(7, $this->wrappers[3]->getLeftValue(), '->insertAsNextSiblingOf updates next node\'s left value'); 589 | $this->assertEquals(8, $this->wrappers[3]->getRightValue(), '->insertAsNextSiblingOf updates next node\'s right value'); 590 | } 591 | 592 | 593 | /** 594 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsNextSiblingOf 595 | * @expectedException InvalidArgumentException 596 | */ 597 | public function testInsertAsNextSiblingOf_CantInsertSelf() 598 | { 599 | $newNode = new NodeWrapper(new NodeMock(21, '1.1.1(.5)'), $this->nsm); 600 | $newNode->insertAsNextSiblingOf($newNode); 601 | } 602 | 603 | 604 | /** 605 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsFirstChildOf 606 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 607 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertNode 608 | */ 609 | public function testInsertAsFirstChildOf() 610 | { 611 | $newNode = new NodeWrapper(new NodeMock(21, '1.1.0'), $this->nsm); 612 | 613 | $newNode->insertAsFirstChildOf($this->wrappers[1]); 614 | $this->assertEquals(3, $newNode->getLeftValue(), '->insertAsFirstChildOf() updates new node\'s left value'); 615 | $this->assertEquals(4, $newNode->getRightValue(), '->insertAsFirstChildOf() updates new node\'s right value'); 616 | $this->assertEquals(2, $this->wrappers[1]->getLeftValue(), '->insertAsFirstChildOf updates parent node\'s left value'); 617 | $this->assertEquals(9, $this->wrappers[1]->getRightValue(), '->insertAsFirstChildOf updates parent node\'s right value'); 618 | $this->assertEquals(5, $this->wrappers[2]->getLeftValue(), '->insertAsFirstChildOf updates first child node\'s left value'); 619 | $this->assertEquals(6, $this->wrappers[2]->getRightValue(), '->insertAsFirstChildOf updates first child node\'s right value'); 620 | } 621 | 622 | 623 | /** 624 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsFirstChildOf 625 | * @expectedException InvalidArgumentException 626 | */ 627 | public function testInsertAsFirstChildOf_CantInsertSelf() 628 | { 629 | $newNode = new NodeWrapper(new NodeMock(21, '1.1.0'), $this->nsm); 630 | $newNode->insertAsFirstChildOf($newNode); 631 | } 632 | 633 | 634 | 635 | /** 636 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsLastChildOf 637 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 638 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertNode 639 | */ 640 | public function testInsertAsLastChildOf() 641 | { 642 | $newNode = new NodeWrapper(new NodeMock(21, '1.1.3'), $this->nsm); 643 | 644 | $newNode->insertAsLastChildOf($this->wrappers[1]); 645 | $this->assertEquals(7, $newNode->getLeftValue(), '->insertAsLastChildOf() updates new node\'s left value'); 646 | $this->assertEquals(8, $newNode->getRightValue(), '->insertAsLastChildOf() updates new node\'s right value'); 647 | $this->assertEquals(2, $this->wrappers[1]->getLeftValue(), '->insertAsLastChildOf updates parent node\'s left value'); 648 | $this->assertEquals(9, $this->wrappers[1]->getRightValue(), '->insertAsLastChildOf updates parent node\'s right value'); 649 | $this->assertEquals(5, $this->wrappers[3]->getLeftValue(), '->insertAsLastChildOf doesn\'t update last child node\'s left value'); 650 | $this->assertEquals(6, $this->wrappers[3]->getRightValue(), '->insertAsLastChildOf doesn\'t update last child node\'s right value'); 651 | } 652 | 653 | 654 | /** 655 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsLastChildOf 656 | * @expectedException InvalidArgumentException 657 | */ 658 | public function testInsertAsLastChildOf_CantInsertSelf() 659 | { 660 | $newNode = new NodeWrapper(new NodeMock(21, '1.1.3'), $this->nsm); 661 | $newNode->insertAsLastChildOf($newNode); 662 | } 663 | 664 | 665 | /** 666 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveAsPrevSiblingOf 667 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::updateNode 668 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 669 | */ 670 | public function testMoveAsPrevSiblingOf() 671 | { 672 | $this->wrappers[4]->moveAsPrevSiblingOf($this->wrappers[1]); 673 | $this->assertEquals(2, $this->wrappers[4]->getLeftValue(), '->moveAsPrevSiblingOf() updates moved node\'s left value'); 674 | $this->assertEquals(3, $this->wrappers[4]->getRightValue(), '->moveAsPrevSiblingOf() updates moved node\'s right value'); 675 | $this->assertEquals(4, $this->wrappers[1]->getLeftValue(), '->moveAsPrevSiblingOf() updates next node\'s left value'); 676 | $this->assertEquals(9, $this->wrappers[1]->getRightValue(), '->moveAsPrevSiblingOf() updates next node\'s right value'); 677 | } 678 | 679 | 680 | /** 681 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveAsPrevSiblingOf 682 | * @expectedException InvalidArgumentException 683 | */ 684 | public function testMoveAsPrevSiblingOf_CantMoveSelf() 685 | { 686 | $this->wrappers[1]->moveAsPrevSiblingOf($this->wrappers[1]); 687 | } 688 | 689 | 690 | /** 691 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveAsPrevSiblingOf 692 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveBetweenTrees 693 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 694 | */ 695 | public function testMoveAsPrevSiblingBetweenTrees() 696 | { 697 | $this->wrappers2[1]->moveAsPrevSiblingOf($this->wrappers[2]); 698 | $this->assertEquals(3, $this->wrappers2[1]->getLeftValue(), '->moveAsPrevSiblingOf() updates moved node\'s left value'); 699 | $this->assertEquals(8, $this->wrappers2[1]->getRightValue(), '->moveAsPrevSiblingOf() updates moved node\'s right value'); 700 | $this->assertEquals(1, $this->wrappers2[1]->getRootValue(), '->moveAsPrevSiblingOf() updates moved node\'s root value'); 701 | $this->assertEquals(9, $this->wrappers[2]->getLeftValue(), '->moveAsPrevSiblingOf() updates next node\'s left value'); 702 | $this->assertEquals(10, $this->wrappers[2]->getRightValue(), '->moveAsPrevSiblingOf() updates next node\'s right value'); 703 | $this->assertEquals(2, $this->wrappers2[4]->getLeftValue(), '->moveAsPrevSiblingOf() updates old tree next node\'s left value'); 704 | $this->assertEquals(3, $this->wrappers2[4]->getRightValue(), '->moveAsPrevSiblingOf() updates old tree next node\'s right value'); 705 | $this->assertEquals(4, $this->wrappers2[2]->getLeftValue(), '->moveAsPrevSiblingOf() updates descendant node\'s left value'); 706 | $this->assertEquals(5, $this->wrappers2[2]->getRightValue(), '->moveAsPrevSiblingOf() updates descendant node\'s right value'); 707 | $this->assertEquals(1, $this->wrappers2[2]->getRootValue(), '->moveAsPrevSiblingOf() updates descendant node\'s root value'); 708 | } 709 | 710 | 711 | /** 712 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveAsNextSiblingOf 713 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::updateNode 714 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 715 | */ 716 | public function testMoveAsNextSiblingOf() 717 | { 718 | $this->wrappers[1]->moveAsNextSiblingOf($this->wrappers[4]); 719 | $this->assertEquals(4, $this->wrappers[1]->getLeftValue(), '->moveAsNextSiblingOf() updates moved node\'s left value'); 720 | $this->assertEquals(9, $this->wrappers[1]->getRightValue(), '->moveAsNextSiblingOf() updates moved node\'s right value'); 721 | $this->assertEquals(2, $this->wrappers[4]->getLeftValue(), '->moveAsNextSiblingOf() updates previous node\'s left value'); 722 | $this->assertEquals(3, $this->wrappers[4]->getRightValue(), '->moveAsNextSiblingOf() updates previous node\'s right value'); 723 | } 724 | 725 | 726 | /** 727 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveAsNextSiblingOf 728 | * @expectedException InvalidArgumentException 729 | */ 730 | public function testMoveAsNextSiblingOf_CantMoveSelf() 731 | { 732 | $this->wrappers[1]->moveAsNextSiblingOf($this->wrappers[1]); 733 | } 734 | 735 | 736 | /** 737 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveAsNextSiblingOf 738 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveBetweenTrees 739 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 740 | */ 741 | public function testMoveAsNextSiblingBetweenTrees() 742 | { 743 | $this->wrappers2[1]->moveAsNextSiblingOf($this->wrappers[2]); 744 | $this->assertEquals(5, $this->wrappers2[1]->getLeftValue(), '->moveAsNextSiblingOf() updates moved node\'s left value'); 745 | $this->assertEquals(10, $this->wrappers2[1]->getRightValue(), '->moveAsNextSiblingOf() updates moved node\'s right value'); 746 | $this->assertEquals(1, $this->wrappers2[1]->getRootValue(), '->moveAsNextSiblingOf() updates moved node\'s root value'); 747 | $this->assertEquals(11, $this->wrappers[3]->getLeftValue(), '->moveAsNextSiblingOf() updates next node\'s left value'); 748 | $this->assertEquals(12, $this->wrappers[3]->getRightValue(), '->moveAsNextSiblingOf() updates next node\'s right value'); 749 | $this->assertEquals(2, $this->wrappers2[4]->getLeftValue(), '->moveAsNextSiblingOf() updates old tree next node\'s left value'); 750 | $this->assertEquals(3, $this->wrappers2[4]->getRightValue(), '->moveAsNextSiblingOf() updates old tree next node\'s right value'); 751 | $this->assertEquals(6, $this->wrappers2[2]->getLeftValue(), '->moveAsNextSiblingOf() updates descendant node\'s left value'); 752 | $this->assertEquals(7, $this->wrappers2[2]->getRightValue(), '->moveAsNextSiblingOf() updates descendant node\'s right value'); 753 | $this->assertEquals(1, $this->wrappers2[2]->getRootValue(), '->moveAsNextSiblingOf() updates descendant node\'s root value'); 754 | } 755 | 756 | 757 | /** 758 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveAsFirstChildOf 759 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::updateNode 760 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 761 | */ 762 | public function testMoveAsFirstChildOf() 763 | { 764 | $this->wrappers[4]->moveAsFirstChildOf($this->wrappers[2]); 765 | $this->assertEquals(4, $this->wrappers[4]->getLeftValue(), '->moveAsFirstChildOf() updates moved node\'s left value'); 766 | $this->assertEquals(5, $this->wrappers[4]->getRightValue(), '->moveAsFirstChildOf() updates moved node\'s right value'); 767 | $this->assertEquals(3, $this->wrappers[2]->getLeftValue(), '->moveAsFirstChildOf() updates parent node\'s left value'); 768 | $this->assertEquals(6, $this->wrappers[2]->getRightValue(), '->moveAsFirstChildOf() updates parent node\'s right value'); 769 | $this->assertEquals(7, $this->wrappers[3]->getLeftValue(), '->moveAsFirstChildOf() updates next node\'s left value'); 770 | $this->assertEquals(8, $this->wrappers[3]->getRightValue(), '->moveAsFirstChildOf() updates next node\'s right value'); 771 | } 772 | 773 | 774 | /** 775 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveAsFirstChildOf 776 | * @expectedException InvalidArgumentException 777 | */ 778 | public function testMoveAsFirstChildOf_CantMoveSelf() 779 | { 780 | $this->wrappers[1]->moveAsFirstChildOf($this->wrappers[1]); 781 | } 782 | 783 | 784 | /** 785 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveAsFirstChildOf 786 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveBetweenTrees 787 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 788 | */ 789 | public function testMoveAsFirstChildBetweenTrees() 790 | { 791 | $this->wrappers2[1]->moveAsFirstChildOf($this->wrappers[1]); 792 | $this->assertEquals(3, $this->wrappers2[1]->getLeftValue(), '->moveAsFirstChildOf() updates moved node\'s left value'); 793 | $this->assertEquals(8, $this->wrappers2[1]->getRightValue(), '->moveAsFirstChildOf() updates moved node\'s right value'); 794 | $this->assertEquals(1, $this->wrappers2[1]->getRootValue(), '->moveAsFirstChildOf() updates moved node\'s root value'); 795 | $this->assertEquals(9, $this->wrappers[2]->getLeftValue(), '->moveAsFirstChildOf() updates next node\'s left value'); 796 | $this->assertEquals(10, $this->wrappers[2]->getRightValue(), '->moveAsFirstChildOf() updates next node\'s right value'); 797 | $this->assertEquals(2, $this->wrappers2[4]->getLeftValue(), '->moveAsFirstChildOf() updates old tree next node\'s left value'); 798 | $this->assertEquals(3, $this->wrappers2[4]->getRightValue(), '->moveAsFirstChildOf() updates old tree next node\'s right value'); 799 | $this->assertEquals(4, $this->wrappers2[2]->getLeftValue(), '->moveAsFirstChildOf() updates descendant node\'s left value'); 800 | $this->assertEquals(5, $this->wrappers2[2]->getRightValue(), '->moveAsFirstChildOf() updates descendant node\'s right value'); 801 | $this->assertEquals(1, $this->wrappers2[2]->getRootValue(), '->moveAsFirstChildOf() updates descendant node\'s root value'); 802 | } 803 | 804 | 805 | /** 806 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveAsLastChildOf 807 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::updateNode 808 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 809 | */ 810 | public function testMoveAsLastChildOf() 811 | { 812 | $this->wrappers[4]->moveAsLastChildOf($this->wrappers[1]); 813 | $this->assertEquals(7, $this->wrappers[4]->getLeftValue(), '->moveAsLastChildOf() updates moved node\'s left value'); 814 | $this->assertEquals(8, $this->wrappers[4]->getRightValue(), '->moveAsLastChildOf() updates moved node\'s right value'); 815 | $this->assertEquals(2, $this->wrappers[1]->getLeftValue(), '->moveAsLastChildOf() updates parent node\'s left value'); 816 | $this->assertEquals(9, $this->wrappers[1]->getRightValue(), '->moveAsLastChildOf() updates parent node\'s right value'); 817 | } 818 | 819 | 820 | /** 821 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveAsLastChildOf 822 | * @expectedException InvalidArgumentException 823 | */ 824 | public function testMoveAsLastChildOf_CantMoveSelf() 825 | { 826 | $this->wrappers[1]->moveAsLastChildOf($this->wrappers[1]); 827 | } 828 | 829 | 830 | /** 831 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveAsLastChildOf 832 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveBetweenTrees 833 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 834 | */ 835 | public function testMoveAsLastChildBetweenTrees() 836 | { 837 | $this->wrappers2[1]->moveAsLastChildOf($this->wrappers[1]); 838 | $this->assertEquals(7, $this->wrappers2[1]->getLeftValue(), '->moveAsLastChildOf() updates moved node\'s left value'); 839 | $this->assertEquals(12, $this->wrappers2[1]->getRightValue(), '->moveAsLastChildOf() updates moved node\'s right value'); 840 | $this->assertEquals(1, $this->wrappers2[1]->getRootValue(), '->moveAsLastChildOf() updates moved node\'s root value'); 841 | $this->assertEquals(2, $this->wrappers[1]->getLeftValue(), '->moveAsLastChildOf() updates parent node\'s left value'); 842 | $this->assertEquals(13, $this->wrappers[1]->getRightValue(), '->moveAsLastChildOf() updates parent node\'s right value'); 843 | $this->assertEquals(2, $this->wrappers2[4]->getLeftValue(), '->moveAsLastChildOf() updates old tree next node\'s left value'); 844 | $this->assertEquals(3, $this->wrappers2[4]->getRightValue(), '->moveAsLastChildOf() updates old tree next node\'s right value'); 845 | $this->assertEquals(8, $this->wrappers2[2]->getLeftValue(), '->moveAsLastChildOf() updates descendant node\'s left value'); 846 | $this->assertEquals(9, $this->wrappers2[2]->getRightValue(), '->moveAsLastChildOf() updates descendant node\'s right value'); 847 | $this->assertEquals(1, $this->wrappers2[2]->getRootValue(), '->moveAsLastChildOf() updates descendant node\'s root value'); 848 | } 849 | 850 | 851 | /** 852 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::makeRoot 853 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 854 | */ 855 | public function testMakeRoot() 856 | { 857 | $this->wrappers[1]->makeRoot(3); 858 | $this->assertEquals(1, $this->wrappers[1]->getLeftValue(), '->makeRoot() updates new root node\'s left value'); 859 | $this->assertEquals(6, $this->wrappers[1]->getRightValue(), '->makeRoot() updates new root node\'s right value'); 860 | $this->assertEquals(3, $this->wrappers[1]->getRootValue(), '->makeRoot() updates new root node\'s root value'); 861 | $this->assertEquals(2, $this->wrappers[2]->getLeftValue(), '->makeRoot() updates child node\'s left value'); 862 | $this->assertEquals(3, $this->wrappers[2]->getRightValue(), '->makeRoot() updates child node\'s right value'); 863 | $this->assertEquals(3, $this->wrappers[3]->getRootValue(), '->makeRoot() updates child node\'s root value'); 864 | $this->assertEquals(2, $this->wrappers[4]->getLeftValue(), '->makeRoot() updates old tree next node\'s left value'); 865 | $this->assertEquals(3, $this->wrappers[4]->getRightValue(), '->makeRoot() updates old tree next node\'s right value'); 866 | $this->assertEquals(4, $this->wrappers[0]->getRightValue(), '->makeRoot() updates old tree parent node\'s right value'); 867 | 868 | $this->wrappers[1]->makeRoot(4); 869 | $this->assertEquals(3, $this->wrappers[1]->getRootValue(), '->makeRoot() doesn\'t change existing roots'); 870 | } 871 | 872 | 873 | /** 874 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::addChild 875 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsLastChildOf 876 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 877 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertNode 878 | */ 879 | public function testAddChild_Node() 880 | { 881 | $newWrapper = $this->wrappers[1]->addChild(new NodeMock(6, '1.1.3', 0, 0, 0)); 882 | $this->assertInstanceOf('DoctrineExtensions\NestedSet\NodeWrapper', $newWrapper, '->addChild() returns a NodeWrapper'); 883 | $this->assertEquals(7, $newWrapper->getLeftValue(), '->addChild() updates new node\'s left value'); 884 | $this->assertEquals(8, $newWrapper->getRightValue(), '->addChild() updates new node\'s right value'); 885 | $this->assertEquals(1, $newWrapper->getRootValue(), '->addChild() updates new node\'s root value'); 886 | $this->assertEquals(9, $this->wrappers[1]->getRightValue(), '->addChild() updates parent\'s right value'); 887 | } 888 | 889 | 890 | /** 891 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::addChild 892 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsLastChildOf 893 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 894 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertNode 895 | */ 896 | public function testAddChild_NodeWrapper() 897 | { 898 | $wrapper = $this->nsm->wrapNode(new NodeMock(6, '1.1.3', 0, 0, 0)); 899 | $newWrapper = $this->wrappers[1]->addChild($wrapper); 900 | $this->assertSame($wrapper, $newWrapper, '->addChild() returns original wrapper when passing a NodeWrapper'); 901 | $this->assertEquals(7, $newWrapper->getLeftValue(), '->addChild() updates new node\'s left value'); 902 | $this->assertEquals(8, $newWrapper->getRightValue(), '->addChild() updates new node\'s right value'); 903 | $this->assertEquals(1, $newWrapper->getRootValue(), '->addChild() updates new node\'s root value'); 904 | $this->assertEquals(9, $this->wrappers[1]->getRightValue(), '->addChild() updates parent\'s right value'); 905 | } 906 | 907 | 908 | /** 909 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::addChild 910 | * @expectedException InvalidArgumentException 911 | */ 912 | public function testAddChild_CantMoveSelf() 913 | { 914 | $this->wrappers[1]->addChild($this->wrappers[1]); 915 | } 916 | 917 | 918 | /** 919 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::delete 920 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 921 | */ 922 | public function testDelete() 923 | { 924 | $this->wrappers[1]->delete(); 925 | $this->assertEquals(2, $this->wrappers[4]->getLeftValue(), '->delete() updates next node\'s left value'); 926 | $this->assertEquals(3, $this->wrappers[4]->getRightValue(), '->delete() updates next node\'s right value'); 927 | $this->assertEquals(4, $this->wrappers[0]->getRightValue(), '->delete() updates parent node\'s right value'); 928 | $this->assertFalse($this->wrappers[1]->isValidNode(), '->delete() sets deleted node to invalid'); 929 | $this->assertFalse($this->wrappers[2]->isValidNode(), '->delete() sets descendant node\'s to invalid'); 930 | } 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | /** 945 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getManager 946 | */ 947 | public function testGetManager() 948 | { 949 | $this->assertSame($this->nsm, $this->wrappers[0]->getManager()); 950 | } 951 | 952 | 953 | 954 | // 955 | // Node Interface Methods 956 | // 957 | 958 | /** 959 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getLeftValue 960 | */ 961 | public function testGetLeftValue() 962 | { 963 | $this->assertEquals(2, $this->wrappers[1]->getLeftValue()); 964 | } 965 | 966 | 967 | /** 968 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::setLeftValue 969 | */ 970 | public function testSetLeftValue() 971 | { 972 | $this->wrappers[1]->setLeftValue(1); 973 | $this->assertEquals(1, $this->wrappers[1]->getLeftValue()); 974 | } 975 | 976 | 977 | /** 978 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getRightValue 979 | */ 980 | public function testGetRightValue() 981 | { 982 | $this->assertEquals(6, $this->wrappers[3]->getRightValue()); 983 | } 984 | 985 | 986 | /** 987 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::setRightValue 988 | */ 989 | public function testSetRightValue() 990 | { 991 | $this->wrappers[0]->setRightValue(2); 992 | $this->assertEquals(2, $this->wrappers[0]->getRightValue()); 993 | } 994 | 995 | 996 | /** 997 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getRootValue 998 | */ 999 | public function testGetRootValue() 1000 | { 1001 | $this->assertEquals(1, $this->wrappers[0]->getRootValue()); 1002 | } 1003 | 1004 | 1005 | /** 1006 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::setRootValue 1007 | */ 1008 | public function testSetRootValue() 1009 | { 1010 | $this->wrappers[0]->setRootValue(4); 1011 | $this->assertEquals(4, $this->wrappers[0]->getRootValue()); 1012 | } 1013 | 1014 | 1015 | /** 1016 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::__toString 1017 | */ 1018 | public function testToString() 1019 | { 1020 | $this->assertType('string', $this->wrappers[0]->__toString(), '->__toString() returns a string'); 1021 | } 1022 | 1023 | 1024 | /** 1025 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getId 1026 | */ 1027 | public function testGetId() 1028 | { 1029 | $id = $this->wrappers[0]->getId(); 1030 | $this->assertNotEmpty($id); 1031 | } 1032 | } 1033 | -------------------------------------------------------------------------------- /tests/DoctrineExtensions/NestedSet/Tests/SingleRootNodeWrapperTest.php: -------------------------------------------------------------------------------- 1 | getEntityManager(); 37 | $this->loadSchema(array($em->getClassMetadata('DoctrineExtensions\NestedSet\Tests\Mocks\SingleRootNodeMock'))); 38 | 39 | $this->nsm = new ManagerMock($em, 'DoctrineExtensions\NestedSet\Tests\Mocks\SingleRootNodeMock'); 40 | $this->nsm->getConfiguration()->setRootFieldName(null); 41 | 42 | $this->nodes = array( 43 | new SingleRootNodeMock(1, '1', 1, 10), # 0 44 | new SingleRootNodeMock(2, '1.1', 2, 7), # 1 45 | new SingleRootNodeMock(3, '1.1.1', 3, 4), # 2 46 | new SingleRootNodeMock(4, '1.1.2', 5, 6), # 3 47 | new SingleRootNodeMock(5, '1.2', 8, 9), # 4 48 | ); 49 | 50 | $this->wrappers = array(); 51 | foreach($this->nodes as $node) 52 | { 53 | $em->persist($node); 54 | $this->wrappers[] = $this->nsm->wrapNode($node); 55 | } 56 | 57 | $em->flush(); 58 | } 59 | 60 | 61 | /** 62 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::makeRoot 63 | * @expectedException BadMethodCallException 64 | */ 65 | public function testMakeRoot() 66 | { 67 | $this->wrappers[1]->makeRoot(3); 68 | } 69 | 70 | 71 | /** 72 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getChildren 73 | */ 74 | public function testGetChildren() 75 | { 76 | $c = $this->wrappers[1]->getChildren(); 77 | $this->assertEquals( 78 | array($this->nodes[2]->getId(), $this->nodes[3]->getId()), 79 | array_map(function($node) {return $node->getNode()->getId();}, $c) 80 | ); 81 | 82 | $c = $this->wrappers[0]->getChildren(); 83 | $this->assertEquals( 84 | array($this->nodes[1]->getId(), $this->nodes[4]->getId()), 85 | array_map(function($node) {return $node->getNode()->getId();}, $c) 86 | ); 87 | 88 | $this->assertEmpty($this->wrappers[2]->getChildren(), 'no children'); 89 | } 90 | 91 | 92 | /** 93 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getDescendants 94 | * @covers DoctrineExtensions\NestedSet\Manager::filterNodeDepth 95 | */ 96 | public function testGetDescendants() 97 | { 98 | $d = $this->wrappers[1]->getDescendants(); 99 | $this->assertEquals( 100 | array($this->nodes[2]->getId(), $this->nodes[3]->getId()), 101 | array_map(function($node) {return $node->getNode()->getId();}, $d), 102 | '->getDescendants() depth=unlimited' 103 | ); 104 | 105 | $this->assertEmpty($this->wrappers[0]->getDescendants(0), '->getDescendants() depth=0'); 106 | 107 | $d = $this->wrappers[0]->getDescendants(1); 108 | $this->assertEquals( 109 | array($this->nodes[1]->getId(), $this->nodes[4]->getId()), 110 | array_map(function($node) {return $node->getNode()->getId();}, $d), 111 | '->getDescendants() depth=1' 112 | ); 113 | 114 | $d = $this->wrappers[0]->getDescendants(2); 115 | $this->assertEquals( 116 | array($this->nodes[1]->getId(), $this->nodes[2]->getId(), $this->nodes[3]->getId(), $this->nodes[4]->getId()), 117 | array_map(function($node) {return $node->getNode()->getId();}, $d), 118 | '->getDescendants() depth=2' 119 | ); 120 | } 121 | 122 | 123 | /** 124 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getSiblings 125 | */ 126 | public function testGetSiblings() 127 | { 128 | $siblings = $this->wrappers[2]->getSiblings(); 129 | $this->assertEquals( 130 | array($this->nodes[3]->getId()), 131 | array_map(function($node) {return $node->getNode()->getId();}, $siblings), 132 | '->getSiblings() excludes current node by default' 133 | ); 134 | 135 | $siblings = $this->wrappers[2]->getSiblings(true); 136 | $this->assertEquals( 137 | array($this->nodes[2]->getId(), $this->nodes[3]->getId()), 138 | array_map(function($node) {return $node->getNode()->getId();}, $siblings), 139 | '->getSiblings() includes current node' 140 | ); 141 | } 142 | 143 | 144 | /** 145 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getParent 146 | */ 147 | public function testGetParent() 148 | { 149 | $this->assertEquals($this->nodes[0]->getId(), $this->wrappers[1]->getParent()->getNode()->getId()); 150 | $this->assertNull($this->wrappers[0]->getParent()); 151 | 152 | $this->wrappers[3]->getAncestors(); 153 | $this->assertEquals($this->nodes[1]->getId(), $this->wrappers[3]->getParent()->getNode()->getId()); 154 | } 155 | 156 | 157 | /** 158 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::getAncestors 159 | */ 160 | public function testGetAncestors() 161 | { 162 | $a = $this->wrappers[3]->getAncestors(); 163 | $this->assertEquals( 164 | array($this->nodes[0]->getId(), $this->nodes[1]->getId()), 165 | array_map(function($node) {return $node->getNode()->getId();}, $a) 166 | ); 167 | 168 | $this->assertEmpty($this->wrappers[0]->getAncestors()); 169 | } 170 | 171 | 172 | /** 173 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsParentOf 174 | */ 175 | public function testInsertAsParentOf() 176 | { 177 | $newWrapper = $this->nsm->wrapNode(new SingleRootNodeMock(6, '1.1', 0, 0, 0)); 178 | 179 | $newWrapper->insertAsParentOf($this->wrappers[4]); 180 | $this->assertEquals(8, $newWrapper->getLeftValue(), '->insertAsParentOf() updates new node\'s left value'); 181 | $this->assertEquals(11, $newWrapper->getRightValue(), '->insertAsParentOf() updates new node\'s right value'); 182 | $this->assertEquals(null, $newWrapper->getRootValue(), '->insertAsParentOf() updates new node\'s root value'); 183 | $this->assertEquals(9, $this->wrappers[4]->getLeftValue(), '->insertAsParentOf() updates next node\'s left value'); 184 | $this->assertEquals(10, $this->wrappers[4]->getRightValue(), '->insertAsParentOf() updates next node\'s right value'); 185 | $this->assertEquals(12, $this->wrappers[0]->getRightValue(), '->insertAsParentOf() updates parent node\'s right value'); 186 | } 187 | 188 | 189 | /** 190 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertAsPrevSiblingOf 191 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 192 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::insertNode 193 | */ 194 | public function testInsertAsPrevSiblingOf() 195 | { 196 | $newNode = new NodeWrapper(new SingleRootNodeMock(21, '1.1.1(.5)'), $this->nsm); 197 | 198 | $newNode->insertAsPrevSiblingOf($this->wrappers[3]); 199 | $this->assertEquals(5, $newNode->getLeftValue(), '->insertAsPrevSiblingOf() updates new node\'s left value'); 200 | $this->assertEquals(6, $newNode->getRightValue(), '->insertAsPrevSiblingOf() updates new node\'s right value'); 201 | $this->assertEquals(3, $this->wrappers[2]->getLeftValue(), '->insertAsPrevSiblingOf updates prev node\'s left value'); 202 | $this->assertEquals(4, $this->wrappers[2]->getRightValue(), '->insertAsPrevSiblingOf updates prev node\'s right value'); 203 | $this->assertEquals(7, $this->wrappers[3]->getLeftValue(), '->insertAsPrevSiblingOf updates next node\'s left value'); 204 | $this->assertEquals(8, $this->wrappers[3]->getRightValue(), '->insertAsPrevSiblingOf updates next node\'s right value'); 205 | } 206 | 207 | 208 | /** 209 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::moveAsPrevSiblingOf 210 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::updateNode 211 | * @covers DoctrineExtensions\NestedSet\NodeWrapper::shiftRLRange 212 | */ 213 | public function testMoveAsPrevSiblingOf() 214 | { 215 | $this->wrappers[4]->moveAsPrevSiblingOf($this->wrappers[1]); 216 | $this->assertEquals(2, $this->wrappers[4]->getLeftValue(), '->moveAsPrevSiblingOf() updates moved node\'s left value'); 217 | $this->assertEquals(3, $this->wrappers[4]->getRightValue(), '->moveAsPrevSiblingOf() updates moved node\'s right value'); 218 | $this->assertEquals(4, $this->wrappers[1]->getLeftValue(), '->moveAsPrevSiblingOf() updates next node\'s left value'); 219 | $this->assertEquals(9, $this->wrappers[1]->getRightValue(), '->moveAsPrevSiblingOf() updates next node\'s right value'); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /tests/README.markdown: -------------------------------------------------------------------------------- 1 | Running the NestedSet Test Suite 2 | ================================ 3 | 4 | 5 | ## Install Test suite Dependencies 6 | 7 | ### PHPUnit 8 | 9 | PHPUnit 3.5.0 or later is required. As of writing, PHPUnit 3.5 is not stable. 10 | The easiest way to install it is via Git: 11 | 12 | $ git clone git://github.com/sebastianbergmann/phpunit.git 13 | $ cd phpunit 14 | $ pear package 15 | $ pear install PHPUnit-3.5.XXX.tgz 16 | 17 | 18 | ### Doctrine2 19 | 20 | The ORM, DBAL, and Common Doctrine2 components are required. The default 21 | location is 22 | 23 | > doctrine2-nestedset/vendor 24 | 25 | Use the `install_vendors.sh` script to install the necessary dependencies: 26 | 27 | $ cd doctrine2-nestedset/tests 28 | $ ./install_vendors.sh 29 | 30 | Alternatively, you can modify the `autoload.php` file for your Doctrine2 31 | installation directories. 32 | 33 | 34 | ### Sqlite 35 | 36 | Sqlite and the PHP bindings for sqlite are required. Consult your 37 | distribution's documentation for installation instructions. For example, on 38 | Ubuntu: 39 | 40 | $ sudo apitutude install sqlite3 php5-sqlite 41 | 42 | 43 | ## Run Tests 44 | 45 | To run all tests: 46 | 47 | $ cd doctrine2-nestedset/tests/ 48 | $ phpunit 49 | 50 | 51 | To generate code coverage (requires XDebug): 52 | 53 | $ cd doctrine2-nestedset/tests/ 54 | $ phpunit --coverage-html=cov/ 55 | 56 | Check the code coverage by opening `cov/index.html` page in a browser. 57 | -------------------------------------------------------------------------------- /tests/autoload.php: -------------------------------------------------------------------------------- 1 | register(); 7 | 8 | $loader = new Doctrine\Common\ClassLoader("Doctrine\\DBAL", __DIR__.'/../vendor/doctrine/lib/vendor/doctrine-dbal/lib'); 9 | $loader->register(); 10 | 11 | $loader = new Doctrine\Common\ClassLoader("Doctrine\\ORM", __DIR__.'/../vendor/doctrine/lib'); 12 | $loader->register(); 13 | 14 | $loader = new Doctrine\Common\ClassLoader("DoctrineExtensions\\NestedSet\\Tests", __DIR__); 15 | $loader->register(); 16 | 17 | $loader = new Doctrine\Common\ClassLoader("DoctrineExtensions\\NestedSet", __DIR__."/../lib"); 18 | $loader->register(); 19 | 20 | -------------------------------------------------------------------------------- /tests/install_vendors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | BASEDIR=`dirname $0`/.. 4 | 5 | # initialization 6 | mkdir -p $BASEDIR/vendor 7 | rm -rf $BASEDIR/vendor/* 8 | cd $BASEDIR/vendor 9 | 10 | # Doctrine 11 | git clone git://github.com/doctrine/doctrine2.git doctrine 12 | cd doctrine 13 | git submodule init 14 | git submodule update 15 | cd .. 16 | 17 | -------------------------------------------------------------------------------- /tests/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | DoctrineExtensions/NestedSet 14 | 15 | 16 | 17 | 18 | 19 | ../lib/DoctrineExtensions/NestedSet 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/update_vendors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | BASEDIR="`dirname $0`/.." 4 | 5 | # Doctrine 6 | cd $BASEDIR/vendor/doctrine 7 | git pull 8 | git submodule update 9 | --------------------------------------------------------------------------------