commonjs/commonjs

A simple CommonJS Module spec implementation for PHP 5.3+

dev-master / 1.0.x-dev 2013-09-12 09:57 UTC

This package is not auto-updated.

Last update: 2024-11-09 12:57:49 UTC


README

A simple CommonJS spec implementation for PHP 5.3+.

build status

It fits in a single PHP file (≈ 150 lines of effective code) and allows a simple and easy application structure, based on the CommonJS "Module" design pattern. You might already know this pattern if you have ever worked with Node.js (server-side Javascript) or RequireJS (client-side Javascript).

From JavaScript Growing Up:

CommonJS introduced a simple API for dealing with modules:

  • "require" for importing a module.
  • "exports" for exposing stuff from a module

This PHP implementation also supports two features inspired by RequireJS:

  • defined-by-Closures Modules, with the $define function
  • resources Plugins (a simple "JSON decoder" is bundled as a sample).

It comes with a "Folder as Modules" feature too, inspired by Node.js.

Why CommonJS for PHP ?

  • CommonJS "Module" pattern is simple and efficient ; it lets you quickly create easily understandable and flexible code.
  • Between 2 beautiful projects based on Symfony, Zend Framework, Slim, Silex or whatever modern PSR-0 heavy Object Oriented framework, have some rest with simple "good ol' procedural" PHP codestyle!
  • Feel comfortable in PHP when you're back from a Node.js or front-end AMD project.
  • CommonJS Module pattern acts as a very simple Service Locator and lazy-loaded dependencies resolver.
  • Have fun with isolated PHP code parts! Every Module runs in an automatically generated Closure, and you can freely create variables and Closures within your Modules without without fearing a pollution of the PHP global space, nor collisions with other Modules code.
  • All your Modules code run in a "Closure sandbox", and Modules communicate between each other only through their $require() function and $exports variable.
  • Every Module content is run only once - the first time it is required.
  • CommonJS for PHP is perfectly interoperable with PSR-0 classes. You can use Symfony 2 or Zend Framework or yet other components in your Modules. It can be used with libraries managed by Composer as well.
  • This CommonJS implementation for PHP can be used as a micro-framework for quick little projects...
  • ...But it can used for large serious projects too ; thousands of Node.js and AMD developers use this CommonJS Module pattern everyday.

The code is willfully 100% procedural and Closures-based - this way, this CommonJS spec implementation code looks like Javascript code :-)

Synopsis

// ******************************* file "index.php"
// CommonJs setup
$commonJS = include './commonjs.php';
$define = $commonJS['define'];
$require = $commonJS['require'];
$commonJS['config']['basePath'] = __DIR__ '/modules';

// Custom plugin?
$commonJS['plugins']['yaml'] = __DIR__ . '/commonsjs-plugin.yaml.php';

// Modules are files ; but you can define "modules-as-Closures" too
$define('logger', function($require) {
    return function($msg) {
        syslog(LOG_DEBUG, $msg);
    };
});

// Boostrap module trigger!
$require('app/bootstrap');


// ******************************* file "modules/app/bootstrap.php"
/**
 * Note that "$define", "$require", "$exports" and "$module"
 * are automatically globally defined in the Module!
 */
$config = $require('yaml!../config/config.yml');//we use the YAML plugin with a relative path
$logger = $require('logger');
$requestBridge = $require('../vendor/symfony-bridge/request');
$router = $require('app/router');

$request = $requestBridge->createFromGlobals();
list($targetControllerModule, $targetAction) = $router->resolveRequest($request);
$config['debug'] && $logger('** $targetControllerModule='.$targetControllerModule);
$require($targetControllerModule)->$targetAction();

API

CommonJS environment initialization

To initialize the CommonJS environment you simply have to do this:

$commonJS = include './commonjs.php';

The $commonJS returned associative Array contains the following keys :

  • define: the CommonJS define() Closure ; outside Modules, you can alias it with $define = $commonJS['define'];
  • require: the CommonJS require() Closure ; outside Modules, you can alias it with $require = $commonJS['require'];
  • config: a simple config associative Array with 2 keys:
    • basePath: the base path of your Modules. Every Module you require() without a relative path will be located in this directory path. Default is the commonjs.php's __DIR__
    • modulesExt: the extension to add to the requested Modules path. Default: '.php'
    • folderAsModuleFileName: the file name for "folders as Modules". Default: 'index.php'
    • autoNamespacing: set this boolean to true to have all your Modules PHP code automatically wrapped in unique namespaces at runtime. See Classes section for more details. Default: false
  • plugins: this associative Array is the CommonJS for PHP "a la RequireJS" plugins registry. Keys are plugin prefixes, values are paths to plugins files. See Plugins section for more details.

Note that you can also use an array for config['basePath'] : this allow you to define multiple modules root paths :

$commonJS = include './commonjs.php';
$commonJS['config']['basePath'] = array(
    __DIR__.'/app/modules',
    __DIR__.'/vendor/symfony-bridge/modules',
);

If you use Composer, a best CommonJS environment initialization can be used:

$commonJS = \CommonJS\CommonJSProvider::getInstance();

Make sure that you first added CommonJS in your composer.json file :

{
    "minimum-stability": "dev",
    "require": {
        "commonjs/commonjs": "1.0.*"
    }
}

You can share a single instance of a CommonJS environment, but you may use multiple CommonJS environments if you need to. Since these environments are very light-weight library instances, you can do this without worrying about performance.

$commonJS = \CommonJS\CommonJSProvider::getInstance();

// ... in another file :
$commonJS = \CommonJS\CommonJSProvider::getInstance();
// --> will return the same shared CommonJS environment

// ... in yet another file :
$commonJS = \CommonJS\CommonJSProvider::getInstance('default');
// --> will also return the same shared CommonJS environment, as 'default' is the default CommonJS instance id

// ... in a last file :
$commonJS = \CommonJS\CommonJSProvider::getInstance('my-provider-instance');
// --> will return a fresh new CommonJS environment, with it own basePath, plugins and Modules

require()

Triggers the resolution of a Module. All Modules resolutions are triggered only once, the first time they are requested. All subsequent calls to this Module will return the same value, retrieved from an internal data cache.

They are 4 types of Module resolutions:

  • Modules mapped to a Closure through the define() function. When the required module path matches a previously defined Module path, the Closure is triggered and we fetch its returned value.
  • Modules mapped to files. This is the most common Module type. The module path is resolved, and the matching PHP file is triggered in a CommonJS environment.
    • The module path resolution follows this rule: if the module path begins with ./ or ../, the PHP file path will be resolved relatively to the current Module path. Otherwise, the Module path is just appended to the CommonJS config "basePath" path.
    • Don't use the ".php" file extension in your required modules paths. It will be automatically appended to the resolved file path.
  • Modules mapped to folders. It is very close to the previous "Modules mapped to files" behaviour, excepted that you only have to use a folder path as the $require-ed module file path. If the folder contains a "index.php" file, this file will be used as the file Module.
  • Modules mapped to plugins. When a module path contains a prefix followed by an exclamation mark, it is considered as a plugin call. The part before the "!" is the plugin name, and the part after the "!" is the resource name. See Plugins section for more detail.
// All Module types:

// Closure-mapped Module:
$define('config', function() { return array('debug' => true, 'appPath' => __DIR__); });
$config = $require('config');

// Absolute Module file resolution: (absolute, but relative to the CommonJS "config['basePath']" path)
// --> will trigger the "app/logger.php" Module file code
$logger = $require('app/logger');

// Relative Module file resolution: (relative to the Module which calls "$require()")
// --> will trigger the "../mailer.php" Module file code
$logger = $require('../mailer');

// Folder as Module resolution: (works with absolute or relative paths)
// --> will trigger the "symfony-bridge/request/index.php" Module file code
$logger = $require('symfony-bridge/request');

// Plugin call:
$myModuleConfig = $require('json!./module-config.json');

Modules scope

This is the heart of the CommonJS coolness! Every Module is isolated from others Modules, and interact with them only through its $require() method (for input) and its $exports array (for output).

In a Module you can create vars and Closures without fearing a pollution of the PHP global space, nor collisions with other Modules code. Every time a Module is triggered, its code is automatically embedded is a generated PHP Closure.

In this "Closure sandbox", your Module have automatically access to the following vars: (and only to them)

  • $require: the $require function. Let's you access to other Modules exports.
  • $define: the $define function. You can dynamically create new "mapped to Closures" Modules definitions in your Modules.
  • $exports: this is an empty Array. Add key/values couples to this Array, and they're will be automatically available in other Modules.
  • $module: is mainly used for direct Modules export value. If you want to export a single value from a Module, use $module['exports'] = $mySingleExportedValue;. Additionally, you have access to $module['id'] and $module['uri'] properties, according to CommonJS spec. You also have access to $module['resolve'] and $module['moduleExists'] functions. The first one returns a resolved full module path or null ; the second one returns a boolean, and allows you to test whether a module is defined or not. uri is the realpath of the Module file, and id is the absolute Module path of the Module :

The "id" property must be such that require(module.id) will return the exports object from which the module.id originated (That is to say module.id can be passed to another module, and requiring that must return the original module).

define()

The define() function lets you create Modules resolved by Closures. The first param is the path of the Module you define, and the second one is a Closure. The first time this Module path is $require-ed, this Closure is triggered and its return value is used a the Module value resolution. Subsequent calls will return the same value, managed by an internal data cache.

$define('config', function() {
    return array('debug' => true, 'appPath' => __DIR__);
});

// PHP 5.4 (needs function array dereferencing)
echo $require('config')['debug'];//--> 'true'
// PHP 5.3
$config = $require('config')
echo $config['debug'];//--> 'true'

The triggered Closure can accept up to 3 injected params: $require, $exports and $module. $require let you require other modules from your Closure, while $exports and $module allows you to define the Module value resolution in a "CommonJS way":

$define('app/logger', function($require, &$exports, &$module) {
    $config = $require('config');
    $logger = new Monolog\Logger('app');
    if ($config['debug']) {
        $logger->pushHandler(new Monolog\Handler\StreamHandler($config['appPath'] . 'logs/app.log'));
    }
    $module['exports'] = $logger;
});

$logger = $require('app/logger');

If you want to use that form instead of a simple return, be aware that because call-time pass by reference has been removed in PHP 5.4, you have to use &$exports and &$module and not just just $exports / $module in your definition Closure params. Although it is possible to omit the "&" in PHP 5.3, it's better to think about the future :-)

Plugins

CommonJS for PHP is bundled with a minimalist "a la RequireJS" plugin system. A plugin is defined by a unique name and a resource path. They are triggered when the $require() function parameter is a path containing an exclamation mark. The part before the "!" is the plugin name, and the part after the "!" is the resource path: [plugin name]![resource path].

Like Modules, plugins are triggered in a generated Closure, and run in their own scope. This scope only contains the $require and $resourcePath variables. With $require() you can have access to other Modules and plugins, while $resourcePath is the part of the required Module path after the "!". The resource path is resolved in the same way than Modules: they can be relative to the Module which triggers the plugin or absolute.

Like $require-ed Modules (which are triggered only once), plugins already triggered with the same resolved resource path will return a cached result value for subsequent calls.

// A YAML sample plugin

// ******************************* file "app/plugins/commonsjs-plugin.yaml.php"
$yamlParser = new \Symfony\Component\Yaml\Parser();
return $yamlParser->parse(file_get_contents($resourcePath));


// ******************************* file "app/bootstrap.php"
$commonJS['plugins']['yaml'] = __DIR__ . '/app/plugins/commonsjs-plugin.yaml.php';


// ******************************* file "app/config.php"
$config = $require('yaml!./resources/config.yml');

Classes

You can declare PHP classes in your CommonJS Modules, but as PHP doesn't support nested classes or namespaces defined at runtime, you have to be careful and not create same classes names in different Modules.

So, the cleaner solution is to use Composer or another class loading system to handle your classes.
Modules and classes declarations are kept separated : you declare your classes in some location, like you usually do in your PHP projects, and you use them in your Modules.

If you really want to declare classes in your PHP "à la CommonJS" Modules, you have two options :

  • you can choose to handle classes collisions prevention yourself. You can use hardcoded classes names prefixes or namespaces to ensure your classes names don't overlap each other, like you usually do in PHP.
  • but you can also use the "autoNamespacing" config setting of this COmmonJS implementation. If you set it to true, all your Modules will be automatically enclosed in dynamic namespaces, created at runtime.

Be aware, though, that the implementation of this dynamic classes names isolation in CommonJS Modules has to rely on namespace and eval().
Yes, eval() is used, and this is absolutely EVIL, but I have not been able to find another way of properly isolating classes in Modules :-)
The fact is that PHP classes are global constants, thus you can't define a Foo class in a Module file and another Foo class in another file. This doesn't fit the CommonJS way, since you should be able to declare a class in a Module without having to care about conflicting classes names and without having to hardcode namespaces in your Module.
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 without having to manually handle classes names uniqueness.

The only trick to declare and export classes is to prefix their name with the magic constant __NAMESPACE__ in your Module exports, or to use a simple Factory:

// file 'lib/mailing/test/mailer.php'
class Mailer
{
    public function sendEmail() {
        $logger->log("test email sent!");//the email is not sent
    }
}

$module['exports'] = __NAMESPACE__.'\\Mailer';

// file 'lib/mailing/prod/mailer.php'
class Mailer
{
    public function sendEmail() {
        $mailerService->sendEmail();//the email is really sent
    }
}

$module['exports'] = __NAMESPACE__.'\\Mailer';

// file 'lib/mailing/dev/mailer.php'
class Mailer
{
    public function sendEmail() {
        $mailerService->sendEmail();//the email is really sent
    }
}

// Instead of having to deal with the __NAMESPACE__,you can also use a Factory!
$exports['getInstance'] = function ()
{
    return new Mailer();
};

// file 'app/subscription/confirm.php'
$mailerClass = $require('lib/mailing/prod/mailer');
$mailerInstance = new $mailerClass(); // the full class name contains a dynamic namespace, but you don't have to deal with this
$mailerInstance->sendEmail();

In this sample, two "Mailer" classes are declared : in a normal PHP world, this should trigger an error, but with this CommonJS implementation the Module content is automatically namespaced at runtime, allowing you to declare classes without having to worry about classes names collision.

Of course, they are some limitations with this system. Since PHP doesn't allow dynamic inheritance (i.e. class SubClass extends $className), you can't inherit a class without using its hardcoded dynamic namespace.

These namespaces naming scheme is : "\CommonJS\Module[Module ID]", where the Module ID slashes are replaced with backslashes and special chars replaced with underscores.
For example, a UserCheck class declared in a "lib/services/user-check" Module will have this full class name:
\CommonJS\Module\lib\services\user_check\UserCheck

You can look at the "tests/module-dir/classes/package/foo-subclass.php" PHP unit test class to see a sample of this dynamic namespacing hardcoded usage.

More info

As the source code of this "CommonJS for PHP" library is a lot shorter than this README, you can have a look at it for further information :-)

You can also look at Unit Tests, since they cover a rather large scope of use cases.

License

(The MIT License)

Copyright (c) 2012 Olivier Philippon https://github.com/DrBenton

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.