Skip to content

09: Build Tools - TypeScript Compiler vs PHP

Coming from TypeScript, you’re used to compilation steps. PHP is different—it’s interpreted and runs directly without compilation. However, PHP projects still use build tools for assets, optimization, and distribution. This chapter explains when and why.

By the end of this chapter, you’ll be able to:

  • ✅ Understand why PHP doesn’t require compilation
  • ✅ Use Vite/Webpack for frontend asset bundling
  • ✅ Create PHAR archives for distribution
  • ✅ Optimize PHP with OPcache
  • ✅ Set up production-ready deployments
  • ✅ Use Docker for consistent environments

📁 View Code Examples on GitHub

This chapter includes build tool examples:

  • Vite configuration for Laravel
  • PHAR creation scripts
  • OPcache configuration
  • Docker setup examples

Explore the examples:

Terminal window
cd code/php-typescript-developers/chapter-09
# Review configurations
cat vite.config.js
cat docker-compose.yml
Terminal window
# TypeScript needs compilation
npm run build
# What happens:
# 1. TypeScript → JavaScript (tsc)
# 2. Bundle modules (Webpack/Vite/esbuild)
# 3. Minify (Terser)
# 4. Tree-shake unused code
# 5. Generate source maps
# Output: dist/ directory with compiled files

tsconfig.json:

{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"outDir": "./dist",
"sourceMap": true
}
}

Result: You deploy the dist/ directory, not src/.

Terminal window
# PHP runs directly
php index.php
# No build step required!
# 1. PHP interpreter reads .php files
# 2. Executes immediately
# 3. No compilation needed

You deploy your src/ directory as-is.

Even though PHP doesn’t need compilation, your frontend assets do:

Vite (Modern, Fast)

Terminal window
npm install --save-dev vite

vite.config.js:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
});

Build:

public/build/assets/app-[hash].js
npm run build
# Output:
# public/build/assets/app-[hash].css

Laravel Mix (Laravel’s Webpack wrapper)

Terminal window
npm install --save-dev laravel-mix

webpack.mix.js:

const mix = require('laravel-mix');
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
.version(); // Cache busting

Build:

Terminal window
npm run production

Yes, you can use TypeScript for your frontend while using PHP for backend!

package.json:

{
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"typescript": "^5.0.0",
"vite": "^4.0.0"
}
}

Your project:

project/
├── src/ # PHP backend (no build needed)
│ └── *.php
├── resources/ # TypeScript frontend (needs build)
│ ├── ts/
│ └── css/
├── public/ # Build output + entry point
│ ├── index.php # PHP entry point
│ └── build/ # Compiled TS/CSS
└── package.json
dist/bundle.js
# Webpack bundles everything into one file
npm run build

PHAR files package your PHP application into a single executable:

Create PHAR:

build-phar.php
<?php
$phar = new Phar('myapp.phar');
$phar->startBuffering();
$phar->buildFromDirectory(__DIR__ . '/src');
$phar->setStub($phar->createDefaultStub('index.php'));
$phar->stopBuffering();

Build:

Terminal window
php build-phar.php

Run:

Terminal window
php myapp.phar
# or
./myapp.phar # If made executable

Use Cases:

  • CLI tools (like Composer, PHPUnit)
  • Distributable applications
  • Self-contained packages

Example: Composer is a PHAR:

Terminal window
php composer.phar install

Box is a more advanced PHAR builder:

Terminal window
composer require --dev humbug/box

box.json:

{
"main": "bin/app.php",
"output": "build/app.phar",
"directories": ["src"],
"files": ["composer.json"],
"stub": true,
"compression": "GZ"
}

Build:

Terminal window
vendor/bin/box compile
{
"scripts": {
"build": "webpack --mode production"
}
}

What happens:

  • Minification
  • Tree-shaking
  • Code splitting
  • Compression

PHP doesn’t need minification, but it has OPcache:

OPcache caches compiled PHP bytecode in memory.

Enable in php.ini:

; Enable OPcache
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000
opcache.revalidate_freq=0
; Production settings
opcache.validate_timestamps=0 ; Never check for file changes
opcache.fast_shutdown=1

Check OPcache status:

<?php
var_dump(opcache_get_status());

Performance impact: 2-3x faster responses!

Composer can optimize autoloading:

Terminal window
# Development
composer dump-autoload
# Production (optimized)
composer dump-autoload --optimize
composer install --optimize-autoloader --no-dev

What it does:

  • Creates a classmap (like tree-shaking)
  • Removes dev dependencies
  • Faster class loading
Terminal window
# 1. Build application
npm run build
# 2. Deploy dist/ directory
rsync -av dist/ user@server:/var/www/app/
# 3. Start application
pm2 start dist/server.js
# 4. Set up reverse proxy (nginx)

Directory structure on server:

/var/www/app/
└── dist/ # Only compiled code
├── server.js
└── assets/
Terminal window
# 1. NO BUILD STEP (just copy source)
rsync -av src/ user@server:/var/www/app/
# 2. Install dependencies
composer install --no-dev --optimize-autoloader
# 3. Configure web server (Apache/Nginx)
# PHP runs via PHP-FPM

Directory structure on server:

/var/www/app/
├── src/ # Source code (uncompiled)
├── vendor/ # Dependencies
└── public/ # Web root
└── index.php # Entry point
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/
CMD ["node", "dist/server.js"]
FROM php:8.2-fpm-alpine
WORKDIR /var/www/app
# Install dependencies
RUN apk add --no-cache \
composer \
nginx
# Copy application
COPY --chown=www-data:www-data . .
# Install Composer dependencies
RUN composer install --no-dev --optimize-autoloader
# Enable OPcache
RUN docker-php-ext-install opcache
# Configure PHP
COPY php.ini /usr/local/etc/php/conf.d/custom.ini
EXPOSE 80
CMD ["php-fpm"]

Key differences:

  • Node.js: Copy dist/ (built files)
  • PHP: Copy src/ (source files) + run Composer
  • PHP: Use PHP-FPM + Nginx (not standalone server like Node)

Directory structure:

project/
├── app/ # PHP application
│ └── Controllers/
├── resources/ # Frontend source
│ ├── js/
│ │ └── app.ts
│ └── css/
│ └── app.css
├── public/ # Web root
│ ├── index.php # PHP entry
│ └── build/ # Vite output
├── vite.config.js
└── package.json

package.json:

{
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"vite": "^4.0.0",
"typescript": "^5.0.0",
"@vitejs/plugin-vue": "^4.0.0"
}
}

vite.config.js:

import { defineConfig } from 'vite';
export default defineConfig({
root: 'resources',
build: {
outDir: '../public/build',
manifest: true,
rollupOptions: {
input: 'resources/js/app.ts'
}
}
});

Use in PHP:

public/index.php
<!DOCTYPE html>
<html>
<head>
<?php
// Read Vite manifest
$manifest = json_decode(file_get_contents('build/manifest.json'), true);
$jsFile = $manifest['resources/js/app.ts']['file'];
$cssFile = $manifest['resources/js/app.ts']['css'][0] ?? null;
?>
<?php if ($cssFile): ?>
<link rel="stylesheet" href="/build/<?= $cssFile ?>">
<?php endif; ?>
</head>
<body>
<div id="app"></div>
<script type="module" src="/build/<?= $jsFile ?>"></script>
</body>
</html>
.github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
# Build step required
- run: npm ci
- run: npm run build
- run: npm test
# Deploy compiled code
- name: Deploy
run: rsync -av dist/ ${{ secrets.DEPLOY_SERVER }}:/var/www/app/
.github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
# NO build step for PHP code
- run: composer install --no-dev --optimize-autoloader
- run: composer test
# Deploy source code directly
- name: Deploy
run: rsync -av --exclude 'node_modules' --exclude '.git' . ${{ secrets.DEPLOY_SERVER }}:/var/www/app/
jobs:
deploy:
steps:
# ...
# Build frontend assets
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run build
# Install PHP dependencies
- run: composer install --no-dev
# Deploy everything
- name: Deploy
run: |
rsync -av \
--exclude 'node_modules' \
--exclude 'resources/js' \
--exclude 'resources/css' \
. ${{ secrets.DEPLOY_SERVER }}:/var/www/app/
Terminal window
# Terminal 1: Watch and rebuild
npm run dev
# Terminal 2: Run server
npm start
Terminal window
# Option 1: Built-in server (dev only)
php -S localhost:8000
# Option 2: Laravel Artisan
php artisan serve
# Option 3: Docker
docker-compose up
# If using frontend assets:
# Terminal 2: Watch assets
npm run dev

Optimization happens at build time:

  • Dead code elimination
  • Minification
  • Compression
  • Code splitting

Optimization happens at runtime:

  • OPcache (bytecode caching)
  • Autoload optimization
  • Database query caching
  • Application-level caching (Redis, Memcached)

No dead code elimination needed because PHP only loads what it uses.

  1. PHP is interpreted - No compilation/transpilation step required (unlike TypeScript)
  2. Frontend assets still need bundling - Use Vite/Webpack for JS/CSS
  3. PHAR files package PHP apps into single executable (like bundled JS)
  4. OPcache is PHP’s runtime optimization (bytecode cache, not build step)
  5. Deployment is simpler - Deploy source code directly, no build artifacts
  6. Composer optimization replaces some build steps (composer install --optimize-autoloader)
  7. Docker works differently - No npm run build in Dockerfile, just composer install
  8. Laravel Mix/Vite handle frontend assets in full-stack PHP apps
  9. No source maps needed for PHP - errors show actual source lines
  10. composer dump-autoload is closest to “rebuild” - regenerates autoloader
  11. Production optimization via OPcache config, not build tooling
  12. Monorepo tools less common - PHP projects often self-contained
AspectTypeScript/Node.jsPHP
CompilationRequiredNot needed
Build Outputdist/ directoryN/A
DeployCompiled filesSource files
OptimizationBuild-timeRuntime (OPcache)
Frontend AssetsWebpack/ViteSame (Webpack/Vite)
Dependenciesnode_modules/vendor/
Production Prepnpm run buildcomposer install --optimize
CachingApplication levelOPcache + application level
TaskTypeScript/Node.jsPHP
Start dev servernpm run devphp -S localhost:8000
Build for productionnpm run buildN/A (or composer install --optimize)
Build assetsnpm run buildnpm run build (same!)
Optimize codeBuild-time minificationOPcache configuration
DeployUpload dist/Upload src/ + run Composer
Create packagenpm packCreate PHAR

Now that you understand the build process (or lack thereof), let’s explore debugging tools.

Next Chapter: 10: Debugging: Node Inspector vs Xdebug


Questions or feedback? Open an issue on GitHub