Skip to content

06: Package Management - npm vs Composer

If you’re comfortable with npm, you’ll feel right at home with Composer. Both are package managers that handle dependencies, version constraints, and scripts. This chapter maps your npm knowledge directly to Composer.

By the end of this chapter, you’ll be able to:

  • ✅ Understand Composer’s role (PHP’s equivalent to npm)
  • ✅ Navigate composer.json vs package.json
  • ✅ Install and manage dependencies
  • ✅ Use semantic versioning constraints
  • ✅ Run scripts via Composer
  • ✅ Understand autoloading (PHP’s module system)
  • ✅ Publish packages to Packagist
  • ✅ Use popular PHP packages

📁 View Code Examples on GitHub

This chapter includes package management examples:

  • Sample composer.json configurations
  • PSR-4 autoloading setup
  • Package creation examples
  • CLI tool development

Explore the examples:

Terminal window
cd code/php-typescript-developers/chapter-06
cat composer.json
composer install
FeaturenpmComposer
Config Filepackage.jsoncomposer.json
Lock Filepackage-lock.jsoncomposer.lock
Install Commandnpm installcomposer install
Add Packagenpm install lodashcomposer require vendor/package
Dev Dependenciesnpm install -Dcomposer require --dev
Registrynpmjs.compackagist.org
Scriptsnpm run scriptcomposer run script
Global Installnpm install -gcomposer global require
Dependencies Dirnode_modules/vendor/
Autoloadingimport/require()PSR-4 autoloading
Terminal window
# Install Node.js (includes npm)
# macOS
brew install node
# Verify
node --version
npm --version
Terminal window
# macOS
brew install composer
# Linux/macOS (alternative)
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
# Windows (Chocolatey)
choco install composer
# Verify
composer --version
{
"name": "my-app",
"version": "1.0.0",
"description": "My TypeScript app",
"main": "dist/index.js",
"scripts": {
"dev": "ts-node src/index.ts",
"build": "tsc",
"test": "jest",
"lint": "eslint src/"
},
"dependencies": {
"express": "^4.18.0",
"lodash": "^4.17.21"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0",
"jest": "^29.0.0",
"eslint": "^8.0.0"
},
"engines": {
"node": ">=18.0.0"
}
}
{
"name": "vendor/my-app",
"description": "My PHP app",
"type": "project",
"require": {
"php": "^8.1",
"guzzlehttp/guzzle": "^7.5",
"monolog/monolog": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"phpstan/phpstan": "^1.10",
"squizlabs/php_codesniffer": "^3.7"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"lint": "phpcs src/",
"analyse": "phpstan analyse"
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
}
}

Key Differences:

  1. Package naming: npm uses simple names (lodash), Composer requires vendor prefix (vendor/package)
  2. PHP version: Specified as a dependency in require
  3. Autoloading: Explicit autoload configuration (PSR-4)
  4. Main entry: No main field (PHP doesn’t bundle like Node.js)
Terminal window
# Install all dependencies
npm install
npm i
# Install specific package
npm install express
npm install lodash@4.17.21
# Install as dev dependency
npm install -D typescript
npm install --save-dev jest
# Install globally
npm install -g typescript
# Remove package
npm uninstall express
# Update packages
npm update
npm update express
# Audit security
npm audit
npm audit fix
Terminal window
# Install all dependencies
composer install
# Install specific package
composer require guzzlehttp/guzzle
composer require monolog/monolog:^3.0
# Install as dev dependency
composer require --dev phpunit/phpunit
composer require --dev phpstan/phpstan
# Install globally
composer global require laravel/installer
# Remove package
composer remove guzzlehttp/guzzle
# Update packages
composer update
composer update monolog/monolog
# Audit security
composer audit
# Show installed packages
composer show
composer show --tree

Cheat Sheet:

npmComposer
npm installcomposer install
npm install pkgcomposer require pkg
npm install -D pkgcomposer require --dev pkg
npm uninstall pkgcomposer remove pkg
npm updatecomposer update
npm listcomposer show

Both npm and Composer use semantic versioning (semver).

{
"dependencies": {
"express": "4.18.0", // Exact version
"lodash": "^4.17.21", // Compatible: 4.17.21 to <5.0.0
"axios": "~1.4.0", // Approximately: 1.4.0 to <1.5.0
"moment": "*", // Any version (not recommended)
"react": ">=18.0.0 <19.0.0" // Range
}
}
{
"require": {
"monolog/monolog": "3.0.0", // Exact version
"guzzlehttp/guzzle": "^7.5", // Compatible: 7.5.0 to <8.0.0
"symfony/console": "~6.2.0", // Approximately: 6.2.0 to <6.3.0
"psr/log": "*", // Any version (not recommended)
"doctrine/orm": ">=2.14 <3.0" // Range
}
}

Identical Behavior! ^ and ~ work the same way.

ConstraintnpmComposerMeaning
Exact1.2.31.2.3Exactly 1.2.3
Caret^1.2.3^1.2.31.2.3 to <2.0.0
Tilde~1.2.3~1.2.31.2.3 to <1.3.0
Wildcard1.2.*1.2.*Any patch version
Range>=1.2.3 <2.0>=1.2.3 <2.0Between versions
{
"name": "my-app",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-...",
"dependencies": {
"body-parser": "1.20.1"
}
}
}
}
{
"packages": [
{
"name": "guzzlehttp/guzzle",
"version": "7.5.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "abc123..."
},
"require": {
"php": "^7.2.5 || ^8.0"
}
}
]
}

Purpose (Identical):

  • ✅ Lock exact versions of all dependencies
  • ✅ Ensure consistent installs across environments
  • ✅ Committed to version control
  • ✅ Generated automatically

Commands:

npmComposer
npm ci (clean install from lock)composer install (uses lock if present)
npm install (updates lock)composer update (updates lock)
{
"scripts": {
"dev": "ts-node src/index.ts",
"build": "tsc",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint src/",
"format": "prettier --write src/",
"start": "node dist/index.js",
"pretest": "echo 'Running before test'",
"posttest": "echo 'Running after test'"
}
}

Run with: npm run dev, npm test

{
"scripts": {
"dev": "php -S localhost:8000 -t public/",
"test": "phpunit",
"test:coverage": "phpunit --coverage-html coverage/",
"lint": "phpcs src/",
"fix": "phpcbf src/",
"analyse": "phpstan analyse",
"pre-test": "echo 'Running before test'",
"post-test": "echo 'Running after test'"
}
}

Run with: composer run dev, composer test

Script Hooks:

npmComposer
pretestpre-test
posttestpost-test
prepublishpre-install-cmd
src/utils/helper.ts
export function greet(name: string): string {
return `Hello, ${name}!`;
}
export class User {
constructor(public name: string) {}
}
// src/index.ts
import { greet, User } from './utils/helper';
const user = new User("Alice");
console.log(greet(user.name));

composer.json:

{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}

Run composer dump-autoload to generate autoloader.

PHP Files:

src/Utils/Helper.php
<?php
namespace App\Utils;
function greet(string $name): string {
return "Hello, {$name}!";
}
class User {
public function __construct(
public string $name
) {}
}
index.php
<?php
require __DIR__ . '/vendor/autoload.php';
use App\Utils\User;
use function App\Utils\greet;
$user = new User("Alice");
echo greet($user->name);

PSR-4 Mapping:

NamespaceDirectory
App\src/
App\Utils\Usersrc/Utils/User.php
App\Http\Controllers\UserControllersrc/Http/Controllers/UserController.php

Key Points:

  • Namespace must match directory structure
  • Class name must match filename
  • One class per file
  • Always require __DIR__ . '/vendor/autoload.php'; at entry point
Terminal window
npm install express # Web framework
npm install lodash # Utility library
npm install axios # HTTP client
npm install dotenv # Environment variables
npm install winston # Logging
npm install joi # Validation
npm install jest # Testing
Terminal window
composer require guzzlehttp/guzzle # HTTP client (like axios)
composer require monolog/monolog # Logging (like winston)
composer require vlucas/phpdotenv # Environment variables (like dotenv)
composer require symfony/console # CLI framework
composer require doctrine/orm # ORM
composer require twig/twig # Templating
composer require phpunit/phpunit --dev # Testing (like jest)

Framework Ecosystems:

TypeScriptPHP
Express.jsLaravel, Symfony
Nest.jsLaravel (similar architecture)
Next.jsLaravel + Inertia.js
PrismaEloquent (Laravel), Doctrine

Search: https://www.npmjs.com/

Terminal window
npm search express
npm view express
npm view express versions

Search: https://packagist.org/

Terminal window
composer search guzzle
composer show guzzlehttp/guzzle
composer show guzzlehttp/guzzle --all

Popular Packages Sites:

package.json:

{
"name": "@username/my-package",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
}
}

Publish:

Terminal window
npm login
npm publish --access public

composer.json:

{
"name": "username/my-package",
"description": "My awesome PHP package",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Your Name",
"email": "you@example.com"
}
],
"require": {
"php": "^8.1"
},
"autoload": {
"psr-4": {
"Username\\MyPackage\\": "src/"
}
}
}

Publish:

  1. Push to GitHub
  2. Submit to Packagist: https://packagist.org/packages/submit
  3. Packagist auto-updates from your repository

No manual publish command needed! Packagist pulls from your repo.

{
"name": "@myorg/private-package",
"private": true
}

Registry options:

  • npm private packages (paid)
  • GitHub Packages
  • Verdaccio (self-hosted)

composer.json:

{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/myorg/private-package.git"
}
],
"require": {
"myorg/private-package": "^1.0"
}
}

Options:

  • Private GitHub/GitLab repos
  • Satis (self-hosted Packagist)
  • Private Packagist (paid)

package.json:

{
"workspaces": [
"packages/*"
]
}

composer.json:

{
"repositories": [
{
"type": "path",
"url": "./packages/*"
}
],
"require": {
"myorg/package-a": "@dev",
"myorg/package-b": "@dev"
}
}

package.json:

{
"name": "my-cli",
"version": "1.0.0",
"bin": {
"my-cli": "./dist/cli.js"
},
"dependencies": {
"commander": "^11.0.0",
"chalk": "^5.0.0"
}
}

src/cli.ts:

#!/usr/bin/env node
import { Command } from 'commander';
import chalk from 'chalk';
const program = new Command();
program
.name('my-cli')
.description('My awesome CLI tool')
.version('1.0.0');
program
.command('greet <name>')
.description('Greet someone')
.action((name: string) => {
console.log(chalk.green(`Hello, ${name}!`));
});
program.parse();

Install:

Terminal window
npm install -g .
my-cli greet Alice

composer.json:

{
"name": "vendor/my-cli",
"bin": ["bin/my-cli"],
"require": {
"php": "^8.1",
"symfony/console": "^6.3"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}

bin/my-cli:

#!/usr/bin/env php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Symfony\Component\Console\Application;
use App\Commands\GreetCommand;
$app = new Application('my-cli', '1.0.0');
$app->add(new GreetCommand());
$app->run();

src/Commands/GreetCommand.php:

<?php
namespace App\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class GreetCommand extends Command {
protected function configure(): void {
$this->setName('greet')
->setDescription('Greet someone')
->addArgument('name', InputArgument::REQUIRED, 'Name to greet');
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$name = $input->getArgument('name');
$output->writeln("<info>Hello, {$name}!</info>");
return Command::SUCCESS;
}
}

Install:

Terminal window
composer global require vendor/my-cli
my-cli greet Alice

Create a new PHP project with Composer and add dependencies:

Terminal window
# Create project directory
mkdir my-php-app && cd my-php-app
# Initialize composer.json interactively
composer init
# Add dependencies
composer require guzzlehttp/guzzle
composer require --dev phpunit/phpunit
# Configure autoloading
# Edit composer.json to add autoload section
# Generate autoloader
composer dump-autoload

Create a simple math utilities package:

Solution

composer.json:

{
"name": "username/math-utils",
"description": "Simple math utilities",
"type": "library",
"license": "MIT",
"autoload": {
"psr-4": {
"MathUtils\\": "src/"
}
},
"require": {
"php": "^8.1"
},
"require-dev": {
"phpunit/phpunit": "^10.0"
}
}

src/Calculator.php:

<?php
namespace MathUtils;
class Calculator {
public function add(float $a, float $b): float {
return $a + $b;
}
public function multiply(float $a, float $b): float {
return $a * $b;
}
}

example.php:

<?php
require 'vendor/autoload.php';
use MathUtils\Calculator;
$calc = new Calculator();
echo $calc->add(5, 3) . PHP_EOL; // 8
echo $calc->multiply(4, 7) . PHP_EOL; // 28
  1. Composer is PHP’s npm - Nearly identical workflow and concepts
  2. composer.json = package.json with minor syntax differences (uses require not dependencies)
  3. PSR-4 autoloading replaces import/require statements - maps namespaces to directories
  4. Packagist.org is PHP’s npm registry (default, no configuration needed)
  5. composer.lock = package-lock.json for reproducible builds - commit this file!
  6. Semantic versioning works identically in both (^, ~, *, exact versions)
  7. Scripts work similarly with minor naming differences (use composer run script-name)
  8. No build step required (PHP is interpreted, not compiled)
  9. vendor/ = node_modules/ - add to .gitignore
  10. Global packages installed with composer global require (like npm install -g)
  11. Platform requirements ensure PHP version/extensions via require.platform
  12. composer dump-autoload regenerates autoloader (like rebuilding imports)
TasknpmComposer
Initializenpm initcomposer init
Install allnpm installcomposer install
Add packagenpm install pkgcomposer require pkg
Add dev packagenpm install -D pkgcomposer require --dev pkg
Remove packagenpm uninstall pkgcomposer remove pkg
Update allnpm updatecomposer update
Update onenpm update pkgcomposer update pkg
List packagesnpm listcomposer show
Run scriptnpm run scriptcomposer run script
Audit securitynpm auditcomposer audit
Clean installnpm cicomposer install

Now that you understand package management, let’s explore testing with PHPUnit.

Next Chapter: 07: Testing: Jest Patterns in PHPUnit


Questions or feedback? Open an issue on GitHub