UNPKG

php-embed

Version:

Bidirectional interoperability between PHP and Node.js in a single process

444 lines (366 loc) 17.3 kB
# php-embed [![NPM][NPM1]][NPM2] [![Build Status][1]][2] [![dependency status][3]][4] [![dev dependency status][5]][6] The node `php-embed` package binds to PHP's "embed SAPI" in order to provide bidirectional interoperability between PHP and JavaScript code in a single process. Node/iojs >= 2.4.0 is currently required, since we use `NativeWeakMap`s in the implementation. This could probably be worked around using v8 hidden properties, but it doesn't seem worth it right now. # Usage ## Basic ```js var path = require('path'); var php = require('php-embed'); php.request({ file: path.join(__dirname, 'hello.php'), stream: process.stdout }).then(function(v) { console.log('php is done and stream flushed.'); }); ``` ## Advanced ```js var php = require('php-embed'); php.request({ source: ['call_user_func(function() {', ' class Foo {', ' var $bar = "bar";', ' }', ' $c = $_SERVER["CONTEXT"];', ' // Invoke an Async JS method', ' $result = $c->jsfunc(new Foo, $c->jsvalue, new Js\\Wait);', ' // And return the value back to JS.', ' return $result;', '})'].join('\n'), context: { jsvalue: 42, // Pass JS values to PHP jsfunc: function(foo, value, cb) { // Access PHP object from JS console.log(foo.bar, value); // Prints "bar 42" // Asynchronous completion, doesn't block node event loop setTimeout(function() { cb(null, "done") }, 500); } } }).then(function(v) { console.log(v); // Prints "done" ($result from PHP) }).done(); ``` ## Running command-line PHP scripts The `php-embed` package contains a binary which can be used as a drop-in replacement for the `php` CLI binary: ```sh npm install -g php-embed php-embed some-file.php argument1 argument2.... ``` Not every feature of the PHP CLI binary has been implemented; this is currently mostly a convenient testing tool. # API ## php.request(options, [callback]) Triggers a PHP "request", and returns a [`Promise`] which will be resolved when the request completes. If you prefer to use callbacks, you can ignore the return value and pass a callback as the second parameter. * `options`: an object containing various parameters for the request. Either `source` or `file` is mandatory; the rest are optional. - `source`: Specifies a source string to evaluate *as an expression* in the request context. (If you want to evaluate a statement, you can wrap it in [`call_user_func`]`(function () { ... })`.) - `file`: Specifies a PHP file to evaluate in the request context. - `stream`: A node [`stream.Writable`] to accept output from the PHP request. If not specified, defaults to `process.stdout`. - `request`: If an [`http.IncomingMessage`] is provided here, the PHP server variables will be set up with information about the request. - `args`: If an array with at least one element is provided, the PHP `$argc` and `$argv` variables will be set up as PHP CLI programs expect. Note that `args[0]` should be the "script file name", as in C convention. - `context`: A JavaScript object which will be made available to the PHP request in `$_SERVER['CONTEXT']`. - `serverInitFunc`: The user can provide a JavaScript function which will be passed an object containing values for the PHP [`$_SERVER`] variable, such as `REQUEST_URI`, `SERVER_ADMIN`, etc. You can add or override values in this function as needed to set up your request. * `callback` *(optional)*: A standard node callback. The first argument is non-null iff an exception was raised. The second argument is the result of the PHP evaluation, converted to a string. # PHP API From the PHP side, there are three new classes defined, all in the `Js` namespace, and one new property defined in the [`$_SERVER`] superglobal. ## `$_SERVER['CONTEXT']` This is the primary mechanism for passing data from the node process to the PHP request. You can pass over a reference to a JavaScript object, and populate it with whatever functions or data you wish to make available to the PHP code. ## class `Js\Object` This is the class which wraps JavaScript objects visible to PHP code. You can't create new objects of this type except by invoking JavaScript functions/methods/constructors. ## class `Js\Buffer` This class wraps a PHP string to indicate that it should be passed to JavaScript as a node `Buffer` object, instead of decoded to UTF-8 and converted to a JavaScript String. Assuming that a node-style Writable stream is made available to PHP as `$stream`, compare: ```php # The PHP string "abc" is decoded as UTF8 to form a JavaScript string, # which is then re-encoded as UTF8 and written to the stream: $stream.write("abc", "utf8"); # The PHP string "abc" is treated as a byte-stream and not de/encoded. $stream.write(new Js\Buffer("abc")); # Write to the stream synchronously (see description of next class) $stream.write(new Js\Buffer("abc"), new Js\Wait()); ``` ## class `Js\Wait` This class allows you to invoke asynchronous JavaScript functions from PHP code as if they were synchronous. You create a new instance of `Js\Wait` and pass that to the function where it would expect a standard node-style callback. For example, if the JavaScript `setTimeout` function were made available to PHP as `$setTimeout`, then: ```php $setTimeout(new Js\Wait, 5000); ``` would halt the PHP thread for 5 seconds. More usefully, if you were to make the node [`fs`] module available to PHP as `$fs`, then: ```php $contents = $fs.readFile('path/to/file', 'utf8', new Js\Wait); ``` would invoke the [`fs.readFile`] method asynchronously in the node context, but block the PHP thread until its callback was invoked. The result returned in the callback would then be used as the return value for the function invocation, resulting in `$contents` getting the result of reading the file. Note that calls using `Js\Wait` block the PHP thread but do not block the node thread. ## class `Js\ByRef` Arguments are passed to JavaScript functions by value, as is the default in PHP. This class allows you to pass arguments by reference; specifically array values (since objects are effectively passed by reference already, and it does not apply to primitive values like strings and integers). Given the following JavaScript function make available to PHP as `$jsfunc`: ```js function jsfunc(arr) { Array.prototype.push.call(arr, 4); } ``` You could call in from PHP as follows: ```php $a = array(1, 2, 3); $jsfunc($a); var_dump($a); # would still print (1, 2, 3) $jsfunc(new Js\ByRef($a)); var_dump($a); # now this would print (1, 2, 3, 4) ``` # Javascript API ## PHP objects The JavaScript `in` operator, when applied to a wrapped PHP object, works the same as the PHP [`isset()`] function. Similarly, when applied to a wrapped PHP object, JavaScript `delete` works like PHP [`unset()`]. ```js var php = require('php-embed'); php.request({ source: 'call_user_func(function() {' + ' class Foo { var $bar = null; var $bat = 42; } ' + ' $_SERVER["CONTEXT"](new Foo()); ' + '})', context: function(foo) { console.log("bar" in foo ? "yes" : "no"); // This prints "no" console.log("bat" in foo ? "yes" : "no"); // This prints "yes" } }).done(); ``` PHP has separate namespaces for properties and methods, while JavaScript has just one. Usually this isn't an issue, but if you need to you can use a leading `$` to specify a property, or `__call` to specifically invoke a method. ```js var php = require('php-embed'); php.request({ source: ['call_user_func(function() {', ' class Foo {', ' var $bar = "bar";', ' function bar($what) { echo "I am a ", $what, "!\n"; }', ' }', ' $foo = new Foo;', ' // This prints "bar"', ' echo $foo->bar, "\n";', ' // This prints "I am a function!"', ' $foo->bar("function");', ' // Now try it in JavaScript', ' $_SERVER["CONTEXT"]($foo);', '})'].join('\n'), context: function(foo) { // This prints "bar" console.log(foo.$bar); // This prints "I am a function" foo.__call("bar", "function"); } }).done(); ``` ## PHP arrays PHP arrays are a sort of fusion of JavaScript arrays and objects. They can store indexed data and have a sort of automatically-updated `length` property, like JavaScript arrays, but they can also store string keys like JavaScript objects. In JavaScript, we've decided to expose arrays as [array-like] [`Map`]s. That is, they have the [`get`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get), [`set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set), [`delete`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete), [`keys`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys), and [`size`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size) methods of [`Map`]. These work as you'd expect, and access *all* the values in the PHP array, with both indexed and string keys. In addition, as a convenience, they make the *indexed* keys (and only the indexed keys) available as properties directly on the object, and export an appropriate `length` field. This lets you use them directly in many JavaScript functions which accept "array-like" objects. For example, you can convert them easily to a "true" JavaScript array with [`Array.from`]. Arrays like objects are live-mapped: changes apply directly to the PHP object they wrap. However, note that arrays are by default passed *by value* to JavaScript functions; you may need to use `Js\ByRef` (see above) in order to have changes you make on the JavaScript side affect the value of a PHP variable. ## PHP ArrayAccess/Countable PHP objects which implement [`ArrayAccess`] and [`Countable`] are treated as PHP arrays, with the accessor methods described above. However note that the `length` property is fixed to `0` on these objects, since there's no way to get a count of only the indexed keys in the array ([`Countable`] gives the count of *all* the keys, counting both indexed and string keys). ## Blocking the JavaScript event loop At the moment, all property accesses and method invocations from JavaScript to PHP are done synchronously; that is, they block the JavaScript event loop. The mechanisms are in place for asynchronous access; I just haven't quite figured out what the syntax for that should look like. # Installing You can use [`npm`](https://github.com/isaacs/npm) to download and install: * The latest `php-embed` package: `npm install php-embed` * GitHub's `master` branch: `npm install https://github.com/cscott/node-php-embed/tarball/master` In both cases the module is automatically built with npm's internal version of `node-gyp`, and thus your system must meet [node-gyp's requirements](https://github.com/TooTallNate/node-gyp#installation). The prebuilt binaries are built using g++-5 on Linux, and so you will need to have the appropriate versions of the C++ standard library available. Something like `apt-get install g++-5` should suffice on Debian/Ubuntu. It is also possible to make your own build of `php-embed` from its source instead of its npm package ([see below](#building-from-the-source)). # Building from source Unless building via `npm install` you will need `node-pre-gyp` installed globally: npm install -g node-pre-gyp The `php-embed` module depends on the PHP embedding API. However, by default, an internal/bundled copy of `libphp5` will be built and statically linked, so an externally installed `libphp5` is not required. If you wish to install against an external `libphp5` then you need to pass the `--libphp5` argument to `node-pre-gyp` or `npm install`. node-pre-gyp --libphp5=external rebuild Or, using `npm`: npm install --libphp5=external If building against an external `libphp5` make sure to have the development headers available. If you don't have them installed, install the `-dev` package with your package manager, e.g. `apt-get install libphp5-embed php5-dev` for Debian/Ubuntu. Your external `libphp5` should have been built with thread-safety enabled (`ZTS` turned on). You will also need a C++11 compiler. We perform builds using clang-3.5 and g++-5; both of these are known to work. (Use `apt-get install g++-5` to install g++-5 if `g++ --version` reveals that you have an older version of `g++`.) To ensure that `npm`/`node-pre-gyp` use your preferred compiler, you may need to do something like: ```sh export CXX="g++-5" export CC="gcc-5" ``` On Mac OSX, you need to limit support to OS X 10.7 and above in order to get C++11 support. You will also need to install `libicu`. Something like the following should work: ```sh export MACOSX_DEPLOYMENT_TARGET=10.7 brew install icu4c ``` Developers hacking on the code will probably want to use: node-pre-gyp --debug build Passing the `--debug` flag to `node-pre-gyp` enables memory checking, and the `build` command (instead of `rebuild`) avoids rebuilding `libphp5` from scratch after every change. (You can also use `npm run debug-build` if you find that easier to remember.) # Testing To run the test suite, use: npm test This will run the JavaScript and C++ linters, as well as a test suite using [mocha](https://github.com/visionmedia/mocha). During development, `npm run jscs-fix` will automatically correct most JavaScript code style issues, and `npm run valgrind` will detect a large number of potential memory issues. Note that node itself will leak a small amount of memory from `node::CreateEnvironment`, `node::cares_wrap::Initialize`, and `node::Start`; these can safely be ignored in the `valgrind` report. # Contributors * [C. Scott Ananian](https://github.com/cscott) Many thanks to [Sara Golemon](https://github.com/sgolemon) without whose [book](http://www.amazon.com/Extending-Embedding-PHP-Sara-Golemon/dp/067232704X/) this project would have been impossible. # Related projects * [`mediawiki-express`](https://github.com/cscott/node-mediawiki-express) is an npm package which uses `php-embed` to run mediawiki inside a node.js [`express`](http://expressjs.com) server. * [`v8js`](https://github.com/preillyme/v8js) is a "mirror image" project: it embeds the v8 JavaScript engine inside of PHP, whereas `php-embed` embeds PHP inside node/v8. The author of `php-embed` is a contributor to `v8js` and they share bits of code. The JavaScript API to access PHP objects is deliberately similar to that used by `v8js`. * [`dnode-php`](https://github.com/bergie/dnode-php) is an RPC protocol implementation for Node and PHP, allowing calls between Node and PHP code running on separate servers. See also [`require-php`](https://www.npmjs.com/package/require-php), which creates the PHP server on the fly to provide a "single server" experience similar to that of `php-embed`. * [`exec-php`](https://www.npmjs.com/package/exec-php) is another clever embedding which uses the ability of the PHP CLI binary to execute a single function in order to first export the set of functions defined in a PHP file (using the `_exec_php_get_user_functions` built-in) and then to implement function invocation. # License Copyright (c) 2015 C. Scott Ananian. `php-embed` is licensed using the same [license](http://www.php.net/license/3_01.txt) as PHP itself. [`Promise`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise [`call_user_func`]: http://php.net/manual/en/function.call-user-func.php [`stream.Writable`]: https://nodejs.org/api/stream.html#stream_class_stream_writable [`http.IncomingMessage`]: https://nodejs.org/api/http.html#http_http_incomingmessage [`$_SERVER`]: http://php.net/manual/en/reserved.variables.server.php [`fs`]: https://nodejs.org/api/fs.html [`fs.readFile`]: https://nodejs.org/api/fs.html#fs_fs_readfile_filename_options_callback [`isset()`]: http://php.net/manual/en/function.isset.php [`unset()`]: http://php.net/manual/en/function.unset.php [`ArrayAccess`]: http://php.net/manual/en/class.arrayaccess.php [`Countable`]: http://php.net/manual/en/class.countable.php [array-like]: http://www.2ality.com/2013/05/quirk-array-like-objects.html [`Map`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map [`Array.from`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from [NPM1]: https://nodei.co/npm/php-embed.png [NPM2]: https://nodei.co/npm/php-embed/ [1]: https://travis-ci.org/cscott/node-php-embed.png [2]: https://travis-ci.org/cscott/node-php-embed [3]: https://david-dm.org/cscott/node-php-embed.png [4]: https://david-dm.org/cscott/node-php-embed [5]: https://david-dm.org/cscott/node-php-embed/dev-status.png [6]: https://david-dm.org/cscott/node-php-embed#info=devDependencies