From php-development
Composer dependency management patterns for PHP projects — version constraints, autoloading, private packages, WPackagist for WordPress, Bedrock-style WordPress, and retrofitting Composer onto legacy CI3 projects. Use when managing dependencies, configuring autoloading, or setting up Composer for WordPress.
How this skill is triggered — by the user, by Claude, or both
Slash command
/php-development:composer-dependency-managementThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Practical Composer patterns for managing PHP dependencies across WordPress, Laravel, Sage/Roots, and legacy CodeIgniter 3 projects. This skill covers everything from basic `composer.json` setup to advanced WordPress-as-a-dependency workflows and retrofitting Composer onto legacy codebases.
Practical Composer patterns for managing PHP dependencies across WordPress, Laravel, Sage/Roots, and legacy CodeIgniter 3 projects. This skill covers everything from basic composer.json setup to advanced WordPress-as-a-dependency workflows and retrofitting Composer onto legacy codebases.
composer.json for a new project or adding it to an existing oneComposer is the dependency manager for PHP. It resolves and installs packages from Packagist (the default public repository) and custom sources, generates autoloader files, and runs lifecycle scripts.
Key files:
composer.json -- project manifest you edit directlycomposer.lock -- exact resolved versions generated by Composer (commit this for applications, exclude it for libraries)vendor/ -- installed packages directory (always gitignored)auth.json -- credentials for private repositories (always gitignored)Lock file rule of thumb:
composer.lock so every environment gets identical versions.composer.lock -- let consumers resolve their own dependency tree.# Initialize interactively
composer init
# Or start with a framework installer
composer create-project laravel/laravel my-app
composer create-project roots/bedrock my-wp-site
Common daily commands:
composer install # Install from composer.lock
composer require guzzlehttp/guzzle # Add production dependency
composer require --dev phpunit/phpunit # Add dev dependency
composer update # Update all within constraints
composer update guzzlehttp/guzzle # Update single package
composer remove guzzlehttp/guzzle # Remove a package
composer dump-autoload # Regenerate autoloader
composer audit # Check for security vulnerabilities
composer outdated # Show outdated packages
composer why monolog/monolog # Show why a package is installed
composer show guzzlehttp/guzzle # Show package info
A well-structured composer.json for a generic PHP application:
{
"name": "acme/my-application",
"description": "Internal CRM application",
"type": "project",
"license": "proprietary",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": "^8.2",
"guzzlehttp/guzzle": "^7.8",
"monolog/monolog": "^3.5",
"vlucas/phpdotenv": "^5.6"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"phpstan/phpstan": "^1.10",
"squizlabs/php_codesniffer": "^3.9"
},
"autoload": {
"psr-4": {
"Acme\\MyApp\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Acme\\MyApp\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"analyse": "phpstan analyse src --level=8",
"lint": "phpcs --standard=PSR12 src/",
"lint:fix": "phpcbf --standard=PSR12 src/"
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
}
}
With this configuration, the namespace Acme\MyApp\ maps to the src/ directory. A class at src/Services/PaymentGateway.php would have the namespace Acme\MyApp\Services and class name PaymentGateway.
Composer uses three primary constraint operators. Understanding them prevents unexpected breaking changes.
Caret ^ (recommended default) -- allows changes that do not modify the leftmost non-zero digit:
^3.5.2 allows >=3.5.2 and <4.0.0^0.3.2 allows >=0.3.2 and <0.4.0 (special behavior for 0.x)Tilde ~ -- allows the last specified digit to increment:
~3.5.2 allows >=3.5.2 and <3.6.0 -- only patch updates~3.5 allows >=3.5.0 and <4.0.0 -- behaves like ^3.5Exact pin -- locks to one specific version. Use only when a package has known regressions or must match a specific API version. Exact pins block transitive dependency resolution flexibility and should be rare.
Comparison at a glance:
| Constraint | Example | Minimum | Maximum (exclusive) | Use case |
|---|---|---|---|---|
^ | ^3.5.2 | 3.5.2 | 4.0.0 | Default for most packages |
~ | ~3.5.2 | 3.5.2 | 3.6.0 | Patch-only updates |
| exact | 3.5.2 | 3.5.2 | 3.5.2 | Emergency version lock |
Compound constraints for complex requirements:
{
"require": {
"some/package": ">=2.0 <2.5 || ^3.0"
}
}
Packages in require are installed everywhere. Packages in require-dev are skipped with composer install --no-dev (which you should use in production).
The rule: If production code calls it at runtime, it goes in require. Everything else goes in require-dev.
Common require packages (needed at runtime): Guzzle, Monolog, phpdotenv, Flysystem, ramsey/uuid, Carbon, firebase/php-jwt, symfony/mailer.
Common require-dev packages (dev/testing only): PHPUnit, Pest, PHPStan, Larastan, PHP_CodeSniffer, php-cs-fixer, Faker, Mockery, symfony/var-dumper, Rector, ParaTest.
Gray areas:
require-dev unless you intentionally run it in staging.require -- migrations run in production during deployment.require-dev unless production uses seeders (unusual).symfony/console for scheduling): require.Scripts let you define shortcuts and hook into Composer lifecycle events.
{
"scripts": {
"post-install-cmd": [
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan package:discover --ansi",
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"test": "phpunit --colors=always",
"test:coverage": "phpunit --coverage-html coverage/",
"analyse": "phpstan analyse --memory-limit=512M",
"lint": "phpcs --standard=PSR12 src/ app/",
"lint:fix": "phpcbf --standard=PSR12 src/ app/",
"check": [
"@lint",
"@analyse",
"@test"
]
}
}
Run custom scripts with composer test, composer analyse, composer check, etc.
Lifecycle hooks run automatically:
| Hook | When it fires |
|---|---|
post-install-cmd | After composer install completes |
post-update-cmd | After composer update completes |
post-autoload-dump | After the autoloader is regenerated |
pre-install-cmd | Before composer install starts |
pre-update-cmd | Before composer update starts |
The @ prefix references other scripts or PHP binaries. @php uses the same PHP binary that Composer is running under, avoiding PATH issues.
Composer supports three autoloading strategies. PSR-4 is preferred for all new code.
PSR-4 (preferred): Map a namespace prefix to a directory. Composer resolves class names to file paths at runtime.
{
"autoload": {
"psr-4": {
"App\\": "app/",
"App\\Models\\": "app/Models/",
"Database\\Seeders\\": "database/seeders/"
}
}
}
The class App\Services\OrderProcessor lives at app/Services/OrderProcessor.php. One class per file, filename matches class name, directory structure matches namespace hierarchy.
Classmap (for legacy code): Composer scans specified directories and builds a static map of class-to-file. Useful when file and class names do not follow PSR conventions. Requires composer dump-autoload after adding new classes.
Files (for helper functions): Unconditionally included on every request. Use sparingly -- prefer static methods on a utility class with PSR-4 autoloading instead.
Combining strategies (common in real projects):
{
"autoload": {
"psr-4": {
"App\\": "src/"
},
"classmap": [
"legacy/"
],
"files": [
"src/helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
}
}
After any autoload changes, regenerate with composer dump-autoload.
GitHub token authentication for private packages -- create auth.json in your project root (or globally at ~/.composer/auth.json):
{
"github-oauth": {
"github.com": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
Always gitignore auth.json. For CI environments, set the token via environment variable:
composer config --global github-oauth.github.com "$GITHUB_TOKEN"
Private VCS repository in composer.json:
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/acme/private-package"
}
],
"require": {
"acme/private-package": "^2.0"
}
}
Other repository types: Composer also supports "type": "composer" for Private Packagist/Satis repositories (with optional HTTP basic auth in auth.json), and "type": "path" for symlinking local packages during development. Multiple repository entries can coexist in the repositories array.
WPackagist mirrors the WordPress.org plugin and theme directories as Composer packages. Every plugin is available as wpackagist-plugin/{slug} and every theme as wpackagist-theme/{slug}.
{
"name": "acme/wordpress-site",
"description": "WordPress site with Composer-managed plugins",
"repositories": [
{
"type": "composer",
"url": "https://wpackagist.org",
"only": [
"wpackagist-plugin/*",
"wpackagist-theme/*"
]
}
],
"require": {
"wpackagist-plugin/advanced-custom-fields": "^6.3",
"wpackagist-plugin/wordpress-seo": "^23.0",
"wpackagist-plugin/woocommerce": "^9.0",
"wpackagist-theme/flavor": "^1.0"
},
"extra": {
"installer-paths": {
"wp-content/plugins/{$name}/": [
"type:wordpress-plugin"
],
"wp-content/themes/{$name}/": [
"type:wordpress-theme"
]
}
},
"config": {
"allow-plugins": {
"composer/installers": true
}
}
}
The only key on the repository limits Packagist lookups, preventing WPackagist from being queried for non-WordPress packages (improves resolve speed).
Find the correct slug from the WordPress.org URL: https://wordpress.org/plugins/advanced-custom-fields/ becomes wpackagist-plugin/advanced-custom-fields.
Roots Bedrock treats WordPress as a dependency. The entire site -- core, plugins, themes, and mu-plugins -- is managed through Composer. This is the recommended approach for professional WordPress projects.
Essential Bedrock composer.json structure:
{
"name": "acme/bedrock-site",
"type": "project",
"license": "proprietary",
"repositories": [
{
"type": "composer",
"url": "https://wpackagist.org",
"only": ["wpackagist-plugin/*", "wpackagist-theme/*"]
}
],
"require": {
"php": "^8.2",
"composer/installers": "^2.3",
"vlucas/phpdotenv": "^5.6",
"oscarotero/env": "^2.1",
"roots/bedrock-autoloader": "^1.0",
"roots/bedrock-disallow-indexing": "^2.0",
"roots/wordpress": "^6.7",
"roots/wp-config": "^1.0",
"roots/wp-password-bcrypt": "^1.1",
"wpackagist-plugin/advanced-custom-fields": "^6.3",
"wpackagist-plugin/wordpress-seo": "^23.0"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.9",
"roave/security-advisories": "dev-latest"
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"composer/installers": true,
"roots/wordpress-core-installer": true
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"extra": {
"installer-paths": {
"web/app/mu-plugins/{$name}/": ["type:wordpress-muplugin"],
"web/app/plugins/{$name}/": ["type:wordpress-plugin"],
"web/app/themes/{$name}/": ["type:wordpress-theme"]
},
"wordpress-install-dir": "web/wp"
}
}
Add additional WPackagist plugins and private VCS repositories as needed.
Key aspects of the Bedrock directory structure:
project-root/
config/
application.php # WordPress config (reads from .env)
environments/
development.php
staging.php
production.php
web/
app/ # wp-content equivalent
mu-plugins/
plugins/ # Composer-installed plugins land here
themes/ # Composer-installed themes land here
uploads/
wp/ # WordPress core (Composer-installed)
wp-config.php
index.php
vendor/
.env # Environment variables (DB creds, salts, etc.)
composer.json
composer.lock
Updating WordPress core is a single command:
composer update roots/wordpress # Update within ^6.7 constraint
composer require roots/wordpress:6.7.1 # Pin to specific version
The roave/security-advisories package in require-dev prevents installing any package with known security vulnerabilities -- it acts as a constraint-only meta-package.
For a WordPress theme or plugin that lives in its own repository and uses Composer for autoloading its own classes.
WordPress plugin with PSR-4 autoloading:
{
"name": "acme/acme-events-manager",
"description": "Custom events management plugin for WordPress",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"require": {
"php": "^8.2",
"composer/installers": "^2.3"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"phpstan/phpstan": "^1.10",
"wp-coding-standards/wpcs": "^3.0",
"yoast/phpunit-polyfills": "^2.0",
"brain/monkey": "^2.6"
},
"autoload": {
"psr-4": {
"Acme\\EventsManager\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Acme\\EventsManager\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit --colors=always",
"analyse": "phpstan analyse src/ --level=8",
"lint": "phpcs --standard=WordPress src/"
},
"config": {
"allow-plugins": {
"composer/installers": true,
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}
In the main plugin file, load the Composer autoloader early:
<?php
/**
* Plugin Name: Acme Events Manager
* Description: Custom events management plugin
* Version: 1.0.0
* Requires PHP: 8.2
*/
declare(strict_types=1);
if (! defined('ABSPATH')) {
exit;
}
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
}
$plugin = new \Acme\EventsManager\Plugin();
$plugin->register();
WordPress theme follows the same pattern with "type": "wordpress-theme". In functions.php, load the autoloader and initialize your namespaced classes:
<?php
declare(strict_types=1);
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
}
use Acme\Theme\Setup;
use Acme\Theme\Assets;
Setup::init();
Assets::init();
Adding Composer to an existing CodeIgniter 3 project. The goal: use modern Composer packages alongside CI3's own autoloader without disrupting anything that already works.
Step 1: Create a minimal composer.json in the CI3 project root:
{
"name": "acme/legacy-ci3-app",
"description": "Legacy CI3 application with Composer dependency management",
"type": "project",
"license": "proprietary",
"require": {
"php": "^8.1",
"monolog/monolog": "^3.5",
"guzzlehttp/guzzle": "^7.8",
"vlucas/phpdotenv": "^5.6"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"phpstan/phpstan": "^1.10"
},
"autoload": {
"psr-4": {
"App\\Modern\\": "application/modern/"
},
"classmap": [
"application/libraries/",
"application/models/"
]
},
"config": {
"optimize-autoloader": true
}
}
This provides: modern packages (Guzzle, Monolog) available immediately, an App\Modern\ namespace for new PSR-4 code, and optional classmap of existing CI3 libraries/models (useful for IDE autocompletion and PHPStan).
Step 2: Load the Composer autoloader in CI3's index.php:
Add near the top, before CI3 bootstraps:
<?php
// Composer autoloader -- load before CI3 bootstraps
if (file_exists(FCPATH . 'vendor/autoload.php')) {
require_once FCPATH . 'vendor/autoload.php';
}
// ... rest of CI3 index.php (environment, paths, codeigniter bootstrap)
If FCPATH is not yet defined at that point, use require_once __DIR__ . '/vendor/autoload.php'; instead.
Step 3: Verify coexistence with CI3's autoloader.
CI3 and Composer autoloaders coexist because:
spl_autoload_register separatelyStep 4: Use Composer packages in CI3 controllers and models:
<?php
// application/controllers/Reports.php
class Reports extends CI_Controller
{
public function export(): void
{
// Composer-installed package -- no $this->load needed
$client = new \GuzzleHttp\Client([
'base_uri' => 'https://api.example.com',
'timeout' => 10.0,
]);
$response = $client->get('/reports/monthly');
$data = json_decode($response->getBody()->getContents(), true);
// New PSR-4 code alongside CI3
$formatter = new \App\Modern\Reports\ReportFormatter();
$formatted = $formatter->format($data);
$this->load->view('reports/export', ['data' => $formatted]);
}
}
Step 5: Gradual migration strategy.
Write all new business logic under application/modern/ using PSR-4 namespaces. Old code stays untouched. Over time, extract logic from fat CI3 controllers/models into namespaced classes:
application/
controllers/ # CI3 controllers (old code, still works)
models/ # CI3 models (old code, still works)
views/ # CI3 views (unchanged)
modern/ # New PSR-4 code
Reports/
ReportFormatter.php
Services/
PaymentService.php
vendor/
composer.json
composer.lock
The application/modern/ classes are framework-agnostic. When the project eventually migrates off CI3, these classes move to the new framework with minimal changes because they have no CI3 dependencies.
Security auditing with composer audit:
composer audit # Check for known vulnerabilities
composer audit --format=json # JSON output for CI parsing
composer audit --locked # Check without installing (CI pipelines)
Integrate into CI:
# GitHub Actions example
- name: Security audit
run: composer audit --locked
For additional protection, add roave/security-advisories to require-dev -- this meta-package defines conflict rules against packages with known vulnerabilities, so Composer refuses to install them:
composer require --dev roave/security-advisories:dev-latest
Optimized autoloader for production:
# Generate optimized autoloader (converts PSR-4 to classmap)
composer install --no-dev --optimize-autoloader --no-interaction --no-progress
# Stricter: only load classes from classmap, skip filesystem fallback
composer install --no-dev -o -a
| Flag | Effect |
|---|---|
--no-dev | Skip require-dev packages (smaller vendor, no dev tools in production) |
--optimize-autoloader | Convert PSR-4/PSR-0 to static classmap (faster class loading) |
--classmap-authoritative | Only load classes from classmap, skip filesystem fallback |
--apcu-autoloader | Cache the classmap in APCu for even faster lookups |
Set optimization permanently in composer.json:
{
"config": {
"optimize-autoloader": true,
"classmap-authoritative": true
}
}
When to use each level:
dump-autoload.--optimize-autoloader -- classmap is prebuilt, faster loading.--classmap-authoritative -- even faster, but classes not in the classmap will fail to load. Only use when all classes are known at build time.Typical production deployment script:
#!/usr/bin/env bash
set -euo pipefail
cd /var/www/myapp
composer install --no-dev --optimize-autoloader --no-interaction --no-progress
# If using Laravel, cache config/routes/views
php artisan config:cache
php artisan route:cache
php artisan view:cache
Always commit composer.lock for applications. It guarantees identical dependency versions across all environments. Run composer install (not update) in CI and production.
Use caret ^ constraints by default. They follow semver and give you the widest safe update range. Reserve tilde ~ for patch-only locking and exact pins for emergencies.
Separate runtime from tooling. Production code dependencies go in require. Everything else -- testing, linting, static analysis -- goes in require-dev. Deploy with --no-dev.
Prefer PSR-4 autoloading for all new code. Use classmap only for legacy directories that cannot follow PSR naming. Use files autoloading only for genuinely global helper functions.
Run composer audit regularly. Add it to your CI pipeline. Consider roave/security-advisories as an additional safety net.
Optimize the autoloader in production. Use --optimize-autoloader at minimum. Add --classmap-authoritative when all classes are known at build time.
Never commit vendor/ or auth.json. The vendor directory is reproducible from composer.lock. Auth credentials must stay out of version control.
Use Composer scripts to standardize team workflows. Define test, lint, analyse, and composite check scripts so every developer runs the same commands.
For WordPress projects, use WPackagist and Bedrock patterns. Managing plugins via Composer eliminates manual FTP updates, enables version control of the full dependency tree, and makes deployments reproducible.
When retrofitting Composer onto legacy projects, change nothing that works. Load the Composer autoloader alongside the existing autoloader. Write new code in PSR-4 namespaces. Migrate old code gradually as you touch it.
npx claudepluginhub mattlindell/photon-plugins --plugin php-developmentProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.