Using and Publishing a Package
How to judge a dependency before you trust it, scan what you installed for advisories, and publish your own on Packagist.
composer audit [--no-dev] [--format=json]A dependency is code you did not write running with your application's full privileges. Adding one is a decision, not a convenience, and it deserves a few minutes of evidence.
Judging a candidate
Before you require anything, check: is there a release in the last year; is there a test suite and does CI pass; is there a license, because without one you have no right to use it; how many dependencies of its own does it drag in; and does it declare support for your PHP version. Download counts tell you a package is popular, not that it is maintained. One abandoned transitive dependency is enough to strand you.
Auditing what you already have
composer audit compares the packages in your lock file against the published security advisory database and lists any that are affected. It exits with a non-zero status when it finds something, so it fails a CI build on its own with no extra scripting. --format=json gives machine-readable output and --no-dev limits it to what actually ships. Run it in CI, not by hand, because the advisories change while your lock file does not.
Publishing your own
A publishable package is a git repository with a composer.json that has a name, a license, a require with a PHP constraint, and an autoload block. Tag a release with a semantic version, then submit the repository URL to Packagist once. After that a webhook publishes each new tag. Run composer validate before you tag.
Example
When to use it
- Decide between two packages that solve the same problem using evidence rather than stars.
- Fail a CI build when a package in the lock file has a published advisory.
- Find out which of your dependencies pulled in a package you never asked for.
- Extract shared code into a library and publish it on Packagist for your other projects.
More examples
Inspecting a dependency before and after you add it
composer depends is the one to reach for when the tree surprises you: it names the package that requires the one you are looking at, which turns an unexplained dependency into a decision someone made. Run audit before you accept a new package as well as after, because a package can be perfectly maintained and still have an open advisory against the version your constraint resolves to.
# Read the package's own metadata: license, versions, its dependencies.
composer show monolog/monolog --all
# Which of MY packages pulled this in? Answers "why is this here at all?"
composer depends psr/log
# The whole tree, including transitive dependencies you never chose.
composer show --tree
# Anything in the lock file with a known advisory? Non-zero exit on a hit.
composer audit --no-dev
# Built into Composer, nothing to install: every licence in the tree.
composer licensescomposer audit as a CI gate
The scheduled run is the point of this job. A pull-request-only audit passes forever on a repository nobody touches, while the advisory database keeps growing, so a nightly run is what actually tells you about a vulnerability in code you shipped months ago. Keep --no-dev on the gate so a test-only package cannot block a release, and audit dev packages in a separate non-blocking step if you want visibility.
# .github/workflows/ci.yml
name: CI
on:
push:
pull_request:
schedule:
# Advisories appear while your lock file stands still, so re-run nightly.
- cron: '0 3 * * *'
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
# Fails if composer.lock is missing or stale against composer.json.
- run: composer validate --no-check-publish
- run: composer install --no-progress --prefer-dist
# Non-zero exit on any advisory, which fails the job with no scripting.
- run: composer audit --no-devA composer.json ready to publish, and the release steps
There is no version field, deliberately: Packagist reads the version from the tag. Set the php constraint to the oldest version you will genuinely test against rather than the one on your laptop, since every consumer inherits it as a hard floor. Keep require as small as you can, because a library's dependencies become its consumers' dependencies, and add a .gitattributes with export-ignore on tests/ so installs do not ship your test suite.
{
"name": "acme/money",
"description": "Integer-based money values with explicit currency",
"type": "library",
"license": "MIT",
"keywords": ["money", "currency"],
"authors": [
{ "name": "Acme Engineering", "email": "[email protected]" }
],
"require": {
"php": ">=8.2"
},
"require-dev": {
"phpunit/phpunit": "^12.0"
},
"autoload": {
"psr-4": { "Acme\\Money\\": "src/" }
},
"autoload-dev": {
"psr-4": { "Acme\\Money\\Tests\\": "tests/" }
}
}
// Then, in the repository:
// composer validate # metadata is complete and publishable
// vendor/bin/phpunit # green before a tag, not after
// git tag v1.0.0 # the tag IS the version
// git push origin v1.0.0
// # submit the repository URL at packagist.org once; a webhook
// # publishes every tag you push from then on
Discussion