commonjs/commonjs

PHP 5.3+ 的简单 CommonJS 模块规范实现

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

This package is not auto-updated.

Last update: 2024-09-28 12:26:07 UTC


README

PHP 5.3+ 的简单 CommonJS 规范实现。

build status

它适合在单个 PHP 文件中(≈ 150 行有效代码)运行,允许基于 CommonJS "模块" 设计模式的简单且易于使用应用程序结构。如果您曾经使用过 Node.js(服务器端 JavaScript)或 RequireJS(客户端 JavaScript),您可能已经了解这种模式。

来自 JavaScript Growing Up

CommonJS 引入了一个简单的 API 来处理模块

  • "require" 用于导入模块。
  • "exports" 用于从模块中导出内容

此 PHP 实现还支持两个由 RequireJS 启发的特性

  • 由闭包定义的 模块,使用 $define 函数
  • 资源 插件(一个简单的 "JSON 解码器" 作为示例捆绑)。

它还包含一个受 Node.js 启发的 "文件夹作为模块" 功能。

为什么选择 CommonJS for PHP ?

  • CommonJS "模块" 模式简单且高效;它允许您快速创建易于理解和灵活的代码。
  • 在基于 Symfony、Zend Framework、Slim、Silex 或其他现代 PSR-0 重型面向对象框架的两个美丽项目之间,用简单的 "老式过程化" PHP 代码风格休息一下!
  • 从 Node.js 或前端 AMD 项目回来时,在 PHP 中感到舒适。
  • CommonJS 模块模式充当一个非常简单的服务定位器和懒加载依赖项解析器。
  • 享受隔离的 PHP 代码部分!每个模块都在自动生成的闭包中运行,您可以在您的模块内部自由创建变量和闭包,而无需担心污染 PHP 全局空间,也不会与其他模块代码发生冲突。
  • 所有模块代码都在 "闭包沙箱" 中运行,模块之间仅通过它们的 $require() 函数和 $exports 变量进行通信。
  • 每个模块内容只运行一次 - 第一次被要求时。
  • CommonJS for PHP 与 PSR-0 类完全兼容。您可以在模块中使用 Symfony 2 或 Zend Framework 或其他组件。它也可以与由 Composer 管理的库一起使用。
  • 此 PHP 的 CommonJS 实现可以用作微框架,用于快速小型项目...
  • ...但它也可以用于大型严肃的项目;数千名 Node.js 和 AMD 开发者每天都在使用此 CommonJS 模块模式。

代码完全基于 100% 的过程化和闭包 - 这样,此 CommonJS 规范实现代码看起来像 JavaScript 代码 :-)

概要

// ******************************* 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 环境初始化

要初始化 CommonJS 环境,您只需这样做

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

返回的 $commonJS 关联数组包含以下键

  • define:CommonJS define() 闭包;在模块外部,您可以将其别名为 $define = $commonJS['define'];
  • require:CommonJS require() 闭包;在模块外部,您可以将其别名为 $require = $commonJS['require'];
  • config:一个包含两个键的简单配置关联数组
    • basePath:模块的基础路径。在没有相对路径的情况下,使用require()加载的每个模块都将位于此目录路径下。默认是commonjs.php__DIR__
    • modulesExt:添加到请求模块路径的扩展名。默认:' .php '
    • folderAsModuleFileName:"文件夹作为模块"的文件名。默认:'index.php'
    • autoNamespacing:将此布尔值设置为true以在运行时自动将所有模块PHP代码包装在唯一的命名空间中。有关更多详细信息,请参阅部分。默认:false
  • plugins:此关联数组是PHP的CommonJS "类似RequireJS"插件注册表。键是插件前缀,值是插件文件的路径。有关更多详细信息,请参阅插件部分。

请注意,您也可以为config['basePath']使用数组:这允许您定义多个模块根路径。

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

如果您使用Composer,则可以使用最佳CommonJS环境初始化。

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

请确保您首先在composer.json文件中添加了CommonJS。

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

您可以共享单个CommonJS环境的实例,但如果需要,您也可以使用多个CommonJS环境。由于这些环境是轻量级库实例,因此您可以无忧地这样做。

$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()

触发模块的解析。所有模块解析仅在第一次请求时触发。所有后续对该模块的调用都将返回相同值,该值从内部数据缓存中检索。

有4种模块解析类型。

  • 通过define()函数映射到闭包的模块。当所需的模块路径与先前定义的模块路径匹配时,将触发闭包,并检索其返回值。
  • 映射到文件的模块。这是最常见的模块类型。解析模块路径,并在CommonJS环境中触发匹配的PHP文件。
    • 模块路径解析遵循以下规则:如果模块路径以./../开头,则PHP文件路径将相对于当前模块路径解析。否则,模块路径将直接附加到CommonJS配置的"basePath"路径。
    • 不要在请求模块的路径中使用".php"文件扩展名。它将自动追加到解析的文件路径。
  • 映射到文件夹的模块。这与之前的"映射到文件"的行为非常相似,只是您只需使用文件夹路径作为$require-ed模块文件路径。如果文件夹包含"index.php"文件,则将使用此文件作为文件模块。
  • 映射到插件。当模块路径包含前缀后跟感叹号时,它被视为插件调用。感叹号之前的部分是插件名称,感叹号之后的部分是资源名称。有关更多详细信息,请参阅插件部分。
// 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');

模块作用域

这是CommonJS酷炫的核心!每个模块都与其他模块隔离,并通过其$require方法(用于输入)及其$exports数组(用于输出)与它们交互。

在模块中,您可以创建变量和闭包,而无需担心PHP全局空间的污染,也不会与其他模块代码发生冲突。每次触发模块时,其代码都会自动嵌入到一个生成的PHP闭包中。

在这个"闭包沙箱"中,您的模块自动可以访问以下变量:(并且只有这些)

  • $require$require函数。允许您访问其他模块的导出。
  • $define:表示 $define 函数。您可以在您的模块中动态创建新的“映射到闭包”的模块定义。
  • $exports:这是一个空数组。向该数组添加键/值对,它们将自动在其他模块中可用。
  • $module:主要用于直接导出模块值。如果您想从一个模块中导出一个单个值,请使用 $module['exports'] = $mySingleExportedValue;。此外,您还可以访问 $module['id']$module['uri'] 属性,根据 CommonJS 规范。您还可以访问 $module['resolve']$module['moduleExists'] 函数。第一个返回一个解析后的完整模块路径或 null;第二个返回一个布尔值,允许您测试模块是否已定义。`uri` 是模块文件的 realpath,而 `id` 是模块的绝对路径。

“id”属性必须确保 require(module.id) 将返回模块.id 产生的导出对象(也就是说,模块.id 可以传递给另一个模块,并调用它必须返回原始模块)。

define()

define() 函数允许您创建由闭包解析的模块。第一个参数是您定义的模块路径,第二个参数是闭包。第一次以 $require 调用此模块路径时,该闭包将被触发,并且其返回值被用作模块值解析。后续调用将返回相同的值,由内部数据缓存管理。

$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'

触发的闭包可以接受最多 3 个注入参数:$require$exports$module。`$require` 允许您在闭包中要求其他模块,而 `$exports` 和 `$module` 允许您以“CommonJS 方式”定义模块值解析。

$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');

如果您想使用这种形式而不是简单的 return,请注意,由于 PHP 5.4 中已经删除了调用时按引用传递,您必须使用 &$exports&$module 而不是在定义闭包参数中使用 $exports / $module。虽然在 PHP 5.3 中可以省略 "&",但为了考虑未来,最好是这样做。

插件

CommonJS for PHP 附带了一个最小化的 类似于 RequireJS 的插件系统。一个插件由一个唯一的名称和资源路径定义。当 $require() 函数参数包含感叹号时,会触发插件。感叹号之前的部分是插件名称,感叹号之后的部分是资源路径:`[插件名称]![资源路径]`。

与模块一样,插件在生成的闭包中触发,并在自己的作用域中运行。此作用域只包含 $require$resourcePath 变量。通过 $require(),您可以访问其他模块和插件,而 $resourcePath 是所需模块路径中感叹号之后的部分。资源路径以与模块相同的方式进行解析:它们可以是相对于触发插件的模块的相对路径,也可以是绝对路径。

与仅触发一次的 $require 模块(只在第一次触发)一样,已经使用相同的解析资源路径触发的插件将返回一个缓存的值,用于后续调用。

// 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');

您可以在 CommonJS 模块中声明 PHP 类,但由于 PHP 不支持嵌套类或运行时定义的命名空间,您必须小心,不要在不同模块中创建相同的类名。

因此,更干净的方法是使用 Composer 或其他类加载系统来处理您的类。
模块和类声明被分离:你可以在某些位置声明你的类,就像你在PHP项目中通常做的那样,然后在模块中使用它们。

如果你真的想在PHP "à la CommonJS" 模块中声明类,你有两种选择

  • 你可以选择自己处理类冲突的预防。你可以使用硬编码的类名前缀或命名空间来确保你的类名不相互重叠,就像你在PHP中通常做的那样。
  • 但你也可以使用此CommonJS实现的"autoNamespacing"配置设置。如果你将其设置为true,所有模块都将自动包含在运行时创建的动态命名空间中。

不过请注意,CommonJS模块中动态类名隔离的实现必须依赖于namespaceeval()
是的,使用了eval(),这绝对是邪恶的,但我找不到另一种在模块中正确隔离类的办法 :-)
事实上,PHP类是全局常量,因此你无法在模块文件中定义一个Foo类,在另一个文件中又定义另一个Foo类。这与CommonJS方式不匹配,因为你应该能够在模块中声明一个类,而无需关心类名冲突,也无需在模块中硬编码命名空间。
但由于PHP不是JavaScript,我们不得不使用这样的技巧,如果我们想在模块中声明类而无需手动处理类名的唯一性。

声明和导出类的唯一技巧是在你的模块导出中使用魔术常量__NAMESPACE__作为前缀,或者使用一个简单的工厂。

// 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();

在这个示例中,声明了两个"Mailer"类:在正常的PHP世界中,这将触发错误,但使用这个CommonJS实现,模块内容在运行时自动命名空间,允许你声明类而无需担心类名冲突。

当然,这个系统有一些限制。由于PHP不允许动态继承(即class SubClass extends $className),你不能不使用其硬编码的动态命名空间而继承一个类。

这些命名空间的命名方案是:"\\CommonJS\\Module[Module ID]",其中模块ID的反斜杠被替换为反斜杠,特殊字符被替换为下划线。
例如,在"lib/services/user-check"模块中声明的UserCheck类将具有这个完整的类名
\\CommonJS\\Module\\lib\\services\\user_check\\UserCheck

你可以查看"tests/module-dir/classes/package/foo-subclass.php"的PHP单元测试类,以查看动态命名空间硬编码使用的示例。

更多信息

由于这个"CommonJS for PHP"库的源代码比这个README要短得多,你可以查看它以获取更多信息 :-)

你还可以查看单元测试,因为它们覆盖了相当广泛的用例。

许可证

(MIT许可证)

版权所有(c)2012 Olivier Philippon https://github.com/DrBenton

在此,任何人获得本软件及其相关文档副本("软件")的副本,免费获得使用、复制、修改、合并、发布、分发、再许可和/或出售软件副本的许可,并允许获得软件的人这样做,但受以下条件的约束

上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。

本软件按“原样”提供,不提供任何形式的保证,无论是明示的还是暗示的,包括但不限于适销性、特定用途的适用性和非侵权性。在任何情况下,作者或版权所有者不应对任何索赔、损害或其他责任承担责任,无论该责任是基于合同、侵权或其他原因,无论该责任是否与软件或软件的使用或其他方面有关。