├── .github └── workflows │ └── .ci.yml ├── .gitignore ├── CHANGELOG.md ├── CONFIGURATION.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── USAGE.md ├── index.d.ts ├── index.js ├── loader.php ├── package-lock.json ├── package.json ├── renovate.json └── test ├── lib ├── _expressSetup.js └── templates │ ├── basicTest.php │ ├── codingError.php │ ├── registerGlobalModelTest.php │ └── selfContainedTest.php └── test.js /.github/workflows/.ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: CI 3 | jobs: 4 | test: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | node-version: [20.x, 22.x, 24.x] 9 | php-versions: ['8.1', '8.2', '8.3', '8.4'] 10 | os: [ubuntu-latest, macos-latest, windows-latest] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - name: Setup PHP Action 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: ${{ matrix.php-versions }} 21 | ini-values: short_open_tag=On, error_reporting=E_ALL & ~E_NOTICE 22 | - run: npm ci 23 | - run: npm run lint 24 | - run: npm run coverage 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Linux ### 2 | *~ 3 | 4 | # temporary files which can be created if a process still has a handle open of a deleted file 5 | .fuse_hidden* 6 | 7 | # KDE directory preferences 8 | .directory 9 | 10 | # Linux trash folder which might appear on any partition or disk 11 | .Trash-* 12 | 13 | # .nfs files are created when an open file is removed but is still being accessed 14 | .nfs* 15 | 16 | ### macOS ### 17 | # General 18 | .DS_Store 19 | .AppleDouble 20 | .LSOverride 21 | 22 | # Icon must end with two \r 23 | Icon 24 | 25 | # Thumbnails 26 | ._* 27 | 28 | # Files that might appear in the root of a volume 29 | .DocumentRevisions-V100 30 | .fseventsd 31 | .Spotlight-V100 32 | .TemporaryItems 33 | .Trashes 34 | .VolumeIcon.icns 35 | .com.apple.timemachine.donotpresent 36 | 37 | # Directories potentially created on remote AFP share 38 | .AppleDB 39 | .AppleDesktop 40 | Network Trash Folder 41 | Temporary Items 42 | .apdisk 43 | 44 | ### Node ### 45 | # Logs 46 | logs 47 | *.log 48 | npm-debug.log* 49 | yarn-debug.log* 50 | yarn-error.log* 51 | lerna-debug.log* 52 | 53 | # Diagnostic reports (https://nodejs.org/api/report.html) 54 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 55 | 56 | # Runtime data 57 | pids 58 | *.pid 59 | *.seed 60 | *.pid.lock 61 | 62 | # Directory for instrumented libs generated by jscoverage/JSCover 63 | lib-cov 64 | 65 | # Coverage directory used by tools like istanbul 66 | coverage 67 | *.lcov 68 | 69 | # nyc test coverage 70 | .nyc_output 71 | 72 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 73 | .grunt 74 | 75 | # Bower dependency directory (https://bower.io/) 76 | bower_components 77 | 78 | # node-waf configuration 79 | .lock-wscript 80 | 81 | # Compiled binary addons (https://nodejs.org/api/addons.html) 82 | build/Release 83 | 84 | # Dependency directories 85 | node_modules/ 86 | jspm_packages/ 87 | 88 | # TypeScript v1 declaration files 89 | typings/ 90 | 91 | # TypeScript cache 92 | *.tsbuildinfo 93 | 94 | # Optional npm cache directory 95 | .npm 96 | 97 | # Optional eslint cache 98 | .eslintcache 99 | 100 | # Optional REPL history 101 | .node_repl_history 102 | 103 | # Output of 'npm pack' 104 | *.tgz 105 | 106 | # Yarn Integrity file 107 | .yarn-integrity 108 | 109 | # dotenv environment variables file 110 | .env 111 | .env.test 112 | 113 | # parcel-bundler cache (https://parceljs.org/) 114 | .cache 115 | 116 | # next.js build output 117 | .next 118 | 119 | # nuxt.js build output 120 | .nuxt 121 | 122 | # rollup.js default build output 123 | dist/ 124 | 125 | # Uncomment the public line if your project uses Gatsby 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 128 | # public 129 | 130 | # Storybook build outputs 131 | .out 132 | .storybook-out 133 | 134 | # vuepress build output 135 | .vuepress/dist 136 | 137 | # Serverless directories 138 | .serverless/ 139 | 140 | # FuseBox cache 141 | .fusebox/ 142 | 143 | # DynamoDB Local files 144 | .dynamodb/ 145 | 146 | # Temporary folders 147 | tmp/ 148 | temp/ 149 | 150 | ### Windows ### 151 | # Windows thumbnail cache files 152 | Thumbs.db 153 | Thumbs.db:encryptable 154 | ehthumbs.db 155 | ehthumbs_vista.db 156 | 157 | # Dump file 158 | *.stackdump 159 | 160 | # Folder config file 161 | [Dd]esktop.ini 162 | 163 | # Recycle Bin used on file shares 164 | $RECYCLE.BIN/ 165 | 166 | # Windows Installer files 167 | *.cab 168 | *.msi 169 | *.msix 170 | *.msm 171 | *.msp 172 | 173 | # Windows shortcuts 174 | *.lnk 175 | 176 | .npm-cache 177 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.0 2 | 3 | - Added `run` and `runWithData` methods so this module can be used as a general purpose PHP runner. 4 | - Updated various dependencies. 5 | 6 | ## 1.0.2 7 | 8 | - Added TypeScript definitions. 9 | - Various dependencies bumped. 10 | 11 | ## 1.0.1 12 | 13 | - Renamed package from `express-php-view-engine` to `php`. Thanks to Elmer Bulthuis for transferring the package name. 14 | - Various dependencies bumped. 15 | 16 | ## 1.0.0 17 | 18 | - Initial version of superseding project. 19 | - `express-php-view-engine` notably does not attempt to finish the work of the old `php` module. Instead it allows the native PHP parser to execute as a child process within Express applications. 20 | 21 | ## 0.0.1 22 | 23 | - Initial version of original project. 24 | - Originally the `php` module on npm was used by a separate project that attempted to implement a PHP parser in JavaScript for Node.js, but was never finished. One work in progress version was published, then the project remained stale for 8 years. 25 | -------------------------------------------------------------------------------- /CONFIGURATION.md: -------------------------------------------------------------------------------- 1 | This module will register values from the data model you pass to the PHP script as global variables in your PHP script by default when you use PHP as an Express view engine or when you call `runWithData`. You can disable this behavior if desired in the following ways: 2 | 3 | Disable registering globally: 4 | 5 | ```js 6 | const php = require('php') 7 | php.disableRegisterGlobalModel() 8 | // can be reenabled by calling php.enableRegisterGlobalModel() 9 | ``` 10 | 11 | Disable registering on a per render basis in Express: 12 | 13 | ```js 14 | app.get('/', (req, res) => { 15 | res.render('index.php', { 16 | _REGISTER_GLOBAL_MODEL: false, 17 | hello: 'world' 18 | }) 19 | }) 20 | ``` 21 | 22 | Disable registering on a per render basis in `runWithData` (though if you're doing this, you probably should just use `php.run()` instead, as that method was written to use simpler logic that doesn't support passing data to PHP): 23 | 24 | ```js 25 | const output = await php.runWithData('some_php_script.php', { 26 | _REGISTER_GLOBAL_MODEL: false, 27 | hello: 'world' 28 | }) 29 | ``` 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Before opening a pull request 4 | 5 | - Be sure all tests pass: `npm t`. 6 | - Ensure 100% code coverage and write new tests if necessary: `npm run coverage`. 7 | - Add your changes to `CHANGELOG.md`. 8 | 9 | ## Release process 10 | 11 | If you are a maintainer of this module, please follow the following release procedure: 12 | 13 | - Merge all desired pull requests into master. 14 | - Bump `package.json` to a new version and run `npm i` to generate a new `package-lock.json`. 15 | - Alter CHANGELOG "Next version" section and stamp it with the new version. 16 | - Paste contents of CHANGELOG into new version commit. 17 | - Open and merge a pull request with those changes. 18 | - Tag the merge commit as the a new release version number. 19 | - Publish commit to npm. 20 | 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | License 2 | === 3 | 4 | All original code in this project is licensed under the [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/). Commercial and noncommercial use is permitted with attribution. 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 🐘 **php** [![npm](https://img.shields.io/npm/v/php.svg)](https://www.npmjs.com/package/php) 2 | 3 | This module allows you to run [PHP](https://php.net) code in Node.js in various ways: 4 | 5 | - Run PHP scripts. 6 | - Run PHP scripts and pass them JSON data from Node.js. 7 | - Use PHP as a view engine (templating system) for [Express framework](https://expressjs.com) applications. 8 | 9 | This module was built and is maintained by the [Roosevelt web framework](https://rooseveltframework.org) [team](https://rooseveltframework.org/contributors), but it can be used independently of Roosevelt as well. 10 | 11 |
12 | Documentation 13 | 17 |
18 | -------------------------------------------------------------------------------- /USAGE.md: -------------------------------------------------------------------------------- 1 | To use this module, you must have PHP installed and in your PATH. 2 | 3 | ## Run a PHP script in Node.js 4 | 5 | ```javascript 6 | const php = require('php') 7 | const output = await php.run('some_php_script.php') 8 | ``` 9 | 10 | ## Run a PHP script in Node.js and pass it data 11 | 12 | ```javascript 13 | const php = require('php') 14 | const output = await php.runWithData('some_php_script.php', { hello: 'world' }) 15 | ``` 16 | 17 | Then, assuming your `some_php_script.php` file looks like this: 18 | 19 | ```php 20 |

21 | ``` 22 | 23 | The output will be: 24 | 25 | ```html 26 |

world

27 | ``` 28 | 29 | ## Use with Express 30 | 31 | ```js 32 | const express = require('express') 33 | const app = express() 34 | const php = require('php') 35 | 36 | // setup PHP templating engine 37 | app.set('views', path.join(__dirname, 'templates')) 38 | app.set('view engine', 'php') // set PHP as a view engine in your Express app 39 | app.engine('php', php.__express) 40 | 41 | // define a route 42 | app.get('/', (req, res) => { 43 | res.render('index.php', { 44 | hello: 'world' 45 | }) 46 | }) 47 | ``` 48 | 49 | Then, assuming your `templates/index.php` looks like this: 50 | 51 | ```php 52 |

53 | ``` 54 | 55 | The output will be: 56 | 57 | ```html 58 |

world

59 | ``` 60 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "php" { 2 | export async function __express(template: any, model: any, callback: any): Promise; 3 | export function disableRegisterGlobalModel(): void; 4 | export function enableRegisterGlobalModel(): void; 5 | } 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { execSync } = require('child_process') 3 | const settings = {} 4 | settings.disableRegisterGlobalModel = false 5 | 6 | function run (script) { 7 | try { 8 | const stdout = execSync(`php ${path.join(__dirname, script)}`) 9 | return stdout.toString() 10 | } catch (err) { 11 | throw new Error(`PHP process exited with code ${err.status}`) 12 | } 13 | } 14 | 15 | function runWithData (template, model) { 16 | if (!model) model = {} 17 | model._TEMPLATE = template 18 | if (typeof model._REGISTER_GLOBAL_MODEL === 'undefined') { 19 | if (settings.disableRegisterGlobalModel) { 20 | model._REGISTER_GLOBAL_MODEL = false 21 | } else { 22 | model._REGISTER_GLOBAL_MODEL = true 23 | } 24 | } 25 | model._REGISTER_GLOBAL_MODEL = !!model._REGISTER_GLOBAL_MODEL 26 | model._VIEWS_PATH = model?.settings?.views || './' 27 | const jsonModel = JSON.stringify(model, circular()) 28 | 29 | try { 30 | const stdout = execSync(`php ${path.join(__dirname, '/loader.php')}`, { 31 | input: jsonModel 32 | }) 33 | return stdout.toString() 34 | } catch (err) { 35 | throw new Error(`PHP process exited with code ${err.status}`) 36 | } 37 | } 38 | 39 | function __express (template, model, callback) { 40 | try { 41 | const stdout = runWithData(template, model) 42 | callback(null, stdout) 43 | } catch (err) { 44 | callback(err) 45 | } 46 | } 47 | 48 | function disableRegisterGlobalModel () { 49 | settings.disableRegisterGlobalModel = true 50 | } 51 | 52 | function enableRegisterGlobalModel () { 53 | settings.disableRegisterGlobalModel = false 54 | } 55 | 56 | function circular (ref, methods) { 57 | ref = ref || '[Circular]' 58 | const seen = [] 59 | return function (key, val) { 60 | if (typeof val === 'function' && methods) { 61 | val = val.toString() 62 | } 63 | if (!val || typeof (val) !== 'object') { 64 | return val 65 | } 66 | if (~seen.indexOf(val)) { 67 | if (typeof ref === 'function') return ref(val) 68 | return ref 69 | } 70 | seen.push(val) 71 | return val 72 | } 73 | } 74 | 75 | module.exports.run = run 76 | module.exports.runWithData = runWithData 77 | module.exports.__express = __express 78 | module.exports.disableRegisterGlobalModel = disableRegisterGlobalModel 79 | module.exports.enableRegisterGlobalModel = enableRegisterGlobalModel 80 | -------------------------------------------------------------------------------- /loader.php: -------------------------------------------------------------------------------- 1 | _REGISTER_GLOBAL_MODEL) { 7 | foreach ($model as $key => $value) { 8 | $$key = $value; 9 | } 10 | } 11 | 12 | // add express templates path to php includes path 13 | set_include_path($model->_VIEWS_PATH); 14 | 15 | // render the template 16 | include "$model->_TEMPLATE"; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php", 3 | "description": "🐘 Allows you to run PHP code in Node.js in various ways.", 4 | "author": "Roosevelt Framework Team ", 5 | "contributors": [ 6 | { 7 | "name": "Contributors", 8 | "url": "https://github.com/rooseveltframework/node-php-runner/graphs/contributors" 9 | } 10 | ], 11 | "version": "1.1.0", 12 | "files": [ 13 | "index.d.ts", 14 | "index.js", 15 | "loader.php", 16 | "*.md" 17 | ], 18 | "homepage": "https://rooseveltframework.org/docs/node-php-runner/latest", 19 | "license": "CC-BY-4.0", 20 | "main": "index.js", 21 | "readmeFilename": "README.md", 22 | "engines": { 23 | "node": ">=18.0.0" 24 | }, 25 | "dependencies": { 26 | }, 27 | "devDependencies": { 28 | "ava": "6.2.0", 29 | "c8": "10.1.3", 30 | "express": "4.21.2", 31 | "standard": "17.1.2", 32 | "supertest": "7.0.0" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git://github.com/rooseveltframework/node-php-runner.git" 37 | }, 38 | "keywords": [ 39 | "Express", 40 | "Express.js", 41 | "php", 42 | "view engine" 43 | ], 44 | "scripts": { 45 | "coverage": "c8 --reporter=text --reporter=lcov ava", 46 | "lint": "standard", 47 | "test": "ava" 48 | }, 49 | "funding": "https://www.paypal.com/donate/?hosted_button_id=2L2X8GRXZCGJ6" 50 | } 51 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":preserveSemverRanges", 5 | ":ignoreUnstable", 6 | ":respectLatest", 7 | ":disableDependencyDashboard" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test/lib/_expressSetup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Setup an Express app to test against 3 | */ 4 | 5 | const express = require('express') 6 | const path = require('path') 7 | 8 | module.exports = () => { 9 | // init express app 10 | const app = express() 11 | const php = require('../..') 12 | 13 | // setup php templating engine 14 | app.set('views', path.join(__dirname, 'templates')) 15 | app.set('view engine', 'php') 16 | app.engine('php', php.__express) 17 | 18 | // route to test the default settings 19 | app.get('/defaults', (req, res) => { 20 | res.render('basicTest', { 21 | hello: 'world' 22 | }) 23 | }) 24 | 25 | // route to test the default settings with a callback function 26 | app.get('/defaultsWithCallbackFunction', (req, res) => { 27 | res.render('basicTest', { 28 | hello: 'world' 29 | }, (err, html) => { 30 | if (err) return 31 | res.send(html) 32 | }) 33 | }) 34 | 35 | // route to test parsing a template with a coding error 36 | app.get('/codingError', (req, res) => { 37 | res.render('codingError', { 38 | hello: 'world' 39 | }) 40 | }) 41 | 42 | // route to test disabling registering the model as globals 43 | app.get('/disableRegisterGlobalModelAtModelLevel', (req, res) => { 44 | res.render('registerGlobalModelTest', { 45 | _REGISTER_GLOBAL_MODEL: false, 46 | hello: 'world' 47 | }) 48 | }) 49 | 50 | // route to test disabling registering the model as globals 51 | app.get('/disableRegisterGlobalModelGlobally', (req, res) => { 52 | php.disableRegisterGlobalModel() 53 | res.render('registerGlobalModelTest', { 54 | hello: 'world' 55 | }) 56 | }) 57 | 58 | // route to test disabling registering the model as globals 59 | app.get('/disableRegisterGlobalModelGloballyThenReenabled', (req, res) => { 60 | php.disableRegisterGlobalModel() 61 | php.enableRegisterGlobalModel() 62 | res.render('registerGlobalModelTest', { 63 | hello: 'world' 64 | }) 65 | }) 66 | 67 | return app 68 | } 69 | -------------------------------------------------------------------------------- /test/lib/templates/basicTest.php: -------------------------------------------------------------------------------- 1 |

2 | -------------------------------------------------------------------------------- /test/lib/templates/codingError.php: -------------------------------------------------------------------------------- 1 | 4 |

5 | -------------------------------------------------------------------------------- /test/lib/templates/registerGlobalModelTest.php: -------------------------------------------------------------------------------- 1 |

hello?>

2 | 3 | -------------------------------------------------------------------------------- /test/lib/templates/selfContainedTest.php: -------------------------------------------------------------------------------- 1 | 4 |

5 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const php = require('../index') 2 | const request = require('supertest') 3 | const test = require('ava') 4 | const expressSetup = require('./lib/_expressSetup') 5 | 6 | test.before(t => { 7 | const app = expressSetup() 8 | 9 | t.context.app = app 10 | t.context.server = app.listen('43711') 11 | }) 12 | 13 | test.after(t => { 14 | t.context.server.close() 15 | }) 16 | 17 | console.log('General tests:\n') 18 | 19 | test.serial('Executing a PHP script with `run`', async t => { 20 | const res = await php.run('./test/lib/templates/selfContainedTest.php') 21 | t.true(res.includes('

world

')) 22 | }) 23 | 24 | test.serial('Executing a PHP script with `runWithData` and passing it some data', async t => { 25 | const res = await php.runWithData('./test/lib/templates/basicTest.php', { hello: 'world' }) 26 | t.true(res.includes('

world

')) 27 | }) 28 | 29 | test.serial('Executing a PHP script that has a coding error with `run`', async t => { 30 | try { 31 | await php.run('./test/lib/templates/codingError.php') 32 | } catch (e) { 33 | t.true(e.message.includes('PHP process exited with code 255')) 34 | } 35 | }) 36 | 37 | test.serial('Executing a PHP script that has a coding error with `runWithData`', async t => { 38 | try { 39 | await php.runWithData('./test/lib/templates/codingError.php') 40 | } catch (e) { 41 | t.true(e.message.includes('PHP process exited with code 255')) 42 | } 43 | }) 44 | 45 | console.log('\nExpress server tests:\n') 46 | 47 | test.serial('Passing a model variable down from Express to PHP and getting it to render as a registered global', async t => { 48 | const res = await request(t.context.app).get('/defaults') 49 | t.true(res.text.includes('

world

')) 50 | }) 51 | 52 | test.serial('Passing a model variable down from Express to PHP and getting it to render as a registered global, then calling a callback function', async t => { 53 | const res = await request(t.context.app).get('/defaultsWithCallbackFunction') 54 | t.true(res.text.includes('

world

')) 55 | }) 56 | 57 | test.serial('Passing a model variable down from Express to a PHP template that has a coding error', async t => { 58 | const res = await request(t.context.app).get('/codingError') 59 | t.true(res.text.includes('Error: PHP process exited with code 255')) 60 | }) 61 | 62 | test.serial('Passing a model down from Express to PHP with the _REGISTER_GLOBAL_MODEL feature disabled at the model level', async t => { 63 | const res = await request(t.context.app).get('/disableRegisterGlobalModelAtModelLevel') 64 | t.true(res.text.includes('

world

')) 65 | }) 66 | 67 | test.serial('Passing a model down from Express to PHP with the _REGISTER_GLOBAL_MODEL feature disabled globally', async t => { 68 | const res = await request(t.context.app).get('/disableRegisterGlobalModelGlobally') 69 | t.true(res.text.includes('

world

')) 70 | }) 71 | 72 | test.serial('Passing a model down from Express to PHP with the _REGISTER_GLOBAL_MODEL feature disabled globally then reenabled', async t => { 73 | const res = await request(t.context.app).get('/disableRegisterGlobalModelGloballyThenReenabled') 74 | t.true(res.text.includes('

world

world

')) 75 | }) 76 | --------------------------------------------------------------------------------