Seamlessly Blend PHP with Node.js

Matteo CollinaMatteo Collina
12 min read

We're excited to announce @platformatic/php-node, a revolutionary new Node.js module designed to bridge the gap between PHP and Node.js. With php-node, you can now embed PHP directly within your Node.js applications, leveraging PHP as a robust request processor. This powerful combination allows you to harness the strengths of both languages in a single, cohesive environment.

What is @platformatic/php-node?

php-node is a Rust-based, Node.js-native module that facilitates the execution of PHP applications within a Node.js environment. It works by dispatching requests to a PHP instance running multi-threaded in the worker pool provided by the Node.js platform. This means you can enjoy the performance and scalability of Node.js while utilizing PHP's extensive ecosystem and functionality. Not only that, it runs PHP requests across multiple threads by default, which benefits from multiple cores with no additional work.

Php-node would not be possible without the great work of David Cole and others in https://github.com/davidcole1340/ext-php-rs, which we forked to add specific functionality that we needed. We plan to upstream our changes soon.

How Does it Work?

At its core, php-node acts as a bridge between Node.js and PHP. When a request needs PHP processing, php-node seamlessly routes it to a PHP worker within the Node.js worker pool. The PHP worker executes the code and returns the result back to Node.js, which then delivers it to the client. This process is handled efficiently and transparently, allowing you to focus on building your application.

Php-node provides a wrapper around PHP built with Rust and napi.rs which translates requests and responses into a language-agnostic system called lang_handler, which facilitates transporting requests across languages and threads.

By using Rust and lang_handler, the complexity of PHP is cleanly isolated into a small and simple interface specifically designed for request handling. It provides a set of Request and Response types along with a generic Handler trait in Rust which allows for providing a consistent interface for dealing with requests and responses while leaving it up to other code to define how requests are handled. A Handler is any object implementing the Handler trait which requires a handle(Request) -> Response method be provided.

The lang_handler requests are processed via PhpRequestTask which can be used both synchronously on the JavaScript thread or asynchronously running request tasks within the Node.js platform worker pool via the Task API provided by napi.rs, which manages transporting work between threads and translating results back to JavaScript. It provides a simple promise-based interface for operating seamlessly through Rust as the intermediary language and back to the JavaScript host language.

By embedding PHP directly it becomes possible to route requests and receive responses without ever going to the network, which significantly improves latency between the languages.

The structure and data flow is represented in the following diagram.

Key Features

  • Seamless Integration: Effortlessly embed PHP within Node.js applications.

  • Multi-threaded Processing: Leverages Node.js platform worker pool for parallel PHP execution.

  • Improved Performance: Enhances application performance by combining Node.js's speed with PHP's capabilities.

  • 'Unified Development Environment: Reduce cognitive load and ship faster with a single development environment for Node.js and PHP

Use Cases

php-node is ideal for a variety of use cases, including:

  • Migrating legacy PHP applications to Node.js.

  • Building hybrid applications that leverage the strengths of both languages.

  • Creating high-performance web services that require PHP processing.

Getting Started

To get started with php-node, simply install the module using npm:

npm install @platformatic/php-node

Further instructions and detailed documentation are available in our GitHub repository.

Example

import { Php, Request } from '@platformatic/php-node'

const php = new Php({
  argv: process.argv,
  docroot: process.cwd()
})

const request = new Request({
  method: 'POST',
  url: 'http://example.com/index.php',
  headers: {
    'Accept': ['text/html']
  },
  body: Buffer.from('some body')
})

const response = await php.handleRequest(request)

console.log({
  status: response.status,
  headers: response.headers,
  body: response.body.toString(),
  log: response.log.toString(),
  exception: response.exception.toString()
})

You would now wonder if php-node could run something complex like WordPress. Keep reading!

Running PHP inside Watt

Last September, we released Watt, the Node.js application server - let’s see if it can run PHP as well!

npx wattpm@latest create --module=@platformatic/php

That will generate a service using the @platformatic/php stackable. It contains a “public” directory which contains a single index.php file, but any additional php files may be added to that directory and will be routed to by the service automatically. Here is the output

? Where would you like to create your project? watt
? Which kind of service do you want to create? @platformatic/php
✔ Installing @platformatic/php...
? What is the name of the service? cool-shaman
? Do you want to create another service? no
? Do you want to use TypeScript? no
? What port do you want to use? 3042
[19:00:22.948] INFO (83227): /Users/matteo/tmp/watt/package.json written!
[19:00:22.952] INFO (83227): /Users/matteo/tmp/watt/watt.json written!
[19:00:22.953] INFO (83227): /Users/matteo/tmp/watt/.env written!
[19:00:22.953] INFO (83227): /Users/matteo/tmp/watt/.env.sample written!
[19:00:22.954] INFO (83227): /Users/matteo/tmp/watt/.gitignore written!
[19:00:22.954] INFO (83227): /Users/matteo/tmp/watt/README.md written!
[19:00:22.955] INFO (83227): /Users/matteo/tmp/watt/web/cool-shaman/package.json written!
[19:00:22.955] INFO (83227): /Users/matteo/tmp/watt/web/cool-shaman/platformatic.json written!
[19:00:22.956] INFO (83227): /Users/matteo/tmp/watt/web/cool-shaman/.gitignore written!
[19:00:22.957] INFO (83227): /Users/matteo/tmp/watt/web/cool-shaman/public/index.php written!
? Do you want to init the git repository? yes
[19:00:27.962] INFO (83227): Git repository initialized.
[19:00:28.031] INFO (83227): Installing dependencies for the application using npm ...
... npm output ...
[19:00:33.882] INFO (83227): You are all set! Run `npm start` to start your project.

Then, we can start our PHP runtime!

cd watt
npm start

Then we can head to http://localhost:3042 to see phpinfo!

You can find the php file to modify in web/cool-shaman/public/index.php.

Running WordPress inside Watt

What if we could mix WordPress with Node.js? With an empty @platformatic/php service, we can turn it into a WordPress server with a few changes. Running the following commands in the project root directory to replace the contents with a Wordpress installation.

curl -O https://wordpress.org/latest.zip
unzip latest.zip
rm latest.zip
mv wordpress/* web/cool-shaman/public
rm -rf wordpress

Then, you’d need to start up a MySQL database, using the following docker-compose.yml:

services:
  mysql:
    image: mysql:8
    container_name: mysql_db
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword  # Change this to a secure password
      MYSQL_DATABASE: mydb               # Your database name
      MYSQL_USER: user                   # Your MySQL user
      MYSQL_PASSWORD: userpassword       # Change this to a secure password
    ports:
      - "3306:3306"
      - "33060:33060"
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:
    driver: local

You can start that with docker compose up.

Then in another terminal, start your server with npm start. Head to http://localhost:3042 and complete the WordPress installation with the following settings:

Then, complete the install scripts then will ask for website name and user credentials for your website admin account. Once that is complete, congratulations, you have successfully installed Wordpress into PHP running in Node.js!

Bridging WordPress and Next.js in a single Application

Teaming up Next.js with a headless WordPress? We got you covered, too! You get WordPress doing what it's best at – handling content like a champ. Then, Next.js swoops in with a super speedy, snappy frontend powered by React, plus all that SEO goodness. WordPress takes care of the behind-the-scenes content magic, and Next.js makes it shine on the web with awesome performance and flexibility. It's a clean setup – each does its job perfectly, giving you a site that's both powerful and a pleasure to use.

To achieve this dream we need a few more things:

Stop the server, then create the Next.js application:

➜  cd web
➜  npx create-next-app@latest

Need to install the following packages:
create-next-app@15.3.2
Ok to proceed? (y) y

✔ What is your project named? … my-app
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like your code inside a `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to use Turbopack for `next dev`? … No / Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No / Yes
Creating a new Next.js app in /Users/matteo/tmp/watt/web/my-app.

Using npm.

Initializing project with template: app-tw


Installing dependencies:
- react
- react-dom
- next

Installing devDependencies:
- @tailwindcss/postcss
- tailwindcss

npm warn workspaces my-app in filter set, but no workspace folder present

added 51 packages in 5s

133 packages are looking for funding
  run `npm fund` for details
Success! Created my-app at /Users/matteo/tmp/watt/web/my-app

Then, we need to import it in Watt correctly:

➜  npx wattpm import
[19:49:35.466] INFO (95779): Detected stackable @platformatic/next for service my-app, adding to the service dependencies.

Finally, install the dependencies with npm install. Then, build the frontend app with npm run build.

➜ npx wattpm create
Hello Matteo Collina, welcome to Watt 2.65.1!
Using existing configuration ...
? Which kind of service do you want to create? @platformatic/composer
? What is the name of the service? composer
? Do you want to create another service? no
? Which service should be exposed? composer
? Do you want to use TypeScript? no
[19:46:33.414] INFO (94579): /Users/matteo/tmp/watt/.env written!
[19:46:33.417] INFO (94579): /Users/matteo/tmp/watt/.env.sample written!
[19:46:33.417] INFO (94579): /Users/matteo/tmp/watt/watt.json written!
[19:46:33.418] INFO (94579): /Users/matteo/tmp/watt/web/composer/package.json written!
[19:46:33.418] INFO (94579): /Users/matteo/tmp/watt/web/composer/platformatic.json written!
[19:46:33.419] INFO (94579): /Users/matteo/tmp/watt/web/composer/.gitignore written!
[19:46:33.419] INFO (94579): /Users/matteo/tmp/watt/web/composer/global.d.ts written!
[19:46:33.419] INFO (94579): /Users/matteo/tmp/watt/web/composer/README.md written!
[19:46:33.478] INFO (94579): Installing dependencies for the application using npm ...
…npm output…
[19:46:34.592] INFO (94579): Project created successfully, executing post-install actions...
[19:46:34.592] INFO (94579): You are all set! Run `npm start` to start your project.

Then, we need to update the routing logic:

{
  "$schema": "https://schemas.platformatic.dev/@platformatic/composer/2.65.1.json",
  "composer": {
    "services": [{
      "id": "my-app",
      "proxy": {
        "prefix": "/"
      }
    }, {
      "id": "cool-shaman",
      "proxy": {
        "prefix": "/wp"
      }
    }],
    "refreshTimeout": 1000
  },
  "watch": true
}

This routes the root path to our Next.js application, and the /wp prefix to our wordpress install. Given that we have added a reverse proxy, we need to add the relevant configuration in web/cool-shaman/wp-config.php:

// If behind a reverse proxy that sets custom headers
$_parent_protocol = $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? (isset($_SERVER['HTTPS']) ? 'https' : 'http');
$_parent_host = $_SERVER['HTTP_X_FORWARDED_HOST'] ?? $_SERVER['HTTP_HOST'];

define('WP_HOME', $_parent_protocol . '://' . $_parent_host . '/wp');
define('WP_SITEURL', $_parent_protocol . '://' . $_parent_host . '/wp');

Now, run the server and try to access http://localhost:3042/wp/wp-admin and http://localhost:3042.

Finally, we need to hook up our Next.js application to talk to our headless CMS by replacing web/my-app/app/page.js with:

// app/blog/page.js
import Link from 'next/link';
import Image from 'next/image';
import undici from 'undici';

// Replace with your WordPress site URL
const WORDPRESS_URL = 'http://cool-shaman.plt.local';

// Server component to fetch posts
async function getPosts() {
  try {
    const response = await fetch(`${WORDPRESS_URL}/index.php?rest_route=/wp/v2/posts&per_page=100`, {
      cache: 'no-store',
    });

    if (!response.ok) {
      throw new Error(`Failed to fetch posts: ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Error fetching WordPress posts:', error);
    throw error;
  }
}

// Utility functions
function stripHtml(html) {
  return html.replace(/<[^>]*>/g, '');
}

function formatDate(dateString) {
  return new Date(dateString).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
}

// Post Card Component
function PostCard({ post }) {
  const featuredImage = post._embedded?.['wp:featuredmedia']?.[0];
  const author = post._embedded?.author?.[0];
  const categories = post._embedded?.['wp:term']?.[0]?.slice(0, 3) || [];

  return (
    <article className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
      {/* Featured Image */}
      {featuredImage && (
        <div className="relative h-48 w-full">
          <Image
            src={featuredImage.source_url}
            alt={featuredImage.alt_text || stripHtml(post.title.rendered)}
            fill
            className="object-cover"
            sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
          />
        </div>
      )}

      <div className="p-6">
        {/* Title */}
        <h2 className="text-xl font-semibold text-gray-900 mb-3">
          <Link 
            href={`/blog/${post.slug}`}
            className="hover:text-blue-600 transition-colors duration-200 line-clamp-2"
          >
            <span dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
          </Link>
        </h2>

        {/* Excerpt */}
        <p className="text-gray-600 mb-4 line-clamp-3">
          {stripHtml(post.excerpt.rendered)}
        </p>

        {/* Meta information */}
        <div className="flex items-center justify-between text-sm text-gray-500 mb-4">
          <time dateTime={post.date}>{formatDate(post.date)}</time>
          {author && <span>By {author.name}</span>}
        </div>

        {/* Categories */}
        {categories.length > 0 && (
          <div className="flex flex-wrap gap-2 mb-4">
            {categories.map((category) => (
              <span
                key={category.id}
                className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full"
              >
                {category.name}
              </span>
            ))}
          </div>
        )}

        {/* Read More Link */}
        <Link
          href={`/blog/${post.slug}`}
          className="inline-flex items-center text-blue-600 hover:text-blue-800 font-medium transition-colors duration-200"
        >
          Read More
          <svg className="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
          </svg>
        </Link>
      </div>
    </article>
  );
}

// Loading Component
function BlogSkeleton() {
  return (
    <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
      {[...Array(6)].map((_, i) => (
        <div key={i} className="bg-white rounded-lg shadow-md overflow-hidden animate-pulse">
          <div className="h-48 bg-gray-300"></div>
          <div className="p-6">
            <div className="h-6 bg-gray-300 rounded mb-3"></div>
            <div className="h-4 bg-gray-300 rounded mb-2"></div>
            <div className="h-4 bg-gray-300 rounded mb-4 w-3/4"></div>
            <div className="flex justify-between mb-4">
              <div className="h-3 bg-gray-300 rounded w-20"></div>
              <div className="h-3 bg-gray-300 rounded w-16"></div>
            </div>
            <div className="h-3 bg-gray-300 rounded w-24"></div>
          </div>
        </div>
      ))}
    </div>
  );
}

export const revalidate = 0

// Main Blog Page Component
export default async function BlogPage() {
  'use server'

  let posts = [];
  let error = null;

  posts = await getPosts();

  return (
    <div className="min-h-screen bg-gray-50 py-8">
      <div className="max-w-6xl mx-auto px-4">
        <header className="text-center mb-12">
          <h1 className="text-4xl font-bold text-gray-900 mb-4">Blog</h1>
          <p className="text-xl text-gray-600 max-w-2xl mx-auto">
            Discover our latest articles, insights, and updates
          </p>
        </header>

        {posts.length === 0 ? (
          <div className="text-center py-12">
            <div className="text-gray-500">
              <svg className="mx-auto h-12 w-12 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
              </svg>
              <p className="text-lg">No blog posts found</p>
              <p className="text-sm mt-2">Check back later for new content!</p>
            </div>
          </div>
        ) : (
          <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
            {posts.map((post) => (
              <PostCard key={post.id} post={post} />
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// Metadata for SEO
export async function generateMetadata() {
  return {
    title: 'Blog | Your Site Name',
    description: 'Read our latest blog posts, articles, and insights.',
    openGraph: {
      title: 'Blog | Your Site Name',
      description: 'Read our latest blog posts, articles, and insights.',
      type: 'website',
    },
  };
}

Then, start the server with npm run build && npm start and enjoy:

You can find the final integration at https://github.com/platformatic/watt-next-wordpress.

Watch a full walkthrough for this demo on YouTube:

Conclusion

Whether you're gradually migrating from PHP to Node.js, building new features that need both ecosystems, or simply trying to reduce the operational overhead of running multiple services, php-node provides a path forward that doesn't require abandoning your existing investments or accepting architectural complexity as inevitable.

We can’t wait for you to try php-node not just as a technical experiment, but as a practical solution to some of the most common integration challenges in modern web development.

The future of application development isn't just about choosing the one right tech, it's about having the tools to use the best technology for each problem within a cohesive, manageable architecture.

And if you have any questions or issues, just hit us up at hello@platformatic.dev — we look forward to hearing from you!

11
Subscribe to my newsletter

Read articles from Matteo Collina directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Matteo Collina
Matteo Collina