Skip to content

Add initial rewriting framework #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 10, 2025
Merged
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
105 changes: 104 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ console.log(response.body.toString())
* `docroot` {String} Document root for PHP. **Default:** process.cwd()
* `throwRequestErrors` {Boolean} Throw request errors rather than returning
responses with error codes. **Default:** false
* `rewriter` {Rewriter} Optional rewrite rules. **Default:** `undefined`
* Returns: {Php}

Construct a new PHP instance to which to dispatch requests.
Expand Down Expand Up @@ -560,9 +561,111 @@ headers.forEach((value, name, headers) => {
})
```

### `new Rewriter(input)`

* `rules` {Array} The set of rewriting rules to apply to each request
* `operation` {String} Operation type (`and` or `or`) **Default:** `and`
* `conditions` {Array} Conditions to match against the request
* `type` {String} Condition type
* `args` {String} Arguments to pass to condition constructor
* `rewriters` {Array} Rewrites to apply if the conditions match
* `type` {String} Rewriter type
* `args` {String} Arguments to pass to rewriter constructor
* Returns: {Rewriter}

Construct a Rewriter to rewrite requests before they are dispatched to PHP.

```js
import { Rewriter } from '@platformatic/php-node'

const rewriter = new Rewriter([{
conditions: [{
type: 'header',
args: ['User-Agent', '^(Mozilla|Chrome)']
}],
rewriters: [{
type: 'path',
args: ['^/old-path/(.*)$', '/new-path/$1']
}]
}])
```

#### Conditions

There are several types of conditions which may be used to match against the
request. Each condition type has a set of arguments which are passed to the
constructor of the condition. The condition will be evaluated against the
request and if it matches, the rewriters will be applied.

The available condition types are:

- `exists` Matches if request path exists in docroot.
- `not_exists` Matches if request path does not exist in docroot.
- `header(name, pattern)` Matches named header against a pattern.
- `name` {String} The name of the header to match.
- `pattern` {String} The regex pattern to match against the header value.
- `method(pattern)` Matches request method against a pattern.
- `pattern` {String} The regex pattern to match against the HTTP method.
- `path(pattern)`: Matches request path against a pattern.
- `pattern` {String} The regex pattern to match against the request path.

#### Rewriters

There are several types of rewriters which may be used to rewrite the request
before it is dispatched to PHP. Each rewriter type has a set of arguments which
are passed to the constructor of the rewriter. The rewriter will be applied to
the request if the conditions match.

The available rewriter types are:

- `header(name, replacement)` Sets a named header to a given replacement.
- `name` {String} The name of the header to set.
- `replacement` {String} The replacement string to use for named header.
- `href(pattern, replacement)` Rewrites request path, query, and fragment to
given replacement.
- `pattern` {String} The regex pattern to match against the request path.
- `replacement` {String} The replacement string to use for request path.
- `method(replacement)` Sets the request method to a given replacement.
- `replacement` {String} The replacement string to use for request method.
- `path(pattern, replacement)` Rewrites request path to given replacement.
- `pattern` {String} The regex pattern to match against the request path.
- `replacement` {String} The replacement string to use for request path.

### `rewriter.rewrite(request, docroot)`

- `request` {Object} The request object.
- `docroot` {String} The document root.

Rewrites the given request using the rules provided to the rewriter.

This is mainly exposed for testing purposes. It is not recommended to use
directly. Rather, the `rewriter` should be provided to the `Php` constructor
to allow rewriting to occur within the PHP environment where it will be aware
of the original `REQUEST_URI` state.

```js
import { Rewriter } from '@platformatic/php-node'

const rewriter = new Rewriter([{
rewriters: [{
type: 'path',
args: ['^(.*)$', '/base/$1'
}]
}])

const request = new Request({
url: 'http://example.com/foo/bar'
})

const modified = rewriter.rewrite(request, import.meta.dirname)

console.log(modified.url) // http://example.com/base/foo/bar
```

## Contributing

This project is part of the [Platformatic](https://github.com/platformatic) ecosystem. Please refer to the main repository for contribution guidelines.
This project is part of the [Platformatic](https://github.com/platformatic)
ecosystem. Please refer to the main repository for contribution guidelines.

## License

Expand Down
34 changes: 34 additions & 0 deletions __test__/handler.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import test from 'ava'
import { Php, Request } from '../index.js'

import { MockRoot } from './util.mjs'
import { Rewriter } from '../index.js'

test('Support input/output streams', async (t) => {
const mockroot = await MockRoot.from({
Expand Down Expand Up @@ -167,3 +168,36 @@ test('Allow receiving true errors', async (t) => {
message: /^Script not found: .*\/index\.php$/
}, 'should throw error')
})

test('Accept rewriter', async (t) => {
const mockroot = await MockRoot.from({
'index.php': '<?php echo "Hello, World!"; ?>'
})
t.teardown(() => mockroot.clean())

const rewriter = new Rewriter([
{
conditions: [
{ type: 'path', args: ['^/rewrite_me$'] }
],
rewriters: [
{ type: 'path', args: ['^/rewrite_me$', '/index.php'] }
]
}
])

const php = new Php({
argv: process.argv,
docroot: mockroot.path,
throwRequestErrors: true,
rewriter
})

const req = new Request({
url: 'http://example.com/rewrite_me'
})

const res = await php.handleRequest(req)
t.is(res.status, 200)
t.is(res.body.toString('utf8'), 'Hello, World!')
})
Loading
Loading