Skip to content
WSLDeep Dive Published Updated 7 min readViews unavailable

PHP and Composer in WSL: Align the CLI, Extensions, and Project Runtime

Run PHP projects in WSL with a Linux CLI, Composer lockfiles, matching extensions, and explicit separation from Windows PHP and web runtimes.

PHP development in WSL works best when Linux owns the interpreter, Composer process, project tree, native extensions, and test commands. Installing PHP on Windows does not install it in the WSL distribution, and installing a PHP extension on one side does not make it available to the other. Composer resolves dependencies against the PHP process and platform packages it sees when Composer runs. A project can therefore install successfully under Windows PHP but fail under Linux PHP because a required extension, PHP version, ABI, or native library differs.

The first design choice is which environment owns the application. If the application is intended to run on Linux, use Linux PHP and Linux development packages inside WSL. If the target is Windows-only, use Windows PHP and its own extension configuration. Avoid alternating Composer commands between those runtimes in one checkout: generated files, lockfile platform data, path references, and native extensions may not match the runtime that runs the tests.

Inventory the PHP process that executes

Run the checks inside the exact WSL distribution and shell that will run the application:

command -v php
php -v
php --ini
php -m | sort
php -r 'printf("%s %s %s\\n", PHP_OS_FAMILY, PHP_VERSION, PHP_SAPI);'
command -v composer
composer --version

The path and SAPI matter. php –ini reports the CLI configuration, and php -m reports modules loaded into that CLI process. A web server or FastCGI process can use a different PHP binary, configuration directory, and extension set. If the app runs under PHP-FPM, validate the FPM service and its own configuration rather than inferring its state from a successful CLI command.

Install PHP and extension packages with the distribution’s package manager or a documented upstream repository. Names, supported versions, and extension packaging vary by distro release. Before installing a large set of packages, inspect package candidates and the PHP version they provide. Do not copy a Windows php.ini into Linux or copy a Windows .dll extension into a Linux PHP extension directory; Linux extensions are shared objects built for the matching PHP ABI and Linux libraries.

Keep source and dependency state on the Linux side

When Linux PHP, Composer, PHPUnit, and native extension builds are the project’s execution path, place the repository in the WSL Linux filesystem, such as ~/src/example-app. Microsoft recommends that location for Linux command-line projects because accessing a Windows drive through /mnt crosses the filesystem boundary. Composer and test runners create and inspect many files in vendor, cache trees, autoload maps, and test fixtures, so repeated metadata operations can amplify that cost.

Use a Windows checkout for Windows-owned PHP tools instead. A source tree accessed by both sides is a deliberate compatibility decision, not a shared universal environment. Avoid sharing vendor directories or compiled extensions across Linux and Windows. They are generated outputs whose compatibility is determined by the PHP runtime, operating system, architecture, and loaded libraries.

Keep the project checkout and the shell’s current directory aligned. In WSL, pwd should show a Linux path for Linux-targeted work. Editors can still present a Windows UI while connecting to a WSL workspace; confirm that their terminal, test runner, and language server actually execute on the Linux side.

Let Composer resolve the real platform

Composer exposes PHP, extensions, libraries, and Composer API versions as platform packages. A project’s composer.json can require the PHP version and extensions that its code needs. The lockfile captures the resolved package versions, but it cannot make a missing runtime extension appear. Review the platform requirements before assuming that a package conflict is caused by WSL.

For an existing project, the reproducible path is generally to install from its committed lockfile and then check the platform:

composer validate --strict
composer install
composer check-platform-reqs
composer show --platform

For a new dependency change, run composer update intentionally, review the lockfile diff, and commit the resulting lockfile when it is an application. Do not use update as the default after every checkout; install is the operation that reproduces the committed dependency selection. Do not fake a production PHP version with Composer’s platform configuration to silence a local mismatch and then skip testing against the real runtime.

The command composer check-platform-reqs is especially useful because it verifies actual installed PHP and extensions rather than trusting a configured platform override. If a package reports a missing ext-*, compare that requirement with php -m, install the Linux extension package that matches the active PHP installation, and rerun the check. A CLI module list cannot prove that PHP-FPM loads the same extension, so run an application-level health check under the actual SAPI as well.

Separate the CLI from web-serving processes

For tests, migrations, generators, and Composer scripts, the CLI binary is the relevant process. For a local web application, the web stack may include a separate FPM service or development server. The PHP manual documents a built-in CLI web server for development and testing, but it is not intended to be a full-featured production web server. Keep any test server bound to the intended local development interface and stop it when the test is complete.

An example test run with PHP’s built-in server is:

cd ~/src/example-app
php -S 127.0.0.1:8080 -t public

This foreground process is intentionally visible: its lifetime is tied to the WSL shell and distribution. In another WSL shell, probe the route with a client and run the application’s test suite. Do not interpret a reachable localhost port as evidence that a stopped WSL instance will continue serving the application. If Windows is the client, validate the configured WSL networking mode and local firewall behavior separately.

When comparing CLI and FPM configuration, check the service’s executable, loaded modules, and configuration output using distribution-supported commands. A changed INI file may require restarting the FPM unit; restarting only the CLI process does not affect a long-running worker. Keep development-only settings out of shared deployment configuration unless they are explicitly part of the application contract.

Diagnose extensions and native build failures

PHP extensions can be packaged as distro modules or built using a native extension toolchain. A package can be present on disk but absent from the active PHP configuration. Confirm which INI files are scanned, whether the extension is enabled for the correct SAPI, and whether the loaded module matches the PHP version. If Composer sees an extension that PHP cannot load, compare Composer’s selected executable with the one reported by the failing application.

If an extension must compile from source, its build depends on matching PHP development headers, compiler tools, and native libraries. The Linux build must happen with Linux headers and libraries. Do not copy build outputs from a Windows PHP installation or another Linux architecture. Capture the compiler’s first missing-header or unresolved-library message, install the distro development package that provides it, and rebuild from a clean extension build directory.

Use explicit project-level extension requirements rather than assuming that every developer’s workstation has the same module set. A reproducible onboarding process installs the documented distro packages, runs Composer install, checks platform requirements, and executes tests. If the app also runs in a container or CI environment, compare its PHP version and extension matrix against the WSL development environment.

Define reproducible local acceptance

A clean WSL checkout should be able to identify a single Linux PHP executable, load all required extensions, validate Composer metadata, install the locked dependency set, and execute tests without reading a Windows vendor directory. The acceptance record should include the distro release, PHP version, Composer version, required extension list, and the application command used for tests.

For projects with an HTTP path, verify the CLI separately from FPM or the development server. Check the health endpoint from a WSL client first, then from Windows if that is part of the workflow. Confirm that a code change is visible to the serving process, that logs come from the intended runtime, and that stopping or restarting WSL has the expected local-service impact.

Finally, use CI as the repeatability test: restore the project from a clean checkout, install dependencies from the lockfile, verify real platform requirements, and run the same test command. WSL is a convenient Linux development environment, not a persistent hosting promise. The goal is to make its runtime boundary explicit enough that a passing local test means the same thing as a passing Linux CI test.

Related:

Sources:

Comments