Skip to content
Merged

1.2.0 #178

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/.ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [20.x, 22.x, 24.x]
php-versions: ['8.1', '8.2', '8.3', '8.4']
node-version: [22.x, 24.x, 26.x]
php-versions: ['8.2', '8.3', '8.4','8.5']
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v7
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 1.2.0

- Added `runCode` and `runCodeWithData` methods so PHP code can be executed from memory instead of from a file.
- Hardened input handling to prevent command injection.
- Altered thrown error message to include PHP error output instead of being written directly to stderr.
- Altered error thrown when failing to start PHP at all. It now reports the underlying spawn error instead of `PHP process exited with code undefined`.
- Added TypeScript definitions for `run` and `runWithData`, which were previously missing.
- Updated various dependencies.

## 1.1.0

- Added `run` and `runWithData` methods so this module can be used as a general purpose PHP runner.
Expand Down
32 changes: 30 additions & 2 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ To use this module, you must have PHP installed and in your PATH.

```javascript
const php = require('php')
const output = await php.run('some_php_script.php')
const output = php.run('some_php_script.php')
```

## Run a PHP script in Node.js and pass it data

```javascript
const php = require('php')
const output = await php.runWithData('some_php_script.php', { hello: 'world' })
const output = php.runWithData('some_php_script.php', { hello: 'world' })
```

Then, assuming your `some_php_script.php` file looks like this:
Expand All @@ -26,6 +26,34 @@ The output will be:
<p>world</p>
```

## Run PHP code from memory in Node.js

If you have PHP code in a string rather than in a file, you can execute it without writing it to disk first:

```javascript
const php = require('php')
const output = php.runCode('<?php echo "<p>world</p>"; ?>')
```

Like a PHP file, the code can mix HTML and PHP:

```javascript
const output = php.runCode('<?php $hello = "world"; ?><p><?=$hello?></p>')
```

## Run PHP code from memory in Node.js and pass it data

```javascript
const php = require('php')
const output = php.runCodeWithData('<p><?=$hello?></p>', { hello: 'world' })
```

The output will be:

```html
<p>world</p>
```

## Use with Express

```js
Expand Down
6 changes: 5 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
declare module "php" {
export async function __express(template: any, model: any, callback: any): Promise<any>;
export function run(script: string): string;
export function runCode(code: string): string;
export function runWithData(template: string, model?: any): string;
export function runCodeWithData(code: string, model?: any): string;
export function __express(template: any, model: any, callback: any): void;
export function disableRegisterGlobalModel(): void;
export function enableRegisterGlobalModel(): void;
}
54 changes: 38 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,48 @@
const path = require('path')
const { execFileSync } = require('child_process')
const { spawnSync } = require('child_process')
const settings = {}
settings.disableRegisterGlobalModel = false

function run (script) {
const scriptPath = path.join(__dirname, script)
try {
const stdout = execFileSync('php', [scriptPath])
return stdout.toString()
} catch (err) {
throw new Error(`PHP process exited with code ${err.status}`)
function execPhp (args, input) {
const result = spawnSync('php', args, { input })

// php could not be started at all, e.g. it is not installed or not in the PATH
if (result.error) throw result.error

// on failure fold php's diagnostics into the error rather than letting them leak to stderr
if (result.status !== 0) {
const stderr = result.stderr.toString().trim()
throw new Error(`PHP process exited with code ${result.status}${stderr ? `\n${stderr}` : ''}`)
}

// on success php may still have emitted warnings or notices, so pass those along
if (result.stderr.length) process.stderr.write(result.stderr)

return result.stdout.toString()
}

function run (script) {
return execPhp([path.join(__dirname, script)])
}

function runCode (code) {
// php reads the script from stdin when it is given no file argument
return execPhp([], code)
}

function runWithData (template, model) {
if (!model) model = {}
model._TEMPLATE = template
return runLoader(model)
}

function runCodeWithData (code, model) {
if (!model) model = {}
model._TEMPLATE_SOURCE = code
return runLoader(model)
}

function runLoader (model) {
if (typeof model._REGISTER_GLOBAL_MODEL === 'undefined') {
if (settings.disableRegisterGlobalModel) {
model._REGISTER_GLOBAL_MODEL = false
Expand All @@ -27,14 +54,7 @@ function runWithData (template, model) {
model._VIEWS_PATH = model?.settings?.views || './'
const jsonModel = JSON.stringify(model, circular())

try {
const stdout = execFileSync('php', [path.join(__dirname, '/loader.php')], {
input: jsonModel
})
return stdout.toString()
} catch (err) {
throw new Error(`PHP process exited with code ${err.status}`)
}
return execPhp([path.join(__dirname, '/loader.php')], jsonModel)
}

function __express (template, model, callback) {
Expand Down Expand Up @@ -74,7 +94,9 @@ function circular (ref, methods) {
}

module.exports.run = run
module.exports.runCode = runCode
module.exports.runWithData = runWithData
module.exports.runCodeWithData = runCodeWithData
module.exports.__express = __express
module.exports.disableRegisterGlobalModel = disableRegisterGlobalModel
module.exports.enableRegisterGlobalModel = enableRegisterGlobalModel
10 changes: 8 additions & 2 deletions loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,11 @@
// add express templates path to php includes path
set_include_path($model->_VIEWS_PATH);

// render the template
include "$model->_TEMPLATE";
// render the template, either from source held in memory or from a file
if (isset($model->_TEMPLATE_SOURCE)) {
// the closing tag prefix drops into inline html mode so the source parses like a template file would
// the newline after it is padding: php swallows one newline following a closing tag, so this absorbs the swallow and leaves leading whitespace in the source intact
eval('?>' . "\n" . $model->_TEMPLATE_SOURCE);
} else {
include "$model->_TEMPLATE";
}
Loading