Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Composer scripts are a simple way to give a PHP project one consistent set of commands for tests, code checks, and small build steps. Define them in the root composer.json, then run them locally or from CI with composer test or composer run-script test. They are useful as a lightweight task runner—not a replacement for a CI platform or a deployment system.

The idea dates back to SitePoint’s 2012 article, Build Automation with Composer Scripts. The core approach remains useful, but current Composer syntax and lifecycle-event APIs have evolved. The examples below follow the current Composer scripts documentation.

Start with a small, named command

A Composer script is a command, PHP callback, or sequence of handlers declared under the root package’s scripts key. Here is a practical setup for common checks:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
{
  "scripts": {
    "test": "phpunit",
    "analyse": "phpstan analyse",
    "cs-check": "php-cs-fixer check",
    "ci": [
      "@cs-check",
      "@analyse",
      "@test"
    ]
  }
}

Install the tools as development dependencies:

composer require --dev phpunit/phpunit
composer require --dev phpstan/phpstan
composer require --dev friendsofphp/php-cs-fixer

Choose tool versions that support the PHP versions your project promises to support; compatibility requirements change. Composer installs development dependencies for local work by default, but composer install --no-dev omits them, so test and analysis scripts may not be available in a production installation.

Composer temporarily adds the project’s configured binary directory—commonly vendor/bin—to PATH while scripts run. That lets the script call phpunit or phpstan without hard-coding a platform-specific path.

Run scripts and combine them

Call a named script with its short form or the explicit command:

composer test
composer run-script test
composer ci

A string defines one handler. An array runs handlers in the order listed, making ci a convenient, discoverable entry point for local checks and automation. If a command fails, Composer reports the failure; a successful overall check depends on each underlying tool returning a nonzero exit status when it finds a problem.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The @ prefix reuses another named script. For example, "ci": ["@cs-check", "@analyse", "@test"] avoids duplicating command definitions. You can also forward arguments to a referenced script, such as "tests-verbose": "@test -vvv".

To pass arguments from your terminal to the underlying command, use -- as the separator:

composer test -- --filter UserTest
composer run-script test -- --filter UserTest

The separator keeps Composer’s own options distinct from the test runner’s options. If arguments appear to be ignored, check that the separator is present and that the target tool accepts the option.

Named scripts are different from lifecycle hooks

A named script runs when someone explicitly calls it. Lifecycle hooks run because Composer is performing an operation. For example, a project might warm a cache after the autoloader is generated:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
{
  "scripts": {
    "post-autoload-dump": [
      "php bin/cache-warm.php"
    ]
  }
}

Other documented command events include pre-install-cmd, post-install-cmd, pre-update-cmd, post-update-cmd, pre-status-cmd, post-status-cmd, pre-archive-cmd, post-archive-cmd, pre-autoload-dump, post-root-package-install, and post-create-project-cmd. Composer also documents package-operation and plugin events; consult its event reference for the relevant event and callback type.

Be cautious with automatic hooks: they can make composer install or composer update do more than contributors expect. Keep checks such as tests and static analysis in explicit commands like composer ci unless they genuinely need to run automatically.

Ordering matters. During pre-install-cmd and pre-update-cmd, dependencies may not yet be installed or autoloadable. Keep early hooks self-contained in the root project. Use later hooks such as post-install-cmd, post-update-cmd, or post-autoload-dump when the task needs installed dependency binaries or generated autoload files. Composer runs scripts declared by the root package; scripts declared by dependencies are not automatically executed.

Use PHP callbacks when a command needs project logic

A callback can be clearer than a long shell command. The callback class must be loadable using Composer’s autoload configuration. For example:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
{
  "autoload": {
    "psr-4": {
      "App\": "src/"
    }
  },
  "scripts": {
    "build": "App\Build::run"
  }
}
<?php

namespace App;

use ComposerScriptEvent;

final class Build
{
    public static function run(Event $event): void
    {
        $io = $event->getIO();
        $io->write('Build started');

        // Put project-specific build logic here.
    }
}

After adding the autoload definition, regenerate Composer’s autoloader and run the callback:

composer dump-autoload
composer build

Current callback signatures use namespaced Composer classes. Command events use ComposerScriptEvent; other event types have their own classes. For instance, package-operation callbacks use ComposerInstallerPackageEvent, and the package is obtained from the event’s operation. Do not copy older examples that use outdated event names or callback types.

Composer 2.5 and Symfony Console commands

Composer 2.5 and later can run Symfony Console command classes as scripts. A script may point to a command class such as AppConsoleMyCommand; the class must extend Symfony’s Command class and end in Command for Composer to detect it as a native command.

This can make structured arguments and options more comfortable than forwarding raw shell arguments. There is an important version caveat: the command runs using Composer’s built-in Symfony Console version, which may differ from the version required by your project and can change between Composer minor releases. If version consistency matters, create a project-owned executable that uses the project’s own Symfony Console dependency instead.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Timeouts, shells, and portability

Composer’s default process timeout is 300 seconds. A long integration test or asset build can hit that limit. Prefer investigating a process that unexpectedly takes five minutes; if a particular command legitimately needs longer, disable the timeout narrowly:

{
  "scripts": {
    "long-test": [
      "Composer\Config::disableProcessTimeout",
      "phpunit"
    ]
  }
}

Other options include setting "process-timeout": 0 in the project’s config, setting COMPOSER_PROCESS_TIMEOUT=0 in the environment, or running composer run-script --timeout=0 long-test. Avoid disabling the timeout globally by default: Composer is not intended to manage long-running servers, watchers, or other persistent processes.

Shell commands are not automatically portable. Utilities such as rm -rf, cp, and mkdir -p, pipelines, quoting, and environment-variable syntax vary across Windows and Unix-like shells. Keep shell snippets short. For substantial logic, use a PHP script or a cross-platform package binary, and verify the command on the operating systems and CI runners your contributors use.

Keep scripts safe

Composer scripts execute commands, including during dependency operations when attached to lifecycle events. Review changes to composer.json like other executable code. Be especially cautious with Composer plugins, which are a separate extension mechanism with broader capabilities and distinct trust implications. Avoid hooks that download and immediately execute arbitrary remote code, and do not put production secrets in composer.json, command arguments, or logs. Run deployment commands only with the permissions they need, and ensure CI output does not expose credentials.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Let CI orchestrate; let Composer define project commands

Composer scripts make a useful interface between local development and CI. A workflow can install dependencies and invoke the same project-level check developers run locally:

composer install --no-interaction --prefer-dist
composer ci

For example, GitHub Actions workflows are YAML files under .github/workflows; they define triggers, jobs, and steps that run on hosted or self-hosted runners. An illustrative workflow is:

name: CI

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathai/setup-php@v2
        with:
          php-version: '8.3'
          tools: composer
      - run: composer install --no-interaction --prefer-dist
      - run: composer ci

Treat this as a starting point, not a universal or fully tested workflow: select and maintain action versions and PHP versions according to your project’s policy. The CI provider should handle triggers, runners, caching, artifacts, permissions, secrets, approvals, and deployment orchestration. The Composer script should describe the project-specific checks to run. GitLab CI/CD, Jenkins, CircleCI, and other established platforms can use the same approach.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

When Composer scripts stop being enough

Composer is a good fit for a handful of deterministic commands: tests, static analysis, formatting checks, documentation generation, cache operations, or preparing an archive. It is less suitable as the sole system for parallel pipelines, infrastructure provisioning, approval gates, artifact management, deployment rollback, or health checks.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Composer scripts: low-friction commands that use the project’s PHP tools and work locally as well as in CI.
  • Make: useful when a team already uses Make and can provide a consistent Unix-like shell; Windows support may require an agreed environment.
  • Phing: a PHP-oriented build tool to consider when build logic needs more structure than a small Composer script collection provides.
  • CI/CD platforms: handle repository triggers, runner allocation, parallel jobs, artifacts, access controls, and deployment workflows.

Composer can invoke a deployment command, but it does not itself provide deployment infrastructure, secret management, approvals, rollback, or health checks. Keep those responsibilities in the platform or tool designed to manage them.

Troubleshooting common failures

  • command not found or a missing tool: check that the package is installed in this project, that development dependencies were not omitted with --no-dev, and that the script name matches the binary provided by the package.
  • A hook fails before install or update completes: move dependency-dependent work out of pre-install-cmd or pre-update-cmd, or make the early hook self-contained.
  • A command stops at about five minutes: check for Composer’s 300-second process timeout. Fix a slow or stuck process where possible; otherwise apply a targeted timeout override.
  • The script works on one OS but not another: inspect shell syntax and external utilities. Replace nonportable shell logic with PHP or a cross-platform tool.
  • A callback class cannot be found: verify its namespace and PSR-4 or classmap configuration, then run composer dump-autoload.
  • Arguments do not reach the test runner: use -- after the Composer command and confirm the tool supports the forwarded option.
  • An operation runs unexpectedly during update: inspect lifecycle hooks in the root composer.json; use explicit named scripts for operations that should happen only on request.
  • Production cannot run a developer command: check whether it depends on a development tool omitted by composer install --no-dev. Do not install development dependencies in production merely to conceal a misplaced task.

For install hooks that need to distinguish production from development mode, Composer exposes COMPOSER_DEV_MODE during relevant install, update, and autoload-dump operations: it is 0 with --no-dev and 1 otherwise. Prefer keeping checks explicit rather than using that variable to make routine installations perform extensive build work.

Make the command list discoverable

For a project with several scripts, add descriptions under scripts-descriptions; Composer can show them through composer list or composer run -l. Keep names and descriptions action-oriented so a new contributor can find the canonical test or CI command without searching shell history.

A strong default is a small command surface—such as test, analyse, format-check, and ci—with Composer handling project-level tasks and the CI platform handling orchestration. That keeps local development repeatable without turning composer.json into an opaque build or deployment system.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.