nodejs-php-fallback/uglify
A PHP fallback for UglifyJS that minifies JavaScript without needing Node.js installed. Keeps asset pipelines simple in Laravel and PHP projects by providing a native minifier when Node is unavailable.
Install the wrapper with Composer. Node is optional — it is preferred when present, but you must explicitly install a PHP fallback or the package has nothing to fall back to.
composer require nodejs-php-fallback/uglify
# Check what the package suggests for the non-Node path, then install it:
composer suggest nodejs-php-fallback/uglify
# historically matthiasmullie/minify (CSS) and a JS minifier equivalent
If you do want the Node path (recommended for output parity with your front-end toolchain), install the binaries locally in the project:
npm install --save-dev uglify-js clean-css
Your first use case is almost always a deploy-time (not request-time) minification step. The smallest useful call:
use NodejsPhpFallback\Uglify;
// Minify into a file (returns the object / path)
Uglify::js('public/js/app.js', 'public/js/app.min.js');
// Omit the output argument to get the minified string back
$css = (string) Uglify::css('public/css/app.css');
Where to look first
README.md on GitHub — it is short; read it end to end before writing code.vendor/nodejs-php-fallback/uglify/src/Uglify.php — the whole public surface is js() / css() plus constructor/output handling; there is no large config layer to learn.vendor/nodejs-php-fallback/nodejs-php-fallback/ — the shared base class. This is where Node detection, node_modules resolution, and the exec/fallback branch live. If behaviour seems "magic", the answer is here, not in the uglify package.The package has no service provider and no Laravel integration, so treat it as a plain library and wire it into a command.
This is the dominant pattern: minify once per deploy, never per request.
namespace App\Console\Commands;
use Illuminate\Console\Command;
use NodejsPhpFallback\Uglify;
class MinifyAssets extends Command
{
protected $signature = 'assets:minify {--force : Rebuild even if output is fresh}';
protected $description = 'Minify CSS and JS assets';
public function handle(): int
{
$pairs = [
['public/css/app.css', 'public/css/app.min.css', 'css'],
['public/js/app.js', 'public/js/app.min.js', 'js'],
];
foreach ($pairs as [$in, $out, $type]) {
$absIn = base_path($in);
$absOut = base_path($out);
if (! $this->option('force')
&& file_exists($absOut)
&& filemtime($absOut) >= filemtime($absIn)) {
$this->line("skip {$in}");
continue;
}
$type === 'css'
? Uglify::css($absIn, $absOut)
: Uglify::js($absIn, $absOut);
$this->info("minified {$in} -> {$out}");
}
return self::SUCCESS;
}
}
The underlying Node call inherits the PHP process working directory, which under FPM is not your project root. base_path() / public_path() avoids a whole class of "file not found" failures that only appear in production.
Where the version supports it, pass an array of inputs and a single output. One Node process for many files is far cheaper than N processes, and it keeps source order deterministic.
Uglify::js([
public_path('js/vendor.js'),
public_path('js/app.js'),
], public_path('js/bundle.min.js'));
Since minification rewrites the file, derive the asset version from filemtime()/md5_file() of the generated file:
$version = file_exists($f = public_path('js/app.min.js'))
? filemtime($f)
: null;
echo asset('js/app.min.js') . ($version ? '?v=' . $version : '');
Do not use mix()/vite() manifests here — this package does not produce hashed filenames.
Typical placement is an Envoyer/Deployer/Envoy hook after composer install and npm ci:
npm ci
php artisan assets:minify --force
--force in CI because checkout timestamps are unreliable and mtime-based skipping will lie.
If you care that production used the Node path, assert it once at boot rather than discovering it after a release:
if (! trim((string) shell_exec('node --version'))) {
logger()->warning('Node unavailable; uglify falling back to PHP minifier.');
}
The Node and PHP paths do not produce identical output. Different variable mangling, different whitespace, sometimes different semantics for edge-case syntax. If you compare builds between environments, or you store SRI hashes, expect noise. Pin one path per environment or accept that output is environment-dependent.
uglify-js is ES5-only and deprecated. It will fail on arrow functions, class, let/const, template literals, and any untranspiled modern syntax. If your build pipeline already uses Babel/esbuild/Vite, this package is redundant — you are paying for a second minifier with older semantics. For anything modern, prefer terser on the Node side or drop this package in favour of Mix/Vite entirely.
The fallback is silent. When Node is missing, the package transparently switches minifiers instead of erroring. That is the point of the library, but it means a typo in node_modules location degrades quality without any signal. Log php_sapi_name(), the resolved binary, and the branch taken at least once.
Package is stale. Last release January 2019, no dependents, no maintenance signal. Audit against your PHP version before adopting: unpatched string/array handling from that era frequently breaks on PHP 8.1+ (implicit nullable parameters, ${var} string interpolation, dynamic properties). If you hit deprecations, pinning a PHP 7.4 runtime may be the only comfortable option — or vendor the class and fix it.
Shell execution may be blocked. Shared hosts disable exec/proc_open/shell_exec in disable_functions. There is no HTTP-only mode; the package will throw rather than fall back when the exec call itself is forbidden. Check php -i | grep disable_functions on the target host before planning to run this at runtime.
node_modules resolution is relative, and version-sensitive. The wrapper looks for uglify-js/clean-css in a node_modules directory it computes itself. Nested or monorepo layouts (hoisted node_modules, pnpm's symlinked store, Yarn PnP) frequently break it. npm install --save-dev at the project root with a flat layout is the only reliably tested arrangement.
Don't minify on every request. The Node round-trip is tens to hundreds of milliseconds and spawns a process. If you need dynamic bundling, cache the output keyed on the input file hashes; never re-minify per hit.
Write to a temp file, then rename(). A partially written *.min.js served mid-deploy is a visible outage. rename() on the same filesystem is atomic.
Check return/exception behaviour per method. js()/css() with an output path write and return; without one they return the minified content (castable to string). Mixing the two forms in a helper silently gives you "1" or the file path instead of code — always (string) the no-output form.
Extension points are limited to the base class. The useful hooks are the binary/binary-path lookup and the exec wrapper in nodejs-php-fallback, plus the PHP fallback implementation. If you need terser, custom options, or source maps, you are better off writing a thin Process call yourself than trying to extend this wrapper.
How can I help you explore Laravel packages today?