├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── autoload.php ├── composer.json ├── src ├── AutoloaderBootstrap.php ├── AutoloaderBootstrapInterface.php ├── ClassLoaderException.php ├── Discovery │ ├── PathFinderBase.php │ ├── PathFinderContrib.php │ ├── PathFinderCore.php │ ├── PathFinderInterface.php │ └── PathFinderNull.php ├── Loader.php ├── LoaderInterface.php ├── TokenResolver.php ├── TokenResolverFactory.php ├── TokenResolverFactoryInterface.php └── TokenResolverInterface.php └── tests ├── .coveralls.yml ├── .gitignore ├── composer.json ├── data ├── acme.inc └── docroot │ ├── file.inc │ ├── includes │ └── common.inc │ ├── index.php │ ├── misc │ └── drupal.js │ ├── modules │ └── field │ │ └── field.module │ └── sites │ └── all │ └── modules │ └── testmodule │ ├── composer.json │ └── testmodule.info ├── install.sh ├── phpunit.xml ├── run-coverage.sh ├── run-tests.sh └── src ├── AutoloaderBootstrapTest.php ├── Discovery ├── PathFinderBaseTest.php ├── PathFinderContribTest.php ├── PathFinderCoreTest.php └── PathFinderNullTest.php ├── LoaderTest.php ├── TokenResolverFactoryTest.php └── TokenResolverTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | 8 | matrix: 9 | fast_finish: true 10 | 11 | env: 12 | global: 13 | # add composer's global bin directory to the path 14 | # see: https://github.com/drush-ops/drush#install---composer 15 | - PATH="$PATH:$HOME/.composer/vendor/bin" 16 | 17 | script: 18 | - cd ./tests 19 | - ./run-tests.sh 20 | - ./run-coverage.sh 21 | - cd .. 22 | 23 | after_script: 24 | - cd ./tests 25 | - php vendor/bin/coveralls 26 | 27 | 28 | notifications: 29 | webhooks: 30 | urls: 31 | - https://webhooks.gitter.im/e/c2147c4ed13ddd031b30 32 | - https://webhooks.gitter.im/e/45a8fac3fb8fd2ae907d 33 | on_success: change 34 | on_failure: always 35 | on_start: false 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Coverage Status](https://coveralls.io/repos/mateu-aguilo-bosch/drupal-unit-autoload/badge.svg?branch=master&service=github)](https://coveralls.io/github/mateu-aguilo-bosch/drupal-unit-autoload?branch=master) [![Build Status](https://travis-ci.org/e0ipso/drupal-unit-autoload.svg?branch=master)](https://travis-ci.org/e0ipso/drupal-unit-autoload) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/e0ipso/drupal-unit-autoload/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/e0ipso/drupal-unit-autoload/?branch=master) 2 | 3 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/e0ipso/drupal-unit-autoload?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 4 | 5 | # Drupal Unit Autoload 6 | 7 | Have you ever wanted to add **PHPUnit** tests to your Drupal 7 module? Well, you should. This tool aims to help you to deal 8 | with autoloading the classes that you have in your Drupal installation. 9 | 10 | ## The Problem 11 | The main problem arises when the class -or code- that you are testing depends on classes declared in other modules, or 12 | Drupal core itself. 13 | 14 | Imagine that you are testing your `Car` class. That class depends on `\DrupalCacheInterface` (you are using a mock cache 15 | provider that **has** to implement that interface), and also depends on several classes from the service container 16 | module. After all you are injecting services in your `Car` class to be able to mock them afterwards, and that may 17 | require to have the `Drupal\service_container\` available to you during tests. 18 | 19 | Since you are doing unit testing, you may not want to bootstrap Drupal to have the database available in order to be 20 | able to check in the registry to find all those classes. 21 | 22 | At this point you can think _I will just use Composer's autoloader and define where to find those classes and 23 | namespaces_. This is when you realize that Drupal allows you to install contrib modules in **many** locations. That 24 | makes it impossible to ship your module with the relative paths that you need. 25 | 26 | Imagine this possibility: 27 | - Our custom module (the one that will have unit testing) is installed in `sites/example.org/modules/contrib/racing_modules/car`. 28 | - The modules that the _car_ module depends on are installed in: 29 | - `sites/all/modules/essentials/service_container`. 30 | - `sites/default/modules/contrib/dependency`. 31 | 32 | It seems that if you wanted to provide the path to `includes/cache.inc` to make `\DrupalCacheInterface` available, then 33 | you would need to add a path like: `../../../../../../includes/cache.inc`. But what if someone decides to install your 34 | `car` module in `sites/all/modules/car`? That path you provided in the module will not work in that situation. The 35 | correct one would be `../../../includes/cache.inc`. Basically every site installation may need a different path. 36 | 37 | The problem that this project aims to solve is to give you a way to provide a single path in your code that will work in 38 | all those scenarios. 39 | 40 | ## The Solution 41 | Meet the Drupal Unit Autoload. To include it, just add the following to your PHPUnit test class (change the path 42 | depending on the location of your test classes): 43 | 44 | ```php 45 | require_once __DIR__ . '/../../vendor/e0ipso/drupal-unit-autoload/autoload.php'; 46 | ``` 47 | 48 | That will load Composer's autoloader + the Drupal capabilities. 49 | 50 | The only thing that you need to do is add a new `composer.json` key with tokens in the path. 51 | 52 | Inside the folder where you have your unit tests you will need to have a `composer.json` file that has: 53 | 54 | ```js 55 | { 56 | "require-dev": { 57 | "phpunit/phpunit": "4.7.*", 58 | "mockery/mockery": "0.9.*", 59 | "e0ipso/drupal-unit-autoload": "1.0.*" 60 | }, 61 | "autoload": { 62 | "psr-0": { 63 | … This is usual Composer business … 64 | }, 65 | "psr-4": { 66 | … This is usual Composer business … 67 | "Drupal\\service_container\\": "DRUPAL_CONTRIB/src", 68 | "Drupal\\Core\\": [ 69 | "DRUPAL_CONTRIB/lib/Core", 70 | "DRUPAL_CONTRIB/src/DrupalCore" 71 | ] 72 | }, 73 | "class-location": { 74 | "\\DrupalCacheInterface": "DRUPAL_ROOT/includes/cache.inc", 75 | "\\ServiceContainer": "DRUPAL_CONTRIB/lib/ServiceContainer.php", 76 | "\\Drupal": "DRUPAL_CONTRIB/lib/Drupal.php" 77 | } 78 | } 79 | } 80 | ``` 81 | 82 | Running `composer install` on that folder will download PHPUnit, Mockery -and all of the tools that you use for your 83 | tests-. Additionally it will download this project, that is what `"e0ipso/drupal-unit-autoload": "0.1.*"` is 84 | for. 85 | 86 | At this point you only need is add the paths with `DRUPAL_ROOT` or `DRUPAL_CONTRIB` in your composer file. 87 | 88 | You have two options: 89 | - Provide class names and the files where they are found using the new key inside of `autoload` called `class-location`. 90 | - Provide psr-4 and psr-0 namespace prefixes and the path where they are mapped. This is very simmilar to what 91 | composer does, but with the magic that finds where the Drupal root is and where the contrib modules are installed. 92 | 93 | In the paths that you provide, you will be able to include two tokens: `DRUPAL_ROOT` and `DRUPAL_CONTRIB`. 94 | Those tokens will be expanded to the real paths that they represent. This way, providing `DRUPAL_CONTRIB` can end up expanding in: 95 | - `/var/www/docroot/sites/all/modules/ctools` in one Drupal installation. 96 | - `/User/Sites/drupal-site/sites/default/modules/contrib/ctools` in another installation. 97 | 98 | The important thing to note is that your code ships with the same _tokenized_ path for everyone, without caring about 99 | where the dependencies are installed. 100 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | register(); 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e0ipso/drupal-unit-autoload", 3 | "description": "Allows you to load classes in non standard unknown file locations.", 4 | "minimum-stability": "stable", 5 | "license": "GPL-2.0", 6 | "authors": [ 7 | { 8 | "name": "Mateu Aguiló Bosch", 9 | "email": "mateu.aguilo.bosch@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.4.0" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Drupal\\Composer\\ClassLoader\\": "src" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/AutoloaderBootstrap.php: -------------------------------------------------------------------------------- 1 | classLoader = $classLoader; 64 | $this->seed = $seed; 65 | $this->loader = $loader ?: new Loader($seed); 66 | $this->tokenFactory = $token_factory ?: new TokenResolverFactory(); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function register() { 73 | if ($this->checkLoadedAutoloader()) { 74 | return; 75 | } 76 | // Parse the composer.json. 77 | $composer_config = $this->getConfig(); 78 | $this->registerDrupalPaths($composer_config); 79 | $this->registerPsr($composer_config); 80 | } 81 | 82 | /** 83 | * Registers the autoloader. 84 | */ 85 | protected function load() { 86 | spl_autoload_register(array($this->loader, static::AUTOLOAD_METHOD)); 87 | } 88 | 89 | /** 90 | * Unregisters the autoloader. 91 | */ 92 | protected function unload() { 93 | spl_autoload_unregister(array($this->loader, static::AUTOLOAD_METHOD)); 94 | } 95 | 96 | /** 97 | * Register the path based autoloader. 98 | * 99 | * @param object $composer_config 100 | * The Composer configuration. 101 | */ 102 | protected function registerDrupalPaths($composer_config) { 103 | if (empty($composer_config['class-location'])) { 104 | return; 105 | } 106 | $this->loader->setClassMap((array) $composer_config['class-location']); 107 | $this->load(); 108 | } 109 | 110 | /** 111 | * Use Composer's autoloader to register the PRS-0 and PSR-4 paths. 112 | * 113 | * @param array $composer_config 114 | * The Composer configuration. 115 | */ 116 | protected function registerPsr(array $composer_config) { 117 | $psr0 = $psr4 = array(); 118 | if (!empty($composer_config['psr-0'])) { 119 | $psr0 = (array) $composer_config['psr-0']; 120 | } 121 | if (!empty($composer_config['psr-4'])) { 122 | $psr4 = (array) $composer_config['psr-4']; 123 | } 124 | if (empty($psr4) && empty($psr0)) { 125 | return; 126 | } 127 | $this->loader->setPsrClassMap(array( 128 | 'psr-0' => $psr0, 129 | 'psr-4' => $psr4, 130 | )); 131 | $this->loader->registerPsr($this->classLoader); 132 | } 133 | 134 | /** 135 | * {@inheritdoc} 136 | */ 137 | public function checkLoadedAutoloader() { 138 | $functions = spl_autoload_functions(); 139 | return in_array(array($this->loader, static::AUTOLOAD_METHOD), $functions); 140 | } 141 | 142 | /** 143 | * {@inheritdoc} 144 | */ 145 | public function getConfig() { 146 | // Initialize empty configuration. 147 | $config = [ 148 | 'psr-0' => [], 149 | 'psr-4' => [], 150 | 'class-location' => [], 151 | ]; 152 | 153 | // Find the tokenized paths. 154 | $psrs = array( 155 | 'psr-0' => $this->classLoader->getPrefixes(), 156 | 'psr-4' => $this->classLoader->getPrefixesPsr4(), 157 | ); 158 | // Get all the PSR-0 and PSR-0 and detect the ones that have Drupal tokens. 159 | foreach ($psrs as $psr_type => $namespaces) { 160 | $namespaces = $namespaces ?: []; 161 | foreach ($namespaces as $prefix => $paths) { 162 | $token_paths = array(); 163 | if (!is_array($paths)) { 164 | $paths = array($paths); 165 | } 166 | foreach ($paths as $path) { 167 | $token_resolver = $this->tokenFactory->factory($path); 168 | if (!$token_resolver->hasToken()) { 169 | continue; 170 | } 171 | $path = $token_resolver->trimPath(); 172 | $token_paths[] = $path; 173 | } 174 | // If there were paths, add them to the config. 175 | if (!empty($token_paths)) { 176 | $config[$psr_type][$prefix] = $token_paths; 177 | } 178 | } 179 | } 180 | 181 | // Get the drupal path configuration. 182 | $composer_config = json_decode(file_get_contents(static::COMPOSER_CONFIGURATION_NAME)); 183 | $config['class-location'] = array(); 184 | if (isset($composer_config->autoload->{'class-location'})) { 185 | $config['class-location'] = array_merge($config['class-location'], (array) $composer_config->autoload->{'class-location'}); 186 | } 187 | if (isset($composer_config->{'autoload-dev'}->{'class-location'})) { 188 | $config['class-location'] = array_merge($config['class-location'], (array) $composer_config->{'autoload-dev'}->{'class-location'}); 189 | } 190 | 191 | return $config; 192 | } 193 | 194 | /** 195 | * {@inheritdoc} 196 | */ 197 | public function getClassLoader() { 198 | return $this->classLoader; 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /src/AutoloaderBootstrapInterface.php: -------------------------------------------------------------------------------- 1 | path = $options[0]; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function requireFile($seed) { 35 | // Get the real path to the file. 36 | $real_path = $this->find($seed); 37 | require_once $real_path; 38 | } 39 | 40 | /** 41 | * Cleans a directory path by removing /. from the end. 42 | * 43 | * @param string $dir_path 44 | * The path name to clean. 45 | * 46 | * @return string 47 | * The clean path name. 48 | */ 49 | protected function cleanDirPath($dir_path) { 50 | // Remove annoying /. at the end. This is needed because the 51 | // DirectoryIterator adds that to the end of the dir name. 52 | $dir_path = rtrim($dir_path, '.'); 53 | $dir_path = rtrim($dir_path, DIRECTORY_SEPARATOR); 54 | return $dir_path; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/Discovery/PathFinderContrib.php: -------------------------------------------------------------------------------- 1 | path = $options[0]; 40 | $this->moduleName = $options[1]; 41 | $this->coreFinder = $core_finder ?: new PathFinderCore(array('')); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function find($seed) { 48 | // To find contrib, we need to know where core is first. Pass the empty 49 | // string to as the path so we get the path for core itself -and not a path 50 | // relative to the core install-. 51 | $core_path = $this->coreFinder->find($seed); 52 | 53 | // Create the RecursiveDirectoryIterator on the core directory. 54 | $core_directory = new \RecursiveDirectoryIterator($core_path . '/sites', \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS); 55 | // Create an iterator that will go recursively through all the files and 56 | // directories -because of SELF_FIRST-. 57 | $files_iterator = new \RecursiveIteratorIterator($core_directory, \RecursiveIteratorIterator::SELF_FIRST); 58 | // Iterate over all of the directories under the sites directory. 59 | foreach ($files_iterator as $path_name => $dir) { 60 | /** @var $dir \SplFileInfo */ 61 | if (!$dir->isDir()) { 62 | // Do not scan all files, just directories. 63 | continue; 64 | } 65 | // Check if the current directory corresponds to the contrib we are 66 | // looking for. 67 | if ($this->isWantedContrib($dir)) { 68 | return $this->cleanDirPath($dir->getPathName()) . $this->path; 69 | } 70 | } 71 | throw new ClassLoaderException(sprintf('Drupal module "%s" could not be found in the Drupal tree that contains: %s.', $this->moduleName, $seed)); 72 | } 73 | 74 | /** 75 | * Checks if the passed directory is the contrib module we are looking for. 76 | * 77 | * @param \SplFileInfo $dir 78 | * The info object about the directory. 79 | * @return bool 80 | * TRUE if the contrib is detected. FALSE otherwise. 81 | */ 82 | protected function isWantedContrib(\SplFileInfo $dir) { 83 | $info_file = $this->cleanDirPath($dir->getPathName()) . DIRECTORY_SEPARATOR . $this->moduleName . '.info'; 84 | return file_exists($info_file); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/Discovery/PathFinderCore.php: -------------------------------------------------------------------------------- 1 | isDrupalRoot($directory)) { 25 | return $directory . $this->path; 26 | } 27 | } 28 | while ($directory = $this->getParentDirectory($directory)); 29 | // @codeCoverageIgnoreStart 30 | // If we have not returned, that means that the Drupal core directory could 31 | // not be found. 32 | throw new ClassLoaderException(sprintf('Drupal core directory could not be found as a parent of: %s.', $seed)); 33 | // @codeCoverageIgnoreEnd 34 | } 35 | 36 | /** 37 | * Checks if the passed directory is the Drupal root. 38 | * 39 | * @param string $directory 40 | * The directory path. 41 | * 42 | * @return bool 43 | * TRUE if the passed directory is the Drupal root. 44 | */ 45 | protected function isDrupalRoot($directory) { 46 | if (!empty($directory) && is_dir($directory) && file_exists($directory . DIRECTORY_SEPARATOR . '/index.php')) { 47 | // Drupal 7 root. 48 | // We check for the presence of 'modules/field/field.module' to differentiate this from a D6 site 49 | return (file_exists($directory . DIRECTORY_SEPARATOR . 'includes/common.inc') 50 | && file_exists($directory . DIRECTORY_SEPARATOR . 'misc/drupal.js') 51 | && file_exists($directory . DIRECTORY_SEPARATOR . 'modules/field/field.module')); 52 | } 53 | return FALSE; 54 | } 55 | 56 | /** 57 | * Gets the parent directory iterator. 58 | * 59 | * @param string $directory 60 | * The current directory path. 61 | * 62 | * @throws ClassLoaderException 63 | * If no parent directory could be found. 64 | * 65 | * @return string 66 | * The parent directory. 67 | */ 68 | protected function getParentDirectory($directory) { 69 | // Get the parent directory. 70 | if ($directory === realpath('/')) { 71 | throw new ClassLoaderException(sprintf('Could not find the parent directory of "%s".', $directory)); 72 | } 73 | return dirname($directory); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/Discovery/PathFinderInterface.php: -------------------------------------------------------------------------------- 1 | path; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/Loader.php: -------------------------------------------------------------------------------- 1 | seed = $seed; 60 | $this->tokenResolverFactory = $token_resolver_factory ?: new TokenResolverFactory(); 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function autoload($class) { 67 | return $this->autoloadPaths($class); 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function setClassMap(array $class_map) { 74 | // Remove the leading \ from the class names. 75 | $unprefixed_class_map = array(); 76 | foreach ($class_map as $class_name => $tokenized_path) { 77 | $unprefixed_class_map[static::unprefixClass($class_name)] = $tokenized_path; 78 | } 79 | $this->classMap = $unprefixed_class_map; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function setPsrClassMap(array $class_map) { 86 | // Remove the leading \ from the partial namespaces. 87 | $unprefixed_class_map = array(); 88 | foreach ($class_map as $psr => $psr_class_map) { 89 | $unprefixed_class_map[$psr] = array(); 90 | foreach ($psr_class_map as $class_name => $tokenized_path) { 91 | $unprefixed_class_map[$psr][static::unprefixClass($class_name)] = $tokenized_path; 92 | } 93 | } 94 | $this->psrClassMap = $unprefixed_class_map; 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | public function setSeed($seed) { 101 | $this->seed = $seed; 102 | } 103 | 104 | /** 105 | * Strips a class name of a preceding backslash if necessary. 106 | * 107 | * @param string $class 108 | * The class to prefix. 109 | * 110 | * @return string 111 | * The prefixed class. 112 | */ 113 | protected static function unprefixClass($class) { 114 | if (strpos($class, '\\') !== 0) { 115 | return $class; 116 | } 117 | return substr($class, 1); 118 | } 119 | 120 | /** 121 | * Helper function to autoload path based files. 122 | * 123 | * @param string $class 124 | * The requested class. 125 | * 126 | * @return bool 127 | * TRUE if the class was found. FALSE otherwise. 128 | */ 129 | protected function autoloadPaths($class) { 130 | $class = static::unprefixClass($class); 131 | // If the class that PHP is trying to find is not in the class map, built 132 | // from the composer configuration, then bail. 133 | if (!in_array($class, array_keys($this->classMap))) { 134 | return FALSE; 135 | } 136 | try { 137 | $resolver = $this->tokenResolverFactory->factory($this->classMap[$class]); 138 | $finder = $resolver->resolve(); 139 | // Have the path finder require the file and return TRUE or FALSE if it 140 | // found the file or not. 141 | $finder->requireFile($this->seed); 142 | return TRUE; 143 | } 144 | catch (ClassLoaderException $e) { 145 | // If there was an error, inform PHP that the class could not be found. 146 | return FALSE; 147 | } 148 | } 149 | 150 | /** 151 | * {@inheritdoc} 152 | */ 153 | public function registerPsr(\Composer\Autoload\ClassLoader $loader) { 154 | // Composer's autoloader uses a different method to add each PSR partial 155 | // namespace. 156 | $psrs = array( 157 | 'psr-0' => 'add', 158 | 'psr-4' => 'addPsr4', 159 | ); 160 | foreach ($psrs as $psr => $loader_method) { 161 | // The $psrClassMap contains an array for psr-0 and an array for psr-4. 162 | // Each one of those contains an array where the keys are the partial 163 | // namespace, and the value is an array of tokenized paths where those 164 | // partial namespaces can be found. 165 | // Ex: 166 | // [ 167 | // 'psr-0' => [ 168 | // [ 'Drupal\\plug\\' => ['DRUPAL_CONTRIB/lib', …] ], 169 | // ], 170 | // ] 171 | foreach ($this->psrClassMap[$psr] as $partial_namespace => $tokenized_paths) { 172 | if (!is_array($tokenized_paths)) { 173 | // If a string was passed, then convert it to an array for 174 | // consistency. 175 | $tokenized_paths = array($tokenized_paths); 176 | } 177 | foreach ($tokenized_paths as $tokenized_path) { 178 | try { 179 | // Find the real path for the tokenized one. 180 | $resolver = $this->tokenResolverFactory->factory($tokenized_path); 181 | $finder = $resolver->resolve(); 182 | // Get the real path of the prefix. 183 | $real_path = $finder->find($this->seed); 184 | $loader->{$loader_method}($partial_namespace, $real_path); 185 | } 186 | catch (ClassLoaderException $e) {} 187 | } 188 | } 189 | } 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /src/LoaderInterface.php: -------------------------------------------------------------------------------- 1 | 'Drupal\Composer\ClassLoader\Discovery\PathFinderCore', 29 | 'DRUPAL_CONTRIB' => 'Drupal\Composer\ClassLoader\Discovery\PathFinderContrib', 30 | ); 31 | 32 | /** 33 | * Constructs a TokenResolver object. 34 | * 35 | * @param string $tokenized_path 36 | * The path containing a potential token. 37 | */ 38 | public function __construct($tokenized_path = '') { 39 | $this->path = $tokenized_path; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function resolve() { 46 | // If the path is not tokenized, then return the NULL discovery object. 47 | if (file_exists($this->path)) { 48 | return new Discovery\PathFinderNull(array($this->path)); 49 | } 50 | if (!$this->getToken()) { 51 | return NULL; 52 | } 53 | // Get the name of the class of the PathFinder depending on if we are 54 | // looking for Drupal root or contrib path. 55 | $class_name = $this->getClassName(); 56 | // Remove the leading token from the tokenized path to get the relative path 57 | // (to the Drupal root or the module/theme path). 58 | $arguments[] = $this->cleanToken(); 59 | // Add more arguments to the constructor, like the module name. 60 | $arguments = array_merge($arguments, $this->parseArguments()); 61 | return new $class_name($arguments); 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function hasToken() { 68 | return (bool) $this->getToken(); 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public function trimPath() { 75 | if (!$token = $this->getToken()) { 76 | return $this->path; 77 | } 78 | $pos = strpos($this->path, $token); 79 | return substr($this->path, $pos); 80 | } 81 | 82 | /** 83 | * Removes the token from the tokenized path. 84 | * 85 | * @throws ClassLoaderException 86 | * If no token can be found. 87 | * 88 | * @return string 89 | * The cleaned path. 90 | */ 91 | protected function cleanToken() { 92 | if ($token_name = $this->getToken()) { 93 | // Remove the token and arguments and return the path. 94 | $path = substr($this->path, strlen($token_name)); 95 | return preg_replace('/<.*>/', '', $path); 96 | } 97 | $message = sprintf('No token could be found in "%s". Available tokens are: %s.', $this->path, implode(', ', array_keys($this->supportedTokens))); 98 | throw new ClassLoaderException($message); 99 | } 100 | 101 | /** 102 | * Checks if the current tokenized path contains a known token. 103 | * 104 | * @return string 105 | * The token found. NULL otherwise. 106 | */ 107 | protected function getToken() { 108 | // Iterate over the supported tokens to find the token in the tokenized 109 | // path. 110 | foreach (array_keys($this->supportedTokens) as $token_name) { 111 | if (strpos($this->path, $token_name) !== FALSE) { 112 | return $token_name; 113 | } 114 | } 115 | return NULL; 116 | } 117 | 118 | /** 119 | * Gets the class name corresponding to the token. 120 | * 121 | * @return string 122 | * The class name. 123 | */ 124 | protected function getClassName() { 125 | $token_name = $this->getToken(); 126 | return $token_name ? $this->supportedTokens[$token_name] : NULL; 127 | } 128 | 129 | /** 130 | * Gets the arguments in the token. 131 | * 132 | * @return string[] 133 | * A numeric array containing the token arguments. 134 | */ 135 | protected function parseArguments() { 136 | $token_name = $this->getToken(); 137 | $delimiter = '/'; 138 | $matches = array(); 139 | if (preg_match($delimiter . preg_quote($token_name) . '<(.+)>.*' . $delimiter, $this->path, $matches)) { 140 | // Some arguments were found. 141 | return explode(',', $matches[1]); 142 | } 143 | return array(); 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/TokenResolverFactory.php: -------------------------------------------------------------------------------- 1 | /src/", 12 | "": "DRUPAL_ROOT/includes" 13 | }, 14 | "psr-0": { 15 | "": "DRUPAL_ROOT/includes" 16 | }, 17 | "class-location": { 18 | "\\Tmp": "DRUPAL_ROOT/file.inc", 19 | "\\Tmp2": "data/acme.inc" 20 | } 21 | }, 22 | "autoload-dev": { 23 | "psr-0": { 24 | "Drupal\\MyModule\\": "DRUPAL_CONTRIB/lib/" 25 | }, 26 | "class-location": { 27 | "\\Tmp3": "data/acme.inc" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/data/acme.inc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./src/ 7 | 8 | 9 | 10 | 11 | 12 | ./vendor 13 | 14 | 15 | ../src 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/run-coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # @file 3 | # Simple script to load composer and run the tests. 4 | 5 | set -e 6 | 7 | DIR=$(dirname $0) 8 | cd $DIR 9 | test -f "./vendor/bin/phpunit" || ./install.sh 10 | ./vendor/bin/phpunit --coverage-html ./report 11 | -------------------------------------------------------------------------------- /tests/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # @file 3 | # Simple script to load composer and run the tests. 4 | 5 | set -e 6 | 7 | DIR=$(dirname $0) 8 | cd $DIR 9 | test -f "./vendor/bin/phpunit" || ./install.sh 10 | ./vendor/bin/phpunit "$@" 11 | -------------------------------------------------------------------------------- /tests/src/AutoloaderBootstrapTest.php: -------------------------------------------------------------------------------- 1 | setAccessible(TRUE); 29 | $value = $reflection_property->getValue($autoloader); 30 | $this->assertEquals($loader, $value); 31 | $reflection_property = new \ReflectionProperty(get_class($autoloader), 'seed'); 32 | $reflection_property->setAccessible(TRUE); 33 | $value = $reflection_property->getValue($autoloader); 34 | $this->assertEquals('composer.json', $value); 35 | } 36 | 37 | /** 38 | * Tests the ::register() method. 39 | * 40 | * @covers ::register() 41 | * @covers ::load() 42 | * @covers ::registerDrupalPaths() 43 | * @covers ::registerPsr() 44 | */ 45 | public function test_register() { 46 | $loader = m::mock('\Composer\Autoload\ClassLoader'); 47 | $loader 48 | ->shouldReceive('add') 49 | ->once(); 50 | $loader 51 | ->shouldReceive('addPsr4') 52 | ->once(); 53 | $loader 54 | ->shouldReceive('getPrefixes') 55 | ->once() 56 | ->andReturn([ 57 | '' => 'DRUPAL_ROOT/includes', 58 | ]); 59 | $loader 60 | ->shouldReceive('getPrefixesPsr4') 61 | ->once() 62 | ->andReturn([ 63 | 'Drupal\\Composer\\ClassLoader\\' => '../src/', 64 | 'Drupal\\Composer\\ClassLoader\\Tests\\' => 'src/', 65 | '' => 'DRUPAL_ROOT/includes', 66 | ]); 67 | $autoloader = new AutoloaderBootstrap($loader, 'data/docroot/sites/all/modules/testmodule/composer.json'); 68 | $autoloader->register(); 69 | 70 | $this->assertTrue($autoloader->checkLoadedAutoloader()); 71 | // Make sure that calling to register a second time does not fail. 72 | $autoloader->register(); 73 | $this->assertTrue($autoloader->checkLoadedAutoloader()); 74 | } 75 | 76 | /** 77 | * Tests the ::checkLoadedAutoloader() method. 78 | */ 79 | public function test_checkLoadedAutoloader() { 80 | $class_loader = m::mock('\Composer\Autoload\ClassLoader'); 81 | $loader = m::mock('\Drupal\Composer\ClassLoader\LoaderInterface'); 82 | $loader 83 | ->shouldReceive(AutoloaderBootstrap::AUTOLOAD_METHOD); 84 | $autoloader = new AutoloaderBootstrap($class_loader, 'data/docroot/sites/all/modules/testmodule/composer.json', $loader); 85 | $this->assertFalse($autoloader->checkLoadedAutoloader()); 86 | spl_autoload_register(array($loader, AutoloaderBootstrap::AUTOLOAD_METHOD)); 87 | $this->assertTrue($autoloader->checkLoadedAutoloader()); 88 | } 89 | 90 | /** 91 | * Tests the protected ::unload() method. 92 | * 93 | * @covers ::unload() 94 | */ 95 | public function test_unload() { 96 | $loader = m::mock('\Composer\Autoload\ClassLoader'); 97 | $autoloader = new AutoloaderBootstrap($loader, 'data/docroot/sites/all/modules/testmodule/composer.json'); 98 | 99 | // First load it. 100 | $reflection_method = new \ReflectionMethod(get_class($autoloader), 'load'); 101 | $reflection_method->setAccessible(TRUE); 102 | $reflection_method->invoke($autoloader); 103 | 104 | // Make sure it's added 105 | $this->assertTrue($autoloader->checkLoadedAutoloader()); 106 | 107 | // Then unload it. 108 | $reflection_method = new \ReflectionMethod(get_class($autoloader), 'unload'); 109 | $reflection_method->setAccessible(TRUE); 110 | $reflection_method->invoke($autoloader); 111 | 112 | // Make sure it's not added 113 | $this->assertFalse($autoloader->checkLoadedAutoloader()); 114 | } 115 | 116 | /** 117 | * Tests the ::registerDrupalPaths() method. 118 | * 119 | * @covers ::registerDrupalPaths() 120 | */ 121 | public function test_registerDrupalPaths_empty() { 122 | $loader = m::mock('\Composer\Autoload\ClassLoader'); 123 | $autoloader = new AutoloaderBootstrap($loader, 'data/docroot/sites/all/modules/testmodule/composer.json'); 124 | $reflection_method = new \ReflectionMethod(get_class($autoloader), 'registerDrupalPaths'); 125 | $reflection_method->setAccessible(TRUE); 126 | $value = $reflection_method->invokeArgs($autoloader, [[]]); 127 | $this->assertNull($value); 128 | } 129 | 130 | /** 131 | * Tests the ::registerPsr() method. 132 | * 133 | * @covers ::registerPsr() 134 | */ 135 | public function test_registerPsr_empty() { 136 | $loader = m::mock('\Composer\Autoload\ClassLoader'); 137 | $autoloader = new AutoloaderBootstrap($loader, 'data/docroot/sites/all/modules/testmodule/composer.json'); 138 | $reflection_method = new \ReflectionMethod(get_class($autoloader), 'registerPsr'); 139 | $reflection_method->setAccessible(TRUE); 140 | $value = $reflection_method->invokeArgs($autoloader, [[]]); 141 | $this->assertNull($value); 142 | } 143 | 144 | /** 145 | * Tests the ::getConfig method. 146 | * 147 | * @covers ::getConfig() 148 | */ 149 | public function test_getConfig() { 150 | $loader = m::mock('\Composer\Autoload\ClassLoader'); 151 | $loader 152 | ->shouldReceive('getPrefixes') 153 | ->once() 154 | ->andReturn([ 155 | '' => '/lorem/ipsum/DRUPAL_ROOT/includes', 156 | ]); 157 | $loader 158 | ->shouldReceive('getPrefixesPsr4') 159 | ->once() 160 | ->andReturn([ 161 | 'Drupal\\Composer\\ClassLoader\\' => '/lorem/ipsum/../src/', 162 | 'Drupal\\Composer\\ClassLoader\\Tests\\' => '/lorem/ipsum/src/', 163 | 'Drupal\\MyModule\\' => '/lorem/ipsum/DRUPAL_CONTRIB/src/', 164 | '' => '/lorem/ipsum/DRUPAL_ROOT/includes', 165 | ]); 166 | $autoloader = new AutoloaderBootstrap($loader, 'data/docroot/sites/all/modules/testmodule/composer.json'); 167 | $config = $autoloader->getConfig(); 168 | $expected = [ 169 | 'psr-0' => [ 170 | '' => ['DRUPAL_ROOT/includes'], 171 | ], 172 | 'psr-4' => [ 173 | 'Drupal\\MyModule\\' => ['DRUPAL_CONTRIB/src/'], 174 | '' => ['DRUPAL_ROOT/includes'], 175 | ], 176 | 'class-location' => [ 177 | '\\Tmp' => 'DRUPAL_ROOT/file.inc', 178 | '\\Tmp2' => 'data/acme.inc', 179 | '\\Tmp3' => 'data/acme.inc', 180 | ], 181 | ]; 182 | $this->assertEquals($expected, $config); 183 | } 184 | 185 | /** 186 | * Tests the ::getClassLoader method. 187 | * 188 | * @covers ::getClassLoader() 189 | */ 190 | public function test_getClassLoader() { 191 | $loader = m::mock('\Composer\Autoload\ClassLoader'); 192 | $autoloader = new AutoloaderBootstrap($loader, 'data/docroot/sites/all/modules/testmodule/composer.json'); 193 | $this->assertEquals($loader, $autoloader->getClassLoader()); 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /tests/src/Discovery/PathFinderBaseTest.php: -------------------------------------------------------------------------------- 1 | setAccessible(true); 30 | $value = $property->getValue($pathFinder); 31 | $this->assertEquals('./testFolder/', $value); 32 | } 33 | 34 | /** 35 | * Tests that PathFinderBase::requireFile() works properly. 36 | * 37 | * @covers ::requireFile() 38 | */ 39 | public function testRequireFile() { 40 | $pathFinder = new PathFinderNull(['data/acme.inc']); 41 | $pathFinder->requireFile('data/acme.inc'); 42 | $included = get_included_files(); 43 | $this->assertTrue(in_array(realpath('data/acme.inc'), $included)); 44 | } 45 | 46 | /** 47 | * Tests that PathFinderBase::cleanDirPath() works properly. 48 | * @dataProvider cleanDirPathProvider 49 | * 50 | * @covers ::cleanDirPath() 51 | */ 52 | public function testCleanDirPath($given, $expected) { 53 | $pathFinder = new PathFinderNull(['']); 54 | 55 | $class = new \ReflectionClass('\Drupal\Composer\ClassLoader\Discovery\PathFinderBase'); 56 | $method = $class->getMethod('cleanDirPath'); 57 | $method->setAccessible(true); 58 | $output = $method->invokeArgs($pathFinder, [$given]); 59 | 60 | $this->assertEquals($expected, $output); 61 | } 62 | 63 | /** 64 | * Provider for testCleanDirPath. 65 | */ 66 | public static function cleanDirPathProvider() { 67 | return array( 68 | array('./testFolder/.', './testFolder'), 69 | array('./testFolder', './testFolder'), 70 | array('./deep/testFolder/.', './deep/testFolder'), 71 | array('testFolder/.', 'testFolder'), 72 | array('deep/testFolder/.', 'deep/testFolder'), 73 | ); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /tests/src/Discovery/PathFinderContribTest.php: -------------------------------------------------------------------------------- 1 | setAccessible(true); 31 | $value = $property->getValue($pathFinder); 32 | $this->assertEquals('./testFolder/', $value); 33 | 34 | // Assert module name is set. 35 | $property = new \ReflectionProperty($pathFinder, 'moduleName'); 36 | $property->setAccessible(true); 37 | $value = $property->getValue($pathFinder); 38 | $this->assertEquals('mymodule', $value); 39 | 40 | // Assert path finder is set. 41 | $property = new \ReflectionProperty($pathFinder, 'coreFinder'); 42 | $property->setAccessible(true); 43 | $value = $property->getValue($pathFinder); 44 | $this->assertInstanceOf('\Drupal\Composer\ClassLoader\Discovery\PathFinderInterface', $value); 45 | } 46 | 47 | /** 48 | * Tests that find() works properly. 49 | * 50 | * @covers ::find() 51 | */ 52 | public function testFind() { 53 | // 1. Test successful path. 54 | $pathFinder = new PathFinderContrib(['', 'testmodule']); 55 | $path = $pathFinder->find('data/docroot/sites/all/modules/testmodule/composer.json'); 56 | $this->assertEquals(realpath('data/docroot/sites/all/modules/testmodule'), $path); 57 | } 58 | 59 | /** 60 | * Tests that ::find() works properly. 61 | * 62 | * @expectedException \Drupal\Composer\ClassLoader\ClassLoaderException 63 | * 64 | * @covers ::find() 65 | */ 66 | public function test_find__noDrupal() { 67 | // 2. Test seed not in Drupal root. 68 | $pathFinder = new PathFinderContrib(['data/acme.inc', 'testmodule']); 69 | $pathFinder->find(__DIR__); 70 | } 71 | 72 | /** 73 | * Tests that ::find() works properly. 74 | * 75 | * @expectedException \Drupal\Composer\ClassLoader\ClassLoaderException 76 | * 77 | * @covers ::find() 78 | */ 79 | public function test_find__noContrib() { 80 | // 3. Test seed not in Drupal contrib. 81 | $coreFinder = m::mock('\Drupal\Composer\ClassLoader\Discovery\PathFinderCore'); 82 | $coreFinder->shouldReceive('find')->andReturn('data/docroot/')->once(); 83 | $pathFinder = new PathFinderContrib(['data/docroot/sites/all/modules/testmodule/composer.json', 'testmodule2'], $coreFinder); 84 | $pathFinder->find(__DIR__); 85 | } 86 | 87 | /** 88 | * Tests that ::isWantedContrib() works properly. 89 | * 90 | * @covers ::isWantedContrib() 91 | */ 92 | public function test_isWantedContrib() { 93 | $pathFinder = new PathFinderContrib(['', 'testmodule']); 94 | $dir = new \SplFileInfo('data/docroot/sites/all/modules/testmodule'); 95 | 96 | $reflection_object = new \ReflectionObject($pathFinder); 97 | $method = $reflection_object->getMethod('isWantedContrib'); 98 | $method->setAccessible(true); 99 | $output = $method->invokeArgs($pathFinder, [$dir]); 100 | 101 | $this->assertTrue($output); 102 | } 103 | 104 | } 105 | 106 | -------------------------------------------------------------------------------- /tests/src/Discovery/PathFinderCoreTest.php: -------------------------------------------------------------------------------- 1 | find('data/docroot/sites/all/modules/testmodule/composer.json'); 31 | 32 | $this->assertEquals(realpath('data/docroot/file.inc'), $found); 33 | } 34 | 35 | /** 36 | * Tests that ::find() works properly. 37 | * 38 | * @expectedException \Drupal\Composer\ClassLoader\ClassLoaderException 39 | * 40 | * @covers ::getParentDirectory() 41 | */ 42 | public function test_getParentDirectory__noParent() { 43 | // 2. Test seed not in Drupal root. 44 | $pathFinder = new PathFinderCore(['data/acme.inc']); 45 | $pathFinder->find('file.inc'); 46 | } 47 | 48 | /** 49 | * Tests that ::find() works properly. 50 | * 51 | * @expectedException \Drupal\Composer\ClassLoader\ClassLoaderException 52 | * 53 | * @covers ::getParentDirectory() 54 | */ 55 | public function test_getParentDirectory__root() { 56 | // 3. Test seed in Drupal root throws exception. 57 | $pathFinder = new PathFinderCore(['/']); 58 | $class = new \ReflectionClass('\Drupal\Composer\ClassLoader\Discovery\PathFinderCore'); 59 | $method = $class->getMethod('getParentDirectory'); 60 | $method->setAccessible(true); 61 | $output = $method->invokeArgs($pathFinder, ['/']); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /tests/src/Discovery/PathFinderNullTest.php: -------------------------------------------------------------------------------- 1 | find(NULL); 29 | $this->assertEquals('data/acme.inc', $path); 30 | } 31 | 32 | } 33 | 34 | -------------------------------------------------------------------------------- /tests/src/LoaderTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('requireFile') 31 | ->once(); 32 | // Mock a resolver object. 33 | $resolver = m::mock('\Drupal\Composer\ClassLoader\TokenResolverInterface'); 34 | $resolver 35 | ->shouldReceive('resolve') 36 | ->once() 37 | ->andReturn($finder); 38 | // Mock a resolver factory object. 39 | $resolver_factory = m::mock('\Drupal\Composer\ClassLoader\TokenResolverFactoryInterface'); 40 | $resolver_factory 41 | ->shouldReceive('factory') 42 | ->once() 43 | ->andReturn($resolver); 44 | 45 | $loader = new Loader(__DIR__, $resolver_factory); 46 | $loader->setClassMap([ 47 | '\\Acme' => './data/acme.inc', 48 | ]); 49 | $this->assertTrue($loader->autoload('Acme')); 50 | } 51 | 52 | /** 53 | * Tests that Loader::autoload() works properly. 54 | * 55 | * @covers ::autoload() 56 | * @covers ::autoloadPaths() 57 | */ 58 | public function test_autoload_unexisting() { 59 | $loader = new Loader(__DIR__); 60 | $loader->setClassMap([ 61 | '\\Acme' => './data/acme.inc', 62 | ]); 63 | $this->assertFalse($loader->autoload('Invalid')); 64 | } 65 | 66 | /** 67 | * Tests that Loader::autoload() works properly. 68 | * 69 | * @covers ::autoload() 70 | * @covers ::autoloadPaths() 71 | */ 72 | public function test_autoload_finderException() { 73 | $loader = new Loader(__DIR__); 74 | $loader->setClassMap([ 75 | '\\Acme' => 'DRUPAL_ROOT/file.inc', 76 | ]); 77 | $this->assertFalse($loader->autoload('Acme')); 78 | } 79 | 80 | /** 81 | * Tests that Loader::setClassMap works properly. 82 | * 83 | * @dataProvider setClassMapProvider 84 | * 85 | * @covers ::setClassMap() 86 | */ 87 | public function test_setClassMap($given, $expected) { 88 | $loader = new Loader(__DIR__); 89 | $loader->setClassMap($given); 90 | $reflection_property = new \ReflectionProperty('\Drupal\Composer\ClassLoader\Loader', 'classMap'); 91 | $reflection_property->setAccessible(TRUE); 92 | $value = $reflection_property->getValue($loader); 93 | $this->assertEquals($expected, $value); 94 | } 95 | 96 | /** 97 | * Provider for test_setClassMap. 98 | */ 99 | public static function setClassMapProvider() { 100 | return [ 101 | [['\\Foo' => 'bar'], ['Foo' => 'bar']], 102 | [['Foo' => 'bar'], ['Foo' => 'bar']], 103 | ]; 104 | } 105 | 106 | /** 107 | * Tests that Loader::setPsrClassMap works properly. 108 | * 109 | * @dataProvider setPsrClassMapProvider 110 | * 111 | * @covers ::setPsrClassMap() 112 | */ 113 | public function test_setPsrClassMap($given, $expected) { 114 | $loader = new Loader(__DIR__); 115 | $loader->setPsrClassMap($given); 116 | $reflection_property = new \ReflectionProperty('\Drupal\Composer\ClassLoader\Loader', 'psrClassMap'); 117 | $reflection_property->setAccessible(TRUE); 118 | $value = $reflection_property->getValue($loader); 119 | $this->assertEquals($expected, $value); 120 | } 121 | 122 | /** 123 | * Provider for test_setClassMap. 124 | */ 125 | public static function setPsrClassMapProvider() { 126 | return [ 127 | [ 128 | ['psr-4' => ['\\Foo' => 'bar'], 'psr-0' => ['\\Baz' => 'oof']], 129 | ['psr-4' => ['Foo' => 'bar'], 'psr-0' => ['Baz' => 'oof']] 130 | ], 131 | [ 132 | ['psr-4' => ['Foo' => 'bar'], 'psr-0' => ['Baz' => 'oof']], 133 | ['psr-4' => ['Foo' => 'bar'], 'psr-0' => ['Baz' => 'oof']] 134 | ], 135 | ]; 136 | } 137 | 138 | /** 139 | * Tests that Loader::setSeed works properly. 140 | * 141 | * @covers ::setSeed() 142 | */ 143 | public function test_setSeed() { 144 | $loader = new Loader('Lorem ipsum'); 145 | $seed = 'Dolor sit'; 146 | $loader->setSeed($seed); 147 | $reflection_property = new \ReflectionProperty('\Drupal\Composer\ClassLoader\Loader', 'seed'); 148 | $reflection_property->setAccessible(TRUE); 149 | $value = $reflection_property->getValue($loader); 150 | $this->assertEquals($seed, $value); 151 | } 152 | 153 | /** 154 | * Tests that Loader::registerPsr works properly. 155 | * 156 | * @covers ::registerPsr() 157 | */ 158 | public function test_registerPsr() { 159 | $loader = new Loader('data/docroot/sites/all/modules/testmodule/composer.json'); 160 | // Mock the \Composer\Autoload\ClassLoader loader. 161 | $classLoader = m::mock('\Composer\Autoload\ClassLoader'); 162 | $classLoader 163 | ->shouldReceive('add') 164 | ->twice(); 165 | $classLoader 166 | ->shouldReceive('addPsr4') 167 | ->once(); 168 | 169 | $psrClassMap = [ 170 | 'psr-0' => [ 171 | 'Drupal\\plug\\' => [ 172 | 'DRUPAL_CONTRIB/testmodule.info', 173 | 'DRUPAL_ROOT/file.inc' 174 | ], 175 | ], 176 | 'psr-4' => [ 177 | 'Drupal\\Kitten\\' => 'DRUPAL_ROOT/file.inc', 178 | ], 179 | ]; 180 | $loader->setPsrClassMap($psrClassMap); 181 | $loader->registerPsr($classLoader); 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /tests/src/TokenResolverFactoryTest.php: -------------------------------------------------------------------------------- 1 | factory('Lorem'); 28 | $this->assertInstanceOf('\Drupal\Composer\ClassLoader\TokenResolverInterface', $resolver); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /tests/src/TokenResolverTest.php: -------------------------------------------------------------------------------- 1 | setAccessible(TRUE); 29 | $value = $reflection_property->getValue($resolver); 30 | $this->assertEquals($tokenized_path, $value); 31 | } 32 | 33 | /** 34 | * Tests that ::resolve() is working. 35 | * 36 | * @dataProvider resolveProvider 37 | * 38 | * @covers ::resolve() 39 | * @covers ::cleanToken() 40 | * @covers ::getToken() 41 | * @covers ::getClassName() 42 | * @covers ::parseArguments() 43 | */ 44 | public function test_resolve($path, $expected) { 45 | $resolver = new TokenResolver($path); 46 | $finder = $resolver->resolve(); 47 | $this->assertInstanceOf($expected, $finder); 48 | } 49 | 50 | /** 51 | * Provider method for test_resolve. 52 | */ 53 | public static function resolveProvider() { 54 | return [ 55 | // 1. Path not tokenized. 56 | ['data/acme.inc', '\Drupal\Composer\ClassLoader\Discovery\PathFinderNull'], 57 | // 2. Core path. 58 | ['DRUPAL_ROOT/file.inc', '\Drupal\Composer\ClassLoader\Discovery\PathFinderCore'], 59 | // 3. Contrib path. 60 | ['DRUPAL_CONTRIB/testmodule.info', '\Drupal\Composer\ClassLoader\Discovery\PathFinderContrib'], 61 | ]; 62 | } 63 | 64 | /** 65 | * Tests that ::resolve() is working. 66 | * 67 | * @covers ::resolve() 68 | * @covers ::cleanToken() 69 | * @covers ::getToken() 70 | * @covers ::getClassName() 71 | * @covers ::parseArguments() 72 | */ 73 | public function test_resolve_unexisting() { 74 | // 4. Unexisting path without a real token. 75 | $resolver = new TokenResolver('Lorem'); 76 | $finder = $resolver->resolve(); 77 | $this->assertNull($finder); 78 | } 79 | 80 | /** 81 | * Tests that the ::cleanToken is working. 82 | * 83 | * @expectedException \Drupal\Composer\ClassLoader\ClassLoaderException 84 | * 85 | * @covers ::cleanToken() 86 | */ 87 | public function test_cleanToken() { 88 | $resolver = new TokenResolver(''); 89 | $reflection_method = new \ReflectionMethod(get_class($resolver), 'cleanToken'); 90 | $reflection_method->setAccessible(TRUE); 91 | $reflection_method->invoke($resolver); 92 | } 93 | 94 | /** 95 | * Tests that the ::cleanToken is working. 96 | * 97 | * @dataProvider hasTokenProvider 98 | * 99 | * @covers ::hasToken() 100 | */ 101 | public function test_hasToken($path, $expected) { 102 | $resolver = new TokenResolver($path); 103 | $this->assertEquals($resolver->hasToken(), $expected); 104 | } 105 | 106 | /** 107 | * Provider method for test_hasToken. 108 | */ 109 | public function hasTokenProvider() { 110 | return [ 111 | ['DRUPAL_ROOT/includes', TRUE], 112 | ['DRUPAL_CONTRIB/src/', TRUE], 113 | ['/lorem/ipsum/DRUPAL_ROOT/src/', TRUE], 114 | ['/lorem/ipsum/DRUPAL_CONTRIB/src/', TRUE], 115 | ['src', FALSE], 116 | ['/lorem/ipsum/src', FALSE], 117 | ]; 118 | } 119 | 120 | /** 121 | * Tests that the ::cleanToken is working. 122 | * 123 | * @dataProvider trimPathProvider 124 | * 125 | * @covers ::trimPath() 126 | */ 127 | public function test_trimPath($path, $expected) { 128 | $resolver = new TokenResolver($path); 129 | $this->assertEquals($resolver->trimPath(), $expected); 130 | } 131 | 132 | /** 133 | * Provider method for test_trimPath. 134 | */ 135 | public function trimPathProvider() { 136 | return [ 137 | ['DRUPAL_ROOT/includes', 'DRUPAL_ROOT/includes'], 138 | ['DRUPAL_CONTRIB/src/', 'DRUPAL_CONTRIB/src/'], 139 | ['/lorem/ipsum/DRUPAL_ROOT/includes/', 'DRUPAL_ROOT/includes/'], 140 | ['/lorem/ipsum/DRUPAL_CONTRIB/src/', 'DRUPAL_CONTRIB/src/'], 141 | ['src', 'src'], 142 | [NULL, NULL], 143 | ]; 144 | } 145 | 146 | } 147 | --------------------------------------------------------------------------------