Use Vite's speed, ecosystem and tooling to build with PHP.
// vite.config.js
import { defineConfig } from 'vite';
import usePHP from 'vite-plugin-php';
export default defineConfig({
plugins: [usePHP()],
});NPM | Wiki | Discussions | Starter-Repo
Plugin now fully utilizes the Vite pipeline to load, transform and HTML-transform files in proper order.
<!-- index.php -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div id="root">
<?= 'Render some text with PHP!' ?>
</div>
<?php if (isset($_GET['show_hello'])): ?>
Hello world!
<?php endif; ?>
<script src="./src/react-app.tsx" type="module"></script>
</body>
</html>The plugin will serve you the processed index.php as usual, including all imported and preprocessed files that are supported by Vite and other loaders.
The configuration takes following properties:
type UsePHPConfig = {
binary?: string;
// Override default PHP server host address. Default is `localhost`.
php?: { host?: string };
entry?: string | string[];
rewriteUrl?: (requestUrl: URL) => URL | undefined;
tempDir?: string;
dev?: {
// Takes on either a bitmask, or named constants EPHPError.
// Default is EPHPError.ALL | EPHPError.STRICT.
errorLevels?: number;
// Cleanup temporary files on shutdown. Default is `true`.
cleanup?: boolean;
};
};
// Detailed description on https://www.php.net/manual/en/errorfunc.constants.php
const EPHPError = {
ERROR: 1,
WARNING: 2,
PARSE: 4,
NOTICE: 8,
CORE_ERROR: 16,
CORE_WARNING: 32,
COMPILE_ERROR: 64,
COMPILE_WARNING: 128,
USER_ERROR: 256,
USER_WARNING: 512,
USER_NOTICE: 1024,
STRICT: 2048,
RECOVERABLE_ERROR: 4096,
DEPRECATED: 8192,
USER_DEPRECATED: 16384,
ALL: 32767,
};By default the plugin tries to access the system php binary, load index.php as the main entry point and writes temporary PHP files to .php-tmp.
However you have the possibility to use an other binary or even compile multiple entry-points:
usePHP({
binary: '/opt/lampp/bin/php-8.1.10',
entry: ['index.php', 'index_alt.php', 'pages/contact.php'],
});Should you have multiple entry-points, you will be able to access each one according to this chart:
| Entry file | Accessible routes | Build output |
|---|---|---|
index.php |
/ /index /index.php |
dist/index.php |
about.php |
/about /about.php |
dist/about.php |
about/details.php |
/about/details /about/details.php |
dist/about/details.php |
contact.php |
/contact /contact.php |
dist/contact.php |
shop/index.php |
/shop/ /shop/index.php |
dist/shop/index.php |
pages/blog/post.php |
/pages/blog/post /pages/blog/post.php |
dist/pages/blog/post.php |
| ... | ... | ... |
You can also specify wildcard entry points:
usePHP({
binary: '/opt/lampp/bin/php-8.1.10',
entry: [
'index.php',
'about.php',
'contact.php',
'pages/**/*.php',
'partials/*.php',
],
});These entries will also render according to the routing table above.
If you are using some sort of Apache mod_rewrite magic or nginx rewrite rules you can simulate them with the rewriteUrl property.
The rewriteUrl function has one parameter — the requested URL given as a URL object — and returns either a modified URL object or undefined:
usePHP({
entry: ['index.php', 'partials/**/*.php'],
rewriteUrl(requestUrl) {
if (['.js', '.css'].some((s) => requestUrl.pathname.includes(s))) {
return;
}
requestUrl.search = '_request_=' + requestUrl.pathname;
requestUrl.pathname = 'index.php';
return requestUrl;
},
});rewriteUrl you must exclude static assets like CSS, JavaScript, images and fonts by returning undefined, otherwise the plugin tries to send them through PHP.
Since version 2.0.4 it is possible to point to an external origin. Make sure the changed URL points to a different origin:
usePHP({
rewriteUrl(requestUrl) {
if (requestUrl.pathname.startsWith('/media/')) {
return new URL(
'https://nititech.de' +
requestUrl.toString().substring(requestUrl.origin.length),
);
}
},
});When the rewritten URL points to a different origin, the plugin responds with a 307 Temporary Redirect.
Just like in native PHP you can specify what errors you want to see:
// vite.config.js
import { defineConfig } from 'vite';
import usePHP, { EPHPError } from 'vite-plugin-php';
export default defineConfig({
plugins: [
usePHP({
dev: {
errorLevels:
EPHPError.ERROR | EPHPError.WARNING | EPHPError.STRICT,
},
}),
],
});This log will be printed into your console, just like any other message about what is happening in Vite.
For more details about the meaning of the error level constants, visit the original PHP-documentation.
With additional Vite-plugins you can now alter the outcome of the transformIndexHtml() pipeline.
You can either apply modifications
// vite.config.ts
...,
plugins: [
{
name: 'pre-transform',
transformIndexHtml: {
order: 'pre',
handler(html, ctx) {
return html.replace(
'</body>',
'<div><?= "Pre PHP transform"; ?></div></body>',
);
},
},
},
usePHP(),
],
...This adds a hook to run between loading the PHP code and unescaping PHP fragments, prior to passing everything further to Vite's own HTML transformation magic.
// vite.config.ts
...,
plugins: [
usePHP(),
{
name: 'post-transform',
transformIndexHtml(html, ctx) {
return html.replace(
'</body>',
'<div><?= "Post PHP transform"; ?></div></body>',
);
},
},
],
...or
// vite.config.ts
...,
plugins: [
usePHP(),
{
name: 'post-transform',
transformIndexHtml: {
order: 'post',
handler(html, ctx) {
return html.replace(
'</body>',
'<div><?= "Post PHP transform"; ?></div></body>',
);
},
},
},
],
...This hook runs right after Vite's transformations and unescaping PHP fragments.
With additional Vite plugins you can alter the outcome of the transformIndexHtml() pipeline.
The plugin temporarily replaces each PHP fragment with a unique token, lets Vite do its work, then restores the original code.
You can place your own Vite plugins in three useful spots:
- Before
usePHP()withtransformIndexHtml.order: 'pre'— inject or modify PHP before the plugin escapes it. - After
usePHP()with plaintransformIndexHtml— modify HTML after Vite transforms but before PHP is restored. - After
usePHP()withtransformIndexHtml.order: 'post'— modify the final HTML after PHP is restored.
// vite.config.ts
...,
plugins: [
{
name: 'pre-transform',
transformIndexHtml: {
order: 'pre',
handler(html, ctx) {
return html.replace(
'</body>',
'<div><?= "Pre PHP transform"; ?></div></body>',
);
},
},
},
usePHP(),
],
...This hook runs between loading the PHP code and unescaping PHP fragments, before Vite's own HTML transformations.
// vite.config.ts
...,
plugins: [
usePHP(),
{
name: 'post-transform',
transformIndexHtml: {
order: 'post',
handler(html, ctx) {
return html.replace(
'</body>',
'<div><?= "Post PHP transform"; ?></div></body>',
);
},
},
},
],
...This hook runs right after Vite's transformations and unescaping PHP fragments.
vite-plugin-php makes PHP and Vite work together, but there are a few patterns that need special care or do not work the way you might expect.
PHP variables are not available inside inline <script type="module"> blocks.
<?php $var = 'foo'; ?>
<script type="module">
console.log('<?= $var ?>');
</script>Vite transpiles inline modules into separate files, so they are no longer part of the same PHP execution. Server variables such as $_GET and $_POST will also have different values there.
Workaround: assign the value to a regular script block or a data attribute before the module:
<script>
window.__VAR__ = '<?= $var ?>';
</script>
<script type="module" src="./src/main.js"></script>Vite cannot process asset paths that are computed by PHP:
<!-- This will not be bundled -->
<script src="./src/<?= $script ?>.js" type="module"></script>Vite needs a literal path at build time. If the set of possible files is known, import them statically and choose at runtime.
If you wrap <script> or <link> tags in PHP conditionals, Vite might move them to another place.
<?php if ($some_condition): ?>
<script src="./src/some_script.js" type="module"></script>
<?php endif; ?>Vite processes these tags independently and merges or splits them. The resulting tags are typically appended to <head> or placed at the start of the document.
A possible workaround is to define those modules in separate PHP files and include them conditionally.
When the file contains a PHP namespace declaration, the plugin tries to place recovered assets:
- after the last closed HTML tag, or
- right before the last
<?tag, or - at the end of the file.
If your layout depends on exact tag placement, prefer keeping assets outside of namespaced blocks.
The rewriteUrl option can redirect to external origins, but it runs before Vite handles assets. You must explicitly return undefined for static assets to avoid sending them through PHP.
Environmental %ENV% placeholders are replaced during both dev and build.
Be careful not to expose secrets because the replaced values are written into dist/.
If you encounter any other bugs or need some other features feel free to open an issue.
Love open source? Enjoying my project?
Your support can keep the momentum going! Consider a donation to fuel the creation of more innovative open source software.
| via Ko-Fi | Buy me a coffee | via PayPal |
|
|
|
|

