├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── commonjs.php ├── composer.json ├── phpunit.xml.dist ├── plugins └── commonsjs-plugin.json.php ├── src └── CommonJS │ └── CommonJSProvider.php └── tests ├── CommonJSTest.php ├── alt-module-dir ├── a-module.php ├── alt-direct-export.php ├── alt-module-consumer.php └── alt-relative-module-consumer.php ├── bootstrap.php └── module-dir ├── a-module.php ├── classes ├── foo.php └── package │ ├── foo-factory.php │ ├── foo-subclass.php │ └── foo.php ├── custom-plugins ├── commonjs-plugin.file-reverser.php └── commonjs-plugin.incrementer.php ├── direct-export.php ├── folder-as-module └── index.php ├── incrementer-module.php ├── module-id-and-uri-exporter.php ├── module-with-another-ext.inc ├── multiple-exports.php ├── package1 └── relative-upper-module-consumer.php ├── relative-module-consumer.php ├── relative-module.php ├── resolution-module.php └── resources ├── data.json └── simple-text.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /phpunit.xml 2 | 3 | /php.iml 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.3 4 | - 5.4 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) Olivier Philippon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CommonJS for PHP 2 | 3 | A simple CommonJS spec implementation for PHP 5.3+. 4 | 5 | [![build status](https://secure.travis-ci.org/DrBenton/CommonJSForPHP.png)](http://travis-ci.org/DrBenton/CommonJSForPHP) 6 | 7 | It fits in a single PHP file (≈ 150 lines of effective code) and allows a simple and easy application structure, based on 8 | the CommonJS "Module" design pattern. You might already know this pattern if you have ever worked with 9 | [Node.js](http://nodejs.org/) (server-side Javascript) or [RequireJS](http://requirejs.org/) (client-side Javascript). 10 | 11 | From [JavaScript Growing Up](http://fr.slideshare.net/davidpadbury/javascript-growing-up): 12 | > CommonJS introduced a simple API for dealing with modules: 13 | 14 | > * "require" for importing a module. 15 | > * "exports" for exposing stuff from a module 16 | > 17 | 18 | 19 | This PHP implementation also supports two features inspired by RequireJS: 20 | * _defined-by-Closures_ Modules, with the [$define](#define) function 21 | * resources [Plugins](#plugins) (a simple "JSON decoder" is bundled as a sample). 22 | 23 | It comes with a "Folder as Modules" feature too, inspired by Node.js. 24 | 25 | ## Why CommonJS for PHP ? 26 | 27 | * CommonJS "Module" pattern is simple and efficient ; it lets you quickly create easily understandable and flexible code. 28 | * Between 2 beautiful projects based on Symfony, Zend Framework, Slim, Silex or whatever modern 29 | [PSR-0](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md) heavy Object Oriented framework, have 30 | some rest with simple "good ol' procedural" PHP codestyle! 31 | * Feel comfortable in PHP when you're back from a Node.js or front-end [AMD](https://github.com/amdjs/amdjs-api/wiki/AMD) project. 32 | * CommonJS Module pattern acts as a very simple Service Locator and lazy-loaded dependencies resolver. 33 | * Have fun with isolated PHP code parts! Every Module runs in an automatically generated Closure, and you can freely create 34 | variables and Closures within your Modules without without fearing a pollution of the PHP global space, nor collisions with 35 | other Modules code. 36 | * All your Modules code run in a "Closure sandbox", and Modules communicate between each other only through their 37 | ```$require()``` function and ```$exports``` variable. 38 | * Every Module content is run only once - the first time it is required. 39 | * CommonJS for PHP is perfectly interoperable with PSR-0 classes. You can use Symfony 2 or Zend Framework or yet other 40 | components in your Modules. It can be used with libraries managed by Composer as well. 41 | * This CommonJS implementation for PHP can be used as a micro-framework for quick little projects... 42 | * ...But it can used for large serious projects too ; thousands of Node.js and AMD developers use this CommonJS 43 | Module pattern everyday. 44 | 45 | The code is willfully 100% procedural and Closures-based - this way, this CommonJS spec implementation code looks 46 | like Javascript code :-) 47 | 48 | ## Synopsis 49 | 50 | ```php 51 | // ******************************* file "index.php" 52 | // CommonJs setup 53 | $commonJS = include './commonjs.php'; 54 | $define = $commonJS['define']; 55 | $require = $commonJS['require']; 56 | $commonJS['config']['basePath'] = __DIR__ '/modules'; 57 | 58 | // Custom plugin? 59 | $commonJS['plugins']['yaml'] = __DIR__ . '/commonsjs-plugin.yaml.php'; 60 | 61 | // Modules are files ; but you can define "modules-as-Closures" too 62 | $define('logger', function($require) { 63 | return function($msg) { 64 | syslog(LOG_DEBUG, $msg); 65 | }; 66 | }); 67 | 68 | // Boostrap module trigger! 69 | $require('app/bootstrap'); 70 | 71 | 72 | // ******************************* file "modules/app/bootstrap.php" 73 | /** 74 | * Note that "$define", "$require", "$exports" and "$module" 75 | * are automatically globally defined in the Module! 76 | */ 77 | $config = $require('yaml!../config/config.yml');//we use the YAML plugin with a relative path 78 | $logger = $require('logger'); 79 | $requestBridge = $require('../vendor/symfony-bridge/request'); 80 | $router = $require('app/router'); 81 | 82 | $request = $requestBridge->createFromGlobals(); 83 | list($targetControllerModule, $targetAction) = $router->resolveRequest($request); 84 | $config['debug'] && $logger('** $targetControllerModule='.$targetControllerModule); 85 | $require($targetControllerModule)->$targetAction(); 86 | ``` 87 | 88 | ## API 89 | 90 | ### CommonJS environment initialization 91 | 92 | To initialize the CommonJS environment you simply have to do this: 93 | 94 | ```php 95 | $commonJS = include './commonjs.php'; 96 | ``` 97 | 98 | The ```$commonJS``` returned associative Array contains the following keys : 99 | * **define**: the CommonJS ```define()``` Closure ; outside Modules, you can alias it with ```$define = $commonJS['define'];``` 100 | * **require**: the CommonJS ```require()``` Closure ; outside Modules, you can alias it with ```$require = $commonJS['require'];``` 101 | * **config**: a simple config associative Array with 2 keys: 102 | * **basePath**: the base path of your Modules. Every Module you ```require()``` without a relative path will be located 103 | in this directory path. Default is the _commonjs.php_'s ```__DIR__``` 104 | * **modulesExt**: the extension to add to the requested Modules path. Default: _'.php'_ 105 | * **folderAsModuleFileName**: the file name for "folders as Modules". Default: _'index.php'_ 106 | * **autoNamespacing**: set this boolean to ```true``` to have all your Modules PHP code automatically wrapped in unique 107 | namespaces at runtime. See [Classes](#classes) section for more details. Default: _false_ 108 | * **plugins**: this associative Array is the CommonJS for PHP "a la RequireJS" plugins registry. Keys are plugin prefixes, 109 | values are paths to plugins files. See [Plugins](#plugins) section for more details. 110 | 111 | Note that you can also use an array for **config['basePath']** : this allow you to define multiple modules root paths : 112 | 113 | ```php 114 | $commonJS = include './commonjs.php'; 115 | $commonJS['config']['basePath'] = array( 116 | __DIR__.'/app/modules', 117 | __DIR__.'/vendor/symfony-bridge/modules', 118 | ); 119 | ``` 120 | 121 | If you use Composer, a best CommonJS environment initialization can be used: 122 | 123 | ```php 124 | $commonJS = \CommonJS\CommonJSProvider::getInstance(); 125 | ``` 126 | 127 | Make sure that you first added CommonJS in your ```composer.json``` file : 128 | 129 | ``` 130 | { 131 | "minimum-stability": "dev", 132 | "require": { 133 | "commonjs/commonjs": "1.0.*" 134 | } 135 | } 136 | ``` 137 | 138 | You can share a single instance of a CommonJS environment, but you may use multiple CommonJS environments 139 | if you need to. 140 | Since these environments are very light-weight library instances, you can do this without worrying about performance. 141 | 142 | ```php 143 | $commonJS = \CommonJS\CommonJSProvider::getInstance(); 144 | 145 | // ... in another file : 146 | $commonJS = \CommonJS\CommonJSProvider::getInstance(); 147 | // --> will return the same shared CommonJS environment 148 | 149 | // ... in yet another file : 150 | $commonJS = \CommonJS\CommonJSProvider::getInstance('default'); 151 | // --> will also return the same shared CommonJS environment, as 'default' is the default CommonJS instance id 152 | 153 | // ... in a last file : 154 | $commonJS = \CommonJS\CommonJSProvider::getInstance('my-provider-instance'); 155 | // --> will return a fresh new CommonJS environment, with it own basePath, plugins and Modules 156 | ``` 157 | 158 | ### require() 159 | 160 | Triggers the resolution of a Module. All Modules resolutions are triggered only once, the first time they are requested. 161 | All subsequent calls to this Module will return the same value, retrieved from an internal data cache. 162 | 163 | They are 4 types of Module resolutions: 164 | * Modules mapped to a Closure through the ```define()``` function. When the required module path matches a previously defined 165 | Module path, the Closure is triggered and we fetch its returned value. 166 | * Modules mapped to files. This is the most common Module type. The module path is resolved, and the matching PHP 167 | file is triggered in a CommonJS environment. 168 | * The module path resolution follows this rule: if the module path begins with **./** or **../**, the PHP file path will 169 | be resolved relatively to the current Module path. Otherwise, the Module path is just appended to the CommonJS 170 | config "basePath" path. 171 | * Don't use the ".php" file extension in your required modules paths. It will be automatically appended to the resolved 172 | file path. 173 | * Modules mapped to folders. It is very close to the previous "Modules mapped to files" behaviour, excepted that you only 174 | have to use a folder path as the ```$require```-ed module file path. If the folder contains a "index.php" file, this file 175 | will be used as the file Module. 176 | * Modules mapped to plugins. When a module path contains a prefix followed by an exclamation mark, it is considered as a 177 | plugin call. The part before the "!" is the plugin name, and the part after the "!" is the resource name. 178 | See [Plugins](#plugins) section for more detail. 179 | 180 | ```php 181 | // All Module types: 182 | 183 | // Closure-mapped Module: 184 | $define('config', function() { return array('debug' => true, 'appPath' => __DIR__); }); 185 | $config = $require('config'); 186 | 187 | // Absolute Module file resolution: (absolute, but relative to the CommonJS "config['basePath']" path) 188 | // --> will trigger the "app/logger.php" Module file code 189 | $logger = $require('app/logger'); 190 | 191 | // Relative Module file resolution: (relative to the Module which calls "$require()") 192 | // --> will trigger the "../mailer.php" Module file code 193 | $logger = $require('../mailer'); 194 | 195 | // Folder as Module resolution: (works with absolute or relative paths) 196 | // --> will trigger the "symfony-bridge/request/index.php" Module file code 197 | $logger = $require('symfony-bridge/request'); 198 | 199 | // Plugin call: 200 | $myModuleConfig = $require('json!./module-config.json'); 201 | ``` 202 | 203 | ### Modules scope 204 | 205 | This is the heart of the CommonJS coolness! Every Module is isolated from others Modules, and interact with them only 206 | through its ```$require()``` method (for input) and its ```$exports``` array (for output). 207 | 208 | In a Module you can create vars and Closures without fearing a pollution of the PHP global space, nor collisions with 209 | other Modules code. Every time a Module is triggered, its code is automatically embedded is a generated PHP Closure. 210 | 211 | In this "Closure sandbox", your Module have automatically access to the following vars: **(and only to them)** 212 | * **$require**: the ```$require``` function. Let's you access to other Modules exports. 213 | * **$define**: the ```$define``` function. You can dynamically create new "mapped to Closures" Modules definitions in your Modules. 214 | * **$exports**: this is an empty Array. Add key/values couples to this Array, and they're will be automatically available 215 | in other Modules. 216 | * **$module**: is mainly used for direct Modules export value. If you want to export a single value from a Module, 217 | use ```$module['exports'] = $mySingleExportedValue;```. 218 | Additionally, you have access to ```$module['id']``` and 219 | ```$module['uri']``` properties, according to [CommonJS spec](http://wiki.commonjs.org/wiki/Modules/1.1.1). 220 | You also have access to ```$module['resolve']``` and ```$module['moduleExists']``` functions. The first one 221 | returns a resolved full module path or ```null``` ; the second one returns a boolean, and allows you to test whether a module 222 | is defined or not. 223 | ```uri``` is the [realpath](php.net/manual/fr/function.realpath.php) of the Module file, 224 | and ```id``` is the absolute Module path of the Module : 225 | 226 | > The "id" property must be such that require(module.id) will return the exports object from which the module.id originated 227 | > (That is to say module.id can be passed to another module, and requiring that must return the original module). 228 | 229 | ### define() 230 | 231 | The ```define()``` function lets you create Modules resolved by Closures. The first param is the path of the Module 232 | you define, and the second one is a Closure. The first time this Module path is ```$require```-ed, this Closure is triggered 233 | and its return value is used a the Module value resolution. Subsequent calls will return the same value, managed by 234 | an internal data cache. 235 | 236 | ```php 237 | $define('config', function() { 238 | return array('debug' => true, 'appPath' => __DIR__); 239 | }); 240 | 241 | // PHP 5.4 (needs function array dereferencing) 242 | echo $require('config')['debug'];//--> 'true' 243 | // PHP 5.3 244 | $config = $require('config') 245 | echo $config['debug'];//--> 'true' 246 | ``` 247 | 248 | The triggered Closure can accept up to 3 injected params: ```$require```, ```$exports``` and ```$module```. 249 | ```$require``` let you require other modules from your Closure, while ```$exports``` and ```$module``` allows you 250 | to define the Module value resolution in a "CommonJS way": 251 | 252 | ```php 253 | $define('app/logger', function($require, &$exports, &$module) { 254 | $config = $require('config'); 255 | $logger = new Monolog\Logger('app'); 256 | if ($config['debug']) { 257 | $logger->pushHandler(new Monolog\Handler\StreamHandler($config['appPath'] . 'logs/app.log')); 258 | } 259 | $module['exports'] = $logger; 260 | }); 261 | 262 | $logger = $require('app/logger'); 263 | ``` 264 | 265 | If you want to use that form instead of a simple ```return```, be aware that because call-time pass by reference 266 | has been removed in PHP 5.4, you have to use ```&$exports``` and ```&$module``` and not just just 267 | ```$exports / $module``` in your definition Closure params. 268 | Although it is possible to omit the "&" in PHP 5.3, it's better to think about the future :-) 269 | 270 | ### Plugins 271 | 272 | CommonJS for PHP is bundled with a minimalist ["a la RequireJS" plugin system](http://requirejs.org/docs/plugins.html). 273 | A plugin is defined by a unique name and a resource path. They are triggered when the ```$require()``` function parameter is 274 | a path containing an exclamation mark. The part before the "!" is the plugin name, and the part after the "!" is the 275 | resource path: ```[plugin name]![resource path]```. 276 | 277 | Like Modules, plugins are triggered in a generated Closure, and run in their own scope. This scope only contains the 278 | ```$require``` and ```$resourcePath``` variables. With ```$require()``` you can have access to other Modules and plugins, 279 | while ```$resourcePath``` is the part of the required Module path after the "!". The resource path is resolved in the 280 | same way than Modules: they can be relative to the Module which triggers the plugin or absolute. 281 | 282 | Like ```$require```-ed Modules (which are triggered only once), plugins already triggered with the same resolved resource 283 | path will return a cached result value for subsequent calls. 284 | 285 | ```php 286 | // A YAML sample plugin 287 | 288 | // ******************************* file "app/plugins/commonsjs-plugin.yaml.php" 289 | $yamlParser = new \Symfony\Component\Yaml\Parser(); 290 | return $yamlParser->parse(file_get_contents($resourcePath)); 291 | 292 | 293 | // ******************************* file "app/bootstrap.php" 294 | $commonJS['plugins']['yaml'] = __DIR__ . '/app/plugins/commonsjs-plugin.yaml.php'; 295 | 296 | 297 | // ******************************* file "app/config.php" 298 | $config = $require('yaml!./resources/config.yml'); 299 | ``` 300 | 301 | ### Classes 302 | 303 | You can declare PHP classes in your CommonJS Modules, but as PHP doesn't support nested classes or namespaces defined at runtime, 304 | you have to be careful and not create same classes names in different Modules. 305 | 306 | So, the cleaner solution is to use Composer or another class loading system to handle your classes. 307 | Modules and classes declarations are kept separated : you declare your classes in some location, like you usually do in 308 | your PHP projects, and you use them in your Modules. 309 | 310 | If you really want to declare classes in your PHP "à la CommonJS" Modules, you have two options : 311 | 312 | * you can choose to handle classes collisions prevention yourself. You can use hardcoded classes names prefixes or 313 | namespaces to ensure your classes names don't overlap each other, like you usually do in PHP. 314 | * but you can also use the "autoNamespacing" config setting of this COmmonJS implementation. If you set it to ```true```, all your 315 | Modules will be automatically enclosed in dynamic namespaces, created at runtime. 316 | 317 | Be aware, though, that the implementation of this dynamic classes names isolation in CommonJS Modules has to rely on 318 | ```namespace``` and ```eval()```. 319 | Yes, ```eval()``` is used, and this is absolutely EVIL, but I have not been able to find another way of properly 320 | isolating classes in Modules :-) 321 | The fact is that PHP classes are global constants, thus you can't define a ```Foo``` class in a Module file and another 322 | ```Foo``` class in another file. This doesn't fit the CommonJS way, since you should be able to declare a class in a 323 | Module without having to care about conflicting classes names and without having to hardcode namespaces in your Module. 324 | But because PHP is not JavaScript, we have to use such a trick if we want to be able to declare classes in our Modules 325 | without having to manually handle classes names uniqueness. 326 | 327 | The only trick to declare and export classes is to prefix their name with the magic constant ```__NAMESPACE__``` in your 328 | Module exports, or to use a simple Factory: 329 | 330 | ```php 331 | // file 'lib/mailing/test/mailer.php' 332 | class Mailer 333 | { 334 | public function sendEmail() { 335 | $logger->log("test email sent!");//the email is not sent 336 | } 337 | } 338 | 339 | $module['exports'] = __NAMESPACE__.'\\Mailer'; 340 | 341 | // file 'lib/mailing/prod/mailer.php' 342 | class Mailer 343 | { 344 | public function sendEmail() { 345 | $mailerService->sendEmail();//the email is really sent 346 | } 347 | } 348 | 349 | $module['exports'] = __NAMESPACE__.'\\Mailer'; 350 | 351 | // file 'lib/mailing/dev/mailer.php' 352 | class Mailer 353 | { 354 | public function sendEmail() { 355 | $mailerService->sendEmail();//the email is really sent 356 | } 357 | } 358 | 359 | // Instead of having to deal with the __NAMESPACE__,you can also use a Factory! 360 | $exports['getInstance'] = function () 361 | { 362 | return new Mailer(); 363 | }; 364 | 365 | // file 'app/subscription/confirm.php' 366 | $mailerClass = $require('lib/mailing/prod/mailer'); 367 | $mailerInstance = new $mailerClass(); // the full class name contains a dynamic namespace, but you don't have to deal with this 368 | $mailerInstance->sendEmail(); 369 | 370 | ``` 371 | 372 | In this sample, two "Mailer" classes are declared : in a normal PHP world, this should trigger an error, 373 | but with this CommonJS implementation the Module content is automatically namespaced at runtime, allowing 374 | you to declare classes without having to worry about classes names collision. 375 | 376 | Of course, they are some limitations with this system. Since PHP doesn't allow dynamic inheritance 377 | (i.e. ```class SubClass extends $className```), you can't inherit a class without using its hardcoded dynamic namespace. 378 | 379 | These namespaces naming scheme is : "\CommonJS\Module\[Module ID]", where the Module ID slashes are replaced with backslashes and 380 | special chars replaced with underscores. 381 | For example, a ```UserCheck``` class declared in a "lib/services/user-check" Module will have this full class name: 382 | _\CommonJS\Module\lib\services\user_check\UserCheck_ 383 | 384 | You can look at the "tests/module-dir/classes/package/foo-subclass.php" PHP unit test class to see a sample of this 385 | dynamic namespacing hardcoded usage. 386 | 387 | 388 | ## More info 389 | 390 | As the source code of this "CommonJS for PHP" library is a lot shorter than this README, you can 391 | [have a look at it](https://github.com/DrBenton/CommonJSForPHP/blob/master/commonjs.php) 392 | for further information :-) 393 | 394 | You can also look at [Unit Tests](https://github.com/DrBenton/CommonJSForPHP/blob/master/tests/CommonJSTest.php), 395 | since they cover a rather large scope of use cases. 396 | 397 | ## License 398 | (The MIT License) 399 | 400 | Copyright (c) 2012 Olivier Philippon 401 | 402 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation 403 | files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, 404 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 405 | Software is furnished to do so, subject to the following conditions: 406 | 407 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 408 | 409 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 410 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 411 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 412 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 413 | -------------------------------------------------------------------------------- /commonjs.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | // Our API is entirely embedded in this "a la Javascript" self executing function 12 | // This way, we do not create any single global variable or function 13 | return call_user_func(function() 14 | { 15 | 16 | $_definitionsRegistry = array(); 17 | $_modulesRegistry = array(); 18 | 19 | $config = array( 20 | // Default config 21 | 'basePath' => __DIR__, 22 | 'modulesExt' => '.php', 23 | 'folderAsModuleFileName' => 'index.php', 24 | 'autoNamespacing' => false, 25 | ); 26 | $plugins = array( 27 | // Default plugins 28 | 'json' => __DIR__ . '/plugins/commonsjs-plugin.json.php' 29 | ); 30 | 31 | $_currentResolvedModuleDir = null; 32 | 33 | $_getResourceFullPath = function ($modulePath, $fileExtToAdd = '') use (&$config, &$_searchResource, &$_currentResolvedModuleDir) 34 | { 35 | static $absolutePathsResolutionsCache = array(); 36 | 37 | $isRelativePath = ('./' === substr($modulePath, 0, 2) || '../' === substr($modulePath, 0, 3)); 38 | 39 | $pathCacheId = $modulePath . $fileExtToAdd; 40 | if (!$isRelativePath && isset($absolutePathsResolutionsCache[$pathCacheId])) { 41 | 42 | return $absolutePathsResolutionsCache[$pathCacheId]; 43 | } 44 | 45 | $basePaths = is_array($config['basePath']) ? $config['basePath'] : array($config['basePath']); 46 | 47 | if (null === $_currentResolvedModuleDir) { 48 | //defaults to $config['basePath'][0] if we are not already in a Module context 49 | $_currentResolvedModuleDir = $basePaths[0]; 50 | } 51 | 52 | // Relative or absolute path? 53 | if ($isRelativePath) { 54 | // Relative path 55 | $fullModulePath = $_currentResolvedModuleDir . DIRECTORY_SEPARATOR . $modulePath; 56 | $resolvedModulePath = $_searchResource($fullModulePath, $fileExtToAdd); 57 | } else { 58 | // Absolute path search from $basePaths 59 | foreach ($basePaths as $currentBasePath) { 60 | $fullModulePath = $currentBasePath . DIRECTORY_SEPARATOR . $modulePath; 61 | $resolvedModulePath = $_searchResource($fullModulePath, $fileExtToAdd); 62 | if (null !== $resolvedModulePath) { 63 | break; 64 | } 65 | } 66 | } 67 | 68 | $absolutePathsResolutionsCache[$pathCacheId] = $resolvedModulePath; 69 | 70 | return $resolvedModulePath;//can be null if no matching module path has been found 71 | }; 72 | 73 | $_moduleExists = function ($modulePath) use (&$_getResourceFullPath, &$config) 74 | { 75 | return (boolean) $_getResourceFullPath($modulePath, $config['modulesExt']); 76 | }; 77 | 78 | $_searchResource = function ($searchedResourceFullPath, $fileExtToAdd = '') use (&$config) 79 | { 80 | static $resolutionsCache = array(); 81 | 82 | $moduleCacheId = $searchedResourceFullPath . '|' . $fileExtToAdd; 83 | if (isset($resolutionsCache[$moduleCacheId])) { 84 | 85 | return $resolutionsCache[$moduleCacheId]; 86 | } 87 | 88 | $resolvedModulePath = null; 89 | if (is_file($searchedResourceFullPath . $fileExtToAdd)) { 90 | // This is a regular "file Module" ; we just add the file extension 91 | $resolvedModulePath = $searchedResourceFullPath . $fileExtToAdd; 92 | } else if (is_dir($searchedResourceFullPath)) { 93 | $directoryModulePath = $searchedResourceFullPath . DIRECTORY_SEPARATOR . $config['folderAsModuleFileName']; 94 | if (file_exists($directoryModulePath)) { 95 | // Yeah! This is a "folder as Module" 96 | $resolvedModulePath = $directoryModulePath; 97 | } 98 | } 99 | 100 | if (null !== $resolvedModulePath) { 101 | $resolvedModulePath = str_replace('/', DIRECTORY_SEPARATOR, $resolvedModulePath); 102 | $resolvedModulePath = realpath($resolvedModulePath); 103 | } 104 | 105 | $resolutionsCache[$moduleCacheId] = $resolvedModulePath; 106 | 107 | return $resolvedModulePath; 108 | }; 109 | 110 | $_triggerModule = function ($moduleFilePath) use (&$config, &$require, &$define, &$_currentResolvedModuleDir, &$_getResourceFullPath, &$_moduleExists) 111 | { 112 | // Env setup... 113 | $module = array(); 114 | $module['id'] = str_replace($config['basePath'], '', $moduleFilePath);//can handle string or array "$config['basePath']" :-) 115 | $module['id'] = str_replace( 116 | array(DIRECTORY_SEPARATOR, $config['modulesExt']), 117 | array('/', ''), 118 | $module['id'] 119 | ); 120 | $module['uri'] = $moduleFilePath; 121 | $module['resolve'] = function ($modulePath) use ($_getResourceFullPath, $config) 122 | { 123 | return $_getResourceFullPath($modulePath, $config['modulesExt']); 124 | }; 125 | $module['moduleExists'] = function ($modulePath) use ($_moduleExists) 126 | { 127 | return $_moduleExists($modulePath); 128 | }; 129 | $exports = array(); 130 | 131 | if ($config['autoNamespacing']) { 132 | 133 | $moduleTrigger = function () use ($moduleFilePath, &$require, &$define, &$module, &$exports) 134 | { 135 | // Yes, you're right : I probably deserve death for this "eval()" usage... 136 | // But I have not been able to find another way of using properly isolated classes in this CommonJS Modules PHP implementation :-) 137 | // 1) Let's create a unique namespace, based on the Module ID and prefixed with "CommonJS\Module" 138 | $moduleDynamicNamespace = 'CommonJS\Module' . str_replace('/', '\\', $module['id']); 139 | $moduleDynamicNamespace = preg_replace('|[\s-]|i', '_', $moduleDynamicNamespace); 140 | // 2) The PHP Module file content is read... 141 | $moduleFileContent = file_get_contents($moduleFilePath); 142 | // 3) ...and we add a dynamic namespace before it 143 | $moduleFileContent = 'namespace '.$moduleDynamicNamespace.'; ?>'.$moduleFileContent; 144 | // 4) Now we can actually trigger this PHP Module code, properly isolated in a unique namespace! 145 | eval($moduleFileContent); 146 | }; 147 | 148 | } else { 149 | 150 | $moduleTrigger = function () use ($moduleFilePath, &$require, &$define, &$module, &$exports) 151 | { 152 | include $moduleFilePath; 153 | }; 154 | 155 | } 156 | 157 | 158 | // Go! 159 | $previousResolvedModuleDir = $_currentResolvedModuleDir;//current dir backup... 160 | $_currentResolvedModuleDir = dirname($moduleFilePath); 161 | call_user_func($moduleTrigger); 162 | $_currentResolvedModuleDir = $previousResolvedModuleDir;//..and restore! 163 | 164 | // Result analysis 165 | if (isset($module['exports'])) { 166 | 167 | return $module['exports']; 168 | } else { 169 | 170 | return $exports; 171 | } 172 | }; 173 | 174 | $_triggerDefine = function ($definitionPath) use (&$_definitionsRegistry, &$require) 175 | { 176 | // Env setup... 177 | $module = array(); 178 | $exports = array(); 179 | $defineArgs = array(&$require, &$exports, &$module); 180 | 181 | // Go! 182 | $definitionResult = call_user_func_array($_definitionsRegistry[$definitionPath], $defineArgs); 183 | 184 | // Result analysis 185 | if (null === $definitionResult) { 186 | if (isset($module['exports'])) { 187 | $definitionResult = $module['exports']; 188 | } else if (sizeof($exports) > 0) { 189 | $definitionResult = $exports; 190 | } 191 | } 192 | 193 | return $definitionResult; 194 | }; 195 | 196 | $_triggerPlugin = function ($extensionName, $resourcePath) use (&$plugins, &$require, &$_getResourceFullPath) 197 | { 198 | static $pluginsResolutionsCache = array(); 199 | 200 | $extensionFilePath = $plugins[$extensionName]; 201 | $resourcePath = $_getResourceFullPath($resourcePath); 202 | 203 | $pluginCacheId = $extensionFilePath . '|' . $resourcePath; 204 | if (isset($pluginsResolutionsCache[$pluginCacheId])) { 205 | 206 | return $pluginsResolutionsCache[$pluginCacheId]; 207 | } 208 | 209 | $extensionTrigger = function () use ($extensionFilePath, &$require, $resourcePath) 210 | { 211 | return require $extensionFilePath; 212 | }; 213 | $extensionResult = call_user_func($extensionTrigger); 214 | 215 | $pluginsResolutionsCache[$pluginCacheId] = $extensionResult; 216 | 217 | return $extensionResult; 218 | }; 219 | 220 | /** 221 | * @param string $definitionPath 222 | * @param callable $moduleDefinition 223 | * @public 224 | */ 225 | $define = function ($definitionPath, \Closure $moduleDefinition) use (&$_definitionsRegistry, &$_modulesRegistry) 226 | { 227 | if (isset($_modulesRegistry[$definitionPath])) { 228 | unset($_modulesRegistry[$definitionPath]);//clear previous defined module result cache 229 | } 230 | 231 | $_definitionsRegistry[$definitionPath] = $moduleDefinition; 232 | }; 233 | 234 | /** 235 | * @param string $modulePath 236 | * @public 237 | * @return mixed 238 | * @throw \Exception 239 | */ 240 | $require = function ($modulePath) use (&$config, &$plugins, &$_definitionsRegistry, &$_modulesRegistry, 241 | &$_triggerModule, &$_getResourceFullPath, &$_triggerDefine, &$_triggerPlugin, &$_currentResolvedModuleDir) 242 | { 243 | // "define()"-ed Module 244 | if (isset($_definitionsRegistry[$modulePath])) { 245 | 246 | if (isset($_modulesRegistry[$modulePath])) { 247 | 248 | return $_modulesRegistry[$modulePath]; 249 | } else { 250 | // First "define()" module definition resolution: after that, it will be resolved with the modules registry 251 | $moduleDefinitionResult = $_triggerDefine($modulePath); 252 | $_modulesRegistry[$modulePath] = $moduleDefinitionResult; 253 | 254 | return $moduleDefinitionResult; 255 | } 256 | 257 | } 258 | 259 | // Do we use a plugin on this Module path (i.e. do we have a [prefix]![resource path] pattern) ? 260 | if (preg_match('|^(\w+)!([a-z0-9/_.-]+)$|i', $modulePath, $matches)) { 261 | $pluginName = $matches[1]; 262 | $resourcePath = $matches[2]; 263 | if (!isset($plugins[$pluginName])) { 264 | throw new Exception('Unregistered plugin "'.$pluginName.'" (resource path: "'.$resourcePath.'")!'); 265 | } 266 | $moduleDefinitionResult = $_triggerPlugin($pluginName, $resourcePath); 267 | 268 | return $moduleDefinitionResult; 269 | } 270 | 271 | // Regular Module resolution 272 | $fullModulePath = $_getResourceFullPath($modulePath, $config['modulesExt']); 273 | if (null === $fullModulePath) { 274 | throw new Exception('Unresolvable module "'.$modulePath.'" (from path: '.$_currentResolvedModuleDir.')!'); 275 | } 276 | 277 | if (isset($_modulesRegistry[$fullModulePath])) { 278 | return $_modulesRegistry[$fullModulePath];//previously resolved Module 279 | } 280 | 281 | // Okay, let's trigger this Module! 282 | $moduleResolution = $_triggerModule($fullModulePath); 283 | $_modulesRegistry[$fullModulePath] = $moduleResolution;//this Module won't have to be resolved again 284 | 285 | return $moduleResolution; 286 | }; 287 | 288 | return array( 289 | 'define' => $define, 290 | 'require' => $require, 291 | 'config' => &$config, 292 | 'plugins' => &$plugins, 293 | ); 294 | }); 295 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commonjs/commonjs", 3 | "type": "library", 4 | "description": "A simple CommonJS Module spec implementation for PHP 5.3+", 5 | "keywords": ["commonjs", "module", "pattern", "dependency injection", "container", "microframework"], 6 | "homepage": "https://github.com/DrBenton/CommonJSForPHP", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Olivier Philippon", 11 | "homepage": "https://github.com/DrBenton" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.3" 16 | }, 17 | "autoload": { 18 | "psr-0": { "CommonJS": "src/" } 19 | }, 20 | "extra": { 21 | "branch-alias": { 22 | "dev-master": "1.0-dev" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | -------------------------------------------------------------------------------- /plugins/commonsjs-plugin.json.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace CommonJS; 12 | 13 | /** 14 | * Allows a Composer-based CommonJS API access 15 | */ 16 | class CommonJSProvider 17 | { 18 | 19 | static protected $instances = array(); 20 | 21 | static public function getInstance ($instanceName = 'default') 22 | { 23 | if (!isset(self::$instances[$instanceName])) { 24 | self::$instances[$instanceName] = include __DIR__ . '/../../commonjs.php'; 25 | } 26 | 27 | return self::$instances[$instanceName]; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /tests/CommonJSTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | class CommonJSTest extends \PHPUnit_Framework_TestCase 12 | { 13 | 14 | public function testSimpleDefinition () 15 | { 16 | $commonJs = include __DIR__ . '/../commonjs.php'; 17 | $define = $commonJs['define']; 18 | $require = $commonJs['require']; 19 | 20 | $define('definition1', function () { 21 | return 10; 22 | }); 23 | $this->assertEquals(10, $require('definition1')); 24 | } 25 | 26 | public function testModuleDirectExport () 27 | { 28 | $commonJs = include __DIR__ . '/../commonjs.php'; 29 | $require = $commonJs['require']; 30 | $commonJs['config']['basePath'] = __DIR__; 31 | 32 | $this->assertEquals('direct-export', ''.$require('module-dir/direct-export')); 33 | } 34 | 35 | public function testModuleMultipleExports () 36 | { 37 | $commonJs = include __DIR__ . '/../commonjs.php'; 38 | $require = $commonJs['require']; 39 | $commonJs['config']['basePath'] = __DIR__; 40 | 41 | $services = $require('module-dir/multiple-exports'); 42 | $this->assertEquals(100, $services['service1']); 43 | $this->assertEquals(200, $services['service2']); 44 | } 45 | 46 | public function testRelativeModuleResolution () 47 | { 48 | $commonJs = include __DIR__ . '/../commonjs.php'; 49 | $require = $commonJs['require']; 50 | $commonJs['config']['basePath'] = __DIR__; 51 | 52 | $this->assertEquals(300, $require('./module-dir/relative-module')); 53 | } 54 | 55 | public function testRecursiveModulesResolution () 56 | { 57 | $commonJs = include __DIR__ . '/../commonjs.php'; 58 | $require = $commonJs['require']; 59 | $commonJs['config']['basePath'] = __DIR__; 60 | 61 | $this->assertEquals(400, $require('module-dir/relative-module-consumer')); 62 | $this->assertEquals(500, $require('./module-dir/package1/relative-upper-module-consumer')); 63 | } 64 | 65 | public function testCommonJsDefine () 66 | { 67 | $commonJs = include __DIR__ . '/../commonjs.php'; 68 | $define = $commonJs['define']; 69 | $require = $commonJs['require']; 70 | $commonJs['config']['basePath'] = __DIR__; 71 | 72 | $that = &$this; 73 | $define('definition1', function () { 74 | return 10; 75 | }); 76 | $define('definition2', function ($require, &$exports, &$module) use ($that) { 77 | $that->assertEquals(10, $require('definition1')); 78 | $module['exports'] = 20; 79 | }); 80 | $define('definition3', function ($require, &$exports, &$module) use ($that) { 81 | $that->assertEquals(20, $require('definition2')); 82 | $exports['value1'] = 30; 83 | }); 84 | $this->assertEquals(20, $require('definition2')); 85 | $definition3 = $require('definition3'); 86 | $this->assertEquals(30, $definition3['value1']); 87 | } 88 | 89 | public function testModuleCodeIsTriggeredOnlyOnce () 90 | { 91 | $commonJs = include __DIR__ . '/../commonjs.php'; 92 | $require = $commonJs['require']; 93 | $commonJs['config']['basePath'] = __DIR__; 94 | 95 | $this->assertEquals(1, $require('module-dir/incrementer-module')); 96 | $this->assertEquals(1, $require('module-dir/incrementer-module')); 97 | } 98 | 99 | public function testOtherModulesFileExtension () 100 | { 101 | $commonJs = include __DIR__ . '/../commonjs.php'; 102 | $require = $commonJs['require']; 103 | $commonJs['config']['basePath'] = __DIR__; 104 | $commonJs['config']['modulesExt'] = '.inc'; 105 | 106 | $this->assertEquals(100, $require('module-dir/module-with-another-ext')); 107 | } 108 | 109 | public function testBundledJsonPlugin () 110 | { 111 | $commonJs = include __DIR__ . '/../commonjs.php'; 112 | $require = $commonJs['require']; 113 | $commonJs['config']['basePath'] = __DIR__; 114 | 115 | $this->assertEquals(array('key1' => 100, 'key2' => 200), $require('json!module-dir/resources/data.json')); 116 | } 117 | 118 | public function testCustomPlugin () 119 | { 120 | $commonJs = include __DIR__ . '/../commonjs.php'; 121 | $require = $commonJs['require']; 122 | $commonJs['config']['basePath'] = __DIR__; 123 | $commonJs['plugins']['fileRev'] = __DIR__ .'/module-dir/custom-plugins/commonjs-plugin.file-reverser.php'; 124 | 125 | $this->assertEquals('notneBrD', $require('fileRev!./module-dir/resources/simple-text.txt')); 126 | } 127 | 128 | public function testPluginIsTriggeredOnlyOnceForSameResourcePath () 129 | { 130 | $commonJs = include __DIR__ . '/../commonjs.php'; 131 | $require = $commonJs['require']; 132 | $commonJs['config']['basePath'] = __DIR__; 133 | $commonJs['plugins']['incrementer'] = __DIR__ .'/module-dir/custom-plugins/commonjs-plugin.incrementer.php'; 134 | 135 | $this->assertEquals(1, $require('incrementer!./module-dir/resources/simple-text.txt')); 136 | $this->assertEquals(1, $require('incrementer!./module-dir/resources/simple-text.txt')); 137 | // Must work with absolute module path if it is resolved the same absolute resource path than the previous absolute resource path 138 | $this->assertEquals(1, $require('incrementer!module-dir/resources/simple-text.txt')); 139 | $this->assertEquals(1, $require('incrementer!/module-dir/resources/simple-text.txt')); 140 | } 141 | 142 | /** 143 | * @depends testPluginIsTriggeredOnlyOnceForSameResourcePath 144 | */ 145 | public function testPluginIsTriggeredMultipleTimesForDifferentsResourcePath () 146 | { 147 | $commonJs = include __DIR__ . '/../commonjs.php'; 148 | $require = $commonJs['require']; 149 | $commonJs['config']['basePath'] = __DIR__; 150 | $commonJs['plugins']['incrementer'] = __DIR__ .'/module-dir/custom-plugins/commonjs-plugin.incrementer.php'; 151 | 152 | $this->assertEquals(2, $require('incrementer!./module-dir/resources/simple-text.txt')); 153 | $this->assertEquals(3, $require('incrementer!module-dir/resources/data.json')); 154 | $this->assertEquals(2, $require('incrementer!module-dir/resources/simple-text.txt')); 155 | $this->assertEquals(3, $require('incrementer!./module-dir/resources/data.json')); 156 | } 157 | 158 | public function testModuleIdAndUri () 159 | { 160 | $commonJs = include __DIR__ . '/../commonjs.php'; 161 | $require = $commonJs['require']; 162 | $commonJs['config']['basePath'] = __DIR__; 163 | 164 | $expectedId = '/module-dir/module-id-and-uri-exporter'; 165 | $expectedUri = realpath(__DIR__.'/module-dir/module-id-and-uri-exporter.php'); 166 | 167 | $result = $require('./module-dir/module-id-and-uri-exporter'); 168 | $this->assertEquals($expectedId, $result['id']); 169 | $this->assertEquals($expectedUri, $result['uri']); 170 | } 171 | 172 | public function testFolderAsModule () 173 | { 174 | $commonJs = include __DIR__ . '/../commonjs.php'; 175 | $require = $commonJs['require']; 176 | $commonJs['config']['basePath'] = __DIR__; 177 | 178 | $this->assertEquals(500, $require('/module-dir/folder-as-module')); 179 | } 180 | 181 | public function testMultipleBasePaths () 182 | { 183 | $commonJs = include __DIR__ . '/../commonjs.php'; 184 | $require = $commonJs['require']; 185 | $commonJs['config']['basePath'] = array( 186 | __DIR__ . '/module-dir', 187 | __DIR__ . '/alt-module-dir', 188 | ); 189 | 190 | $this->assertEquals('direct-export', ''.$require('direct-export')); 191 | $this->assertEquals('alt-direct-export', $require('alt-direct-export')); 192 | $this->assertEquals(600, $require('alt-module-consumer')); 193 | $this->assertEquals(700, $require('alt-relative-module-consumer')); 194 | } 195 | 196 | public function testObjectOrientedProgrammingAccess () 197 | { 198 | $commonJs = \CommonJS\CommonJSProvider::getInstance(); 199 | 200 | $this->assertArrayHasKey('require', $commonJs); 201 | $this->assertArrayHasKey('define', $commonJs); 202 | $this->assertArrayHasKey('config', $commonJs); 203 | 204 | $commonJs['config']['basePath'] = __DIR__ . '/module-dir'; 205 | 206 | $commonJsBis = \CommonJS\CommonJSProvider::getInstance(); 207 | 208 | $this->assertEquals($commonJs['config']['basePath'], $commonJsBis['config']['basePath']); 209 | 210 | $commonJsTer = \CommonJS\CommonJSProvider::getInstance('test'); 211 | 212 | $this->assertNotEquals($commonJs['config']['basePath'], $commonJsTer['config']['basePath']); 213 | 214 | $commonJsTer['config']['basePath'] = __DIR__ . '/alt-module-dir'; 215 | 216 | $this->assertEquals('direct-export', call_user_func($commonJs['require'], 'direct-export')); 217 | $this->assertEquals('alt-direct-export', call_user_func($commonJsTer['require'], 'alt-direct-export')); 218 | 219 | $this->assertEquals('a-module', call_user_func($commonJs['require'], 'a-module')); 220 | $this->assertEquals('alt-a-module', call_user_func($commonJsTer['require'], 'a-module')); 221 | } 222 | 223 | public function testModuleResolution () 224 | { 225 | $commonJs = include __DIR__ . '/../commonjs.php'; 226 | $require = $commonJs['require']; 227 | $commonJs['config']['basePath'] = __DIR__; 228 | 229 | $resolutionModuleResult = $require('module-dir/resolution-module'); 230 | $this->assertEquals(array( 231 | __DIR__ . '/module-dir/direct-export.php', 232 | __DIR__ . '/module-dir/a-module.php', 233 | null, 234 | null, 235 | true, 236 | true, 237 | false, 238 | false, 239 | ), $resolutionModuleResult); 240 | } 241 | 242 | public function testRawModulesClasses () 243 | { 244 | $commonJs = include __DIR__ . '/../commonjs.php'; 245 | $require = $commonJs['require']; 246 | $commonJs['config']['basePath'] = __DIR__; 247 | 248 | $fooClass = $require('module-dir/classes/foo'); 249 | $fooInstance = new $fooClass(); 250 | $this->assertEquals('Foo', ''.$fooInstance); 251 | } 252 | 253 | public function testAutomaticallyNamespacedModulesClassesWithoutCollision () 254 | { 255 | $commonJs = include __DIR__ . '/../commonjs.php'; 256 | $require = $commonJs['require']; 257 | $commonJs['config']['basePath'] = __DIR__; 258 | $commonJs['config']['autoNamespacing'] = true; 259 | 260 | $fooClass = $require('module-dir/classes/foo'); 261 | $fooInstance = new $fooClass(); 262 | $this->assertEquals('CommonJS\Module\module_dir\classes\foo\Foo', ''.$fooInstance); 263 | 264 | $anotherFooClass = $require('module-dir/classes/package/foo'); 265 | $anotherFooInstance = new $anotherFooClass(); 266 | $this->assertEquals('CommonJS\Module\module_dir\classes\package\foo\Foo', ''.$anotherFooInstance); 267 | 268 | $fooFactoryModule = $require('module-dir/classes/package/foo-factory'); 269 | $fooInstanceWithFactory = call_user_func($fooFactoryModule['getInstance']); 270 | $this->assertEquals('CommonJS\Module\module_dir\classes\package\foo_factory\Foo', ''.$fooInstanceWithFactory); 271 | 272 | $anotherFooSubClass = $require('module-dir/classes/package/foo-subclass'); 273 | $anotherFooSubInstance = new $anotherFooSubClass(); 274 | $this->assertEquals('CommonJS\Module\module_dir\classes\package\foo_subclass\Foo', ''.$anotherFooSubInstance); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /tests/alt-module-dir/a-module.php: -------------------------------------------------------------------------------- 1 |