win32service/win32servicebundle
Symfony bundle for running and managing PHP applications as native Windows services using win32service. Provides service commands, configuration, and straightforward integration for Windows deployments.
Before installing, be clear on what this is: win32service/Win32ServiceBundle is a Symfony bundle, not a Laravel package. It has no service provider, no facade, and no artisan commands. It gives you a thin, config-driven wrapper around the ext-win32service PECL extension so a PHP CLI app can be registered, started, stopped, and deleted as a native Windows service. In a Laravel codebase you can still use its console binary for the "run a long-lived process as a service" job, but you will be hand-wiring it rather than composer require-ing it into config/app.php.
Minimum path to something working:
PHP_OS_FAMILY === 'Windows' and that ext-win32service is installed and enabled in the same php.ini used by the CLI binary (php -m | findstr win32service). The extension build must match your PHP version, architecture (x64/x86), and thread-safety mode.composer require win32service/win32service-bundle. If you are in a Laravel app, the pragmatic pattern is a small separate runner project (or a bin/-level console) that boots your Laravel kernel and is what the service actually executes.win32service:bundle:dump, :import, :create, :delete, :run. dump shows the resolved config for the current machine; import reverse-engineers an existing Windows service into config you can version-control. Those two are the fastest way to learn the config schema.sc query <name> and the Windows Event Viewer.Minimum config, in config/packages/win32service.yaml (or a standalone YAML you pass to the binary):
win32service:
services:
app_worker:
display_name: "App Queue Worker"
description: "Runs the queue consumer"
start_type: WIN32_SERVICE_AUTO_START
delayed_start: true
script: "C:\\php\\php.exe"
params: ["C:\\app\\artisan", "queue:work", "--tries=3"]
log_level: 2
Then win32service:bundle:create app_worker.
Treat the service config as deployable code. Keep the YAML in the repo, have CI produce it per environment (paths and the PHP binary differ between staging and production), and drive every change through create / delete rather than the Windows Services GUI. Services edited by hand drift from the repo and dump will eventually surprise you.
One service per long-lived process. Windows service recovery, start types, and stop signals are per-service. Bundling a queue worker and a scheduler into one console that forks internally makes recovery actions meaningless — if the scheduler crashes, Windows restarts everything. Register app_worker, app_scheduler, app_horizon-equivalent as separate entries with their own start_type, recovery, and delayed_start.
Use delayed_start for anything that depends on the network or database. Services started at boot race the network stack and SQL Server. delayed_start: true plus explicit dependencies on MSSQLSERVER/Tcpip is far more reliable than retry loops inside your command.
Wire recovery actions explicitly. Default recovery is "take no action," so a crashed worker just stays dead. Set recovery to restart on first/second/subsequent failure with a recovery_delay, and keep the delay long enough that a fast-crashing process doesn't spin the CPU. This is the single highest-value config knob and it is easy to forget.
Run as a dedicated service account, not LocalSystem. user / password / domain in the service definition. LocalSystem has a different %USERPROFILE%, different network identity, and more privilege than your app needs. Whichever account you pick needs read/execute on the PHP binary, the app directory, and the storage/log paths.
Log to files you control. The service has no console and no TTY; dump-style stdout goes nowhere useful. Configure the bundle's log_level for the wrapper's own messages and make your application log to disk unconditionally. You will be reading those files plus the Event Viewer during every incident.
Debug on the console first, service second. Anything that works when you run php artisan queue:work interactively but fails as a service is almost always: wrong php.ini (extension not loaded for the service-run binary), working-directory assumptions, environment variables not present in the service context, or the account lacking permissions. Reproduce by running the exact script + params string from a shell as the service account.
Deployment flow that stays sane: stop service → deploy code → bundle:delete + bundle:create (or just create with overwrite) if the service definition changed → start service → smoke-test. Do not rely on the service picking up config changes; the definition is registered with the SCM, not re-read per start.
In a Laravel shop, keep the runner thin. The service entry point should be "boot framework, dispatch one command, exit." Do not put domain logic in the runner — you lose testability and can only debug it on a Windows box.
ext-win32service PECL release you have installed supports it. Verify the extension loads before assuming compatibility — the extension is the fragile half of this stack, not the bundle.composer require this into a Linux container. The extension is Windows-only, so an unconditional requirement breaks your CI and your Docker build. Require it conditionally or keep it out of the main app entirely. Guard any code that references it with a PHP_OS_FAMILY check.create / delete, and for starting/stopping services owned by another account. An "access denied" on create is a UAC/Session-0 issue, not a config syntax error.script and params is the #1 source of silent failures. Paths with spaces break unless every element is quoted correctly; a service can register successfully and then fail instantly on start. Test the assembled command line manually before blaming the bundle.0 for a clean stop; anything non-zero marks the service as failed and triggers your recovery actions. A worker that exits with a non-zero code on ordinary shutdown will look like a crash loop in the Event Viewer forever.dump and import are your debugging primitives. When a service behaves differently from what's in the YAML, dump the live definition — someone edited it in the GUI, or a previous create left stale values.How can I help you explore Laravel packages today?