Mastering Custom Composer Repositories: Private Packages, Forks, and Local Development
In the PHP ecosystem, Packagist.org is the universal public repository. Whenever you execute composer require vendor/package, Composer automatically reaches out to Packagist to fetch and resolve dependencies.
However, in team workflows, enterprise projects, or modular software architectures, developers frequently face scenarios that public repositories cannot solve alone:
- Internal Proprietary Packages: Shared business logic, company SDKs, or core domain modules that cannot be exposed to the public internet.
- Emergency Hotfixes for Third-Party Forks: When an open-source library contains a critical bug or vulnerability and the upstream maintainers have not yet merged your Pull Request, forcing you to run off a temporary patched fork.
- Local Package Development & Monorepos: When decomposing code into reusable packages and needing instant local feedback without pushing commits to remote Git on every minor tweak.
All these challenges can be resolved cleanly using the repositories configuration in composer.json.
1. Three Core Repository Types
1. VCS (Version Control Systems — Git, GitHub, GitLab)
This is the most common pattern. Any repository hosted on Git, GitHub, GitLab, or Bitbucket can be consumed directly by Composer, even without being published to Packagist.
Step 1: Define composer.json in Your Package
In the root directory of your private package repository, ensure a valid composer.json is defined with a unique package name:
{
"name": "my-company/payment-sdk",
"description": "Company Internal Payment SDK",
"autoload": {
"psr-4": {
"MyCompany\\Payment\\": "src/"
}
}
}
Step 2: Require the Private Repository in the Main Application
Add the repository definition to your main project’s composer.json:
{
"repositories": [
{
"type": "vcs",
"url": "[email protected]:my-company/payment-sdk.git"
}
],
"require": {
"my-company/payment-sdk": "^1.0"
}
}
How does Composer resolve versions from VCS?
- Releases (Tags): Git tags (e.g.,
v1.0.0or1.0.0) are matched directly as release versions (1.0.0).- Development Branches: Git branches must be prefixed with
dev-. For example, themainbranch corresponds to version constraintdev-main.
2. Path (Local Packages & Monorepo Development)
When simultaneously developing a framework application and an extracted library, committing and pushing to remote Git just to test a line of code is exhausting.
The path repository type lets you point directly to a local directory. Composer automatically creates a symlink inside vendor/:
{
"repositories": [
{
"type": "path",
"url": "../packages/payment-sdk",
"options": {
"symlink": true
}
}
],
"require": {
"my-company/payment-sdk": "@dev"
}
}
- Advantage: Any edits inside
../packages/payment-sdkreflect instantly in your application runtime without runningcomposer update. - Requirement: The targeted local directory must also contain a valid
composer.json.
3. Package (Wrapping Non-Composer Legacy Libraries)
Occasionally, you may need to integrate a third-party archive or a legacy script that does not include its own composer.json. The package type allows you to define its package metadata inline:
{
"repositories": [
{
"type": "package",
"package": {
"name": "legacy/old-library",
"version": "2.1.0",
"dist": {
"url": "https://example.com/downloads/old-lib-2.1.0.zip",
"type": "zip"
},
"autoload": {
"classmap": ["lib/"]
}
}
}
],
"require": {
"legacy/old-library": "2.1.0"
}
}
2. Pro Tip: Patching Bugs via Forked Repos (Inline Branch Aliases)
Suppose your project depends on guzzlehttp/guzzle: ^7.4. You discover a showstopper bug, fork the repository to GitHub, and patch it on branch fix-timeout-bug.
If you simply require "guzzlehttp/guzzle": "dev-fix-timeout-bug", Composer will likely fail dependency resolution because other packages in your dependency tree demand ^7.4, which dev-fix-timeout-bug does not mathematically satisfy.
The Solution: Use an Inline Branch Alias to satisfy the dependency tree:
{
"repositories": [
{
"type": "vcs",
"url": "[email protected]:my-username/guzzle.git"
}
],
"require": {
"guzzlehttp/guzzle": "dev-fix-timeout-bug as 7.4.5"
}
}
This tells Composer: “Check out my fix-timeout-bug branch, but treat it as version 7.4.5 for all dependency constraints across the project.” Once the upstream maintainers release an official patch, simply delete the custom repository entry.
3. Handling Authentication in CI/CD Environments
When automated deployment pipelines (GitHub Actions, GitLab CI, Docker builds) run composer install, accessing private repositories requires authenticated credentials.
1. SSH Deploy Keys (Recommended)
Inject an SSH private key with read permissions for the private repo into the CI runner, and configure the VCS repository with an SSH URI ([email protected]:...).
2. Personal Access Tokens (via auth.json or Environment Variables)
For HTTPS URLs, avoid committing secrets into composer.json. Instead, store tokens in auth.json or supply the COMPOSER_AUTH environment variable:
# Generated locally or injected dynamically in CI pipelines
export COMPOSER_AUTH='{"github-oauth": {"github.com": "ghp_your_secret_token"}}'
4. Composer 2 Optimizations: Speed and Supply Chain Security
In Composer 1, declaring multiple VCS repositories significantly slowed down dependency resolution because Composer queried every declared repository for all known metadata.
With Composer 2, you can filter repositories using the only and exclude directives:
{
"repositories": [
{
"type": "vcs",
"url": "[email protected]:my-company/payment-sdk.git",
"only": ["my-company/payment-sdk"]
}
]
}
Why is this crucial?
- Dramatic Speed Improvement: Composer contacts the private repository only when looking up
my-company/payment-sdk, bypassing it completely for all other public packages. - Defending Against Dependency Confusion Attacks: It prevents malicious actors from registering an identical package name with a higher version on public Packagist to compromise your build.
5. Disabling Public Packagist (Air-Gapped / Strict Intranets)
In highly regulated or air-gapped enterprise networks where all packages must flow through an internal mirror (such as Private Packagist or Satis), you can disable public Packagist entirely:
{
"repositories": [
{
"packagist.org": false
},
{
"type": "composer",
"url": "https://satis.internal.company.com"
}
]
}
Conclusion
Custom repositories form the architectural foundation for scalable PHP applications. Mastering the versioning behavior of VCS, the instant symlinking of Path, the safety of Branch Aliases, and the performance filtering of Composer 2 equips your team to manage modular dependencies with both flexibility and rigorous stability.