nodejs-php-fallback/less
Compiles LESS to CSS from PHP, using Node.js lessc when available and falling back to a pure-PHP compiler when it isn’t. Ideal for Laravel/PHP asset pipelines that need reliable LESS builds without a mandatory Node.js dependency.
This is not a Laravel package — there is no service provider, config file, or facade. It is a thin Composer library (nodejs-php-fallback/less) that gives you one class which compiles LESS either by shelling out to the Node lessc binary or by falling back to the pure-PHP less.php parser. Treat it as a build utility you wire up yourself, not a framework feature.
Install
composer require nodejs-php-fallback/less
This pulls in the small nodejs-php-fallback core plus a PHP LESS compiler (less.php) as the fallback. Node is optional — if node/lessc is on PATH, it is used; otherwise the PHP path handles it.
First look
Read in this order:
vendor/nodejs-php-fallback/less/src/ — there is essentially one class. Skim its public methods (compile, compileFile, options setter). That is the whole API surface.vendor/nodejs-php-fallback/nodejs-php-fallback/src/ — this is where the Node-vs-PHP detection lives. If compilation behaves oddly, the bug is almost always here, not in the LESS wrapper.First use case: compile a stylesheet in a console command
The most natural fit in a Laravel app is an artisan command or a deploy step that turns resources/less/app.less into public/css/app.css:
use NodejsPhpFallback\Less;
$less = new Less();
$less->setOptions(['compress' => true]);
$css = $less->compileFile(base_path('resources/less/app.less'));
file_put_contents(public_path('css/app.css'), $css);
If you have lessc installed locally, that call fans out to Node; on a CI box or a locked-down shared host with no Node, the identical call runs in PHP. That graceful degradation is the entire value proposition — you get to keep one code path across environments.
Wrap it in a dedicated compiler service. Do not instantiate Less in controllers or Blade. Put a small class behind the container so you can inject it, mock it, and swap it later:
namespace App\Assets;
use NodejsPhpFallback\Less;
class LessCompiler
{
public function __construct(private array $options = []) {}
public function compileFile(string $path): string
{
$less = new Less();
$less->setOptions($this->options);
return $less->compileFile($path);
}
}
Bind it in a service provider with your preferred options (compress, sourceMap, etc.) read from config/view.php or a small config/assets.php.
Cache aggressively — the library will not. There is no built-in cache, mtime check, or partial invalidation. For "compile on request" workflows (useful when you cannot run a build step, e.g. cheap shared hosting), key a cache on the source file's mtime plus the mtimes of every @imported partial:
$key = 'less:'.md5($entryPath.json_encode(array_map(
'filemtime',
glob(resource_path('less/**/*.less'))
)));
$css = Cache::rememberForever($key, fn () => $compiler->compileFile($entryPath));
Without this, compiling through lessc on each request will dominate your response time.
Use compileFile, not compile, when @import is involved. Passing a raw string means relative imports resolve against the PHP process working directory (usually public/ under FPM), not against resources/less/. compileFile anchors resolution to the file path, which is what you almost always want for Laravel's typical resources/less/app.less layout.
Integrate with a build pipeline, not the request lifecycle. The strongest pattern is a Composer script or artisan command invoked by your deploy tooling:
"scripts": {
"build:css": "@php artisan assets:less"
}
Then lessc runs once per deploy. Reserve on-request compilation (with the cache above) for environments where you genuinely have no build stage.
Watch the two-compiler divergence. Node lessc and the PHP fallback are separate implementations tracking different versions of the LESS language. Keep compress and other options to the common denominator (variables, nesting, mixins, @import, basic functions all work in both; newer plugin APIs and some math-mode semantics do not). If you must guarantee identical bytes across dev and prod, pin the Node lessc version and accept that the PHP fallback is a lower-fidelity mirror.
Extension point: if you need different behavior on the Node side (custom plugin paths, --global-var args), the core package exposes the command construction; subclass or decorate the Less class rather than editing vendor code. It is a very small class — a decorator is easier than patching.
It is effectively unmaintained (last release January 2019). Zero dependents and four stars tell you nobody is filing issues you will find answers to. Pin the version in composer.json and expect to own any bug you hit. For a new Laravel project, wikimedia/less.php directly (or a Vite/Sass/Tailwind pipeline) is the safer long-term bet; this package's differentiator — automatic Node fallback — matters mostly for legacy or mixed environments.
Node detection is environment-sensitive. The core checks for a node binary on PATH. Under PHP-FPM, PATH is often stripped compared to your shell, so a machine that compiles fine via CLI will silently fall back to PHP in web requests (or vice versa). This produces "works in dev, different output in prod" reports that are maddening to debug. Log which engine was used during a deploy and compare across environments.
Temporary files and permissions. Both engines route through temp files in sys_get_temp_dir(). On hardened hosts where open_basedir or a read-only /tmp is in play, you get opaque failures. Set a writable, app-owned temp dir if needed and check storage/ permissions before blaming the LESS.
Shell escaping / input trust. The Node path ultimately shells out. Never pass user-submitted LESS through this (and never interpolate user data into option strings). Assets should come from the repo. If you need dynamic theming, generate LESS source from validated variables — do not accept raw LESS.
Error reporting differs by engine. The Node path surfaces lessc stderr; the PHP path throws exceptions from less.php. Wrap calls and normalize messages. An empty CSS file is also a common silent failure mode with the PHP parser — always assert non-empty output in your build command.
compress naming. Options are forwarded as CLI flags in the Node path and as array keys in the PHP path. Option names must match what each accepts; the shared subset is small. Test any non-default option on both engines.
No source maps story worth relying on. If you need debugging-grade source maps, do not use the fallback path — it is inconsistent. Use a real build step with lessc --source-map.
Debugging tactic: temporarily force the fallback by making node unreachable (e.g. run with a modified PATH), and confirm your LESS file compiles in pure PHP. If it does not, your LESS uses a construct the PHP parser lacks, and your "works locally" build will break on any No-Node host. Catch this in CI by running your compile command twice — once normally, once with Node hidden.
When to skip it entirely. If you already run npm/Vite, you do not need this package: lessc in a build script is more predictable. This library earns its keep only when you cannot guarantee Node in the runtime environment and want a single PHP entry point that degrades gracefully.
How can I help you explore Laravel packages today?