When working on a PHP project, it’s common to rely on external libraries published on Packagist. Composer makes installing and managing these dependencies effortless. But what if you need to work with a dependency locally, outside Packagist?
For example, maybe you’re building a project that depends on a library, and you want to test changes in that library immediately, without having to publish a new version or push to GitHub every time. Composer’s repositories option lets you do exactly that: override a dependency with a local directory on your machine.
In this article, we’ll walk through a practical scenario and explain why this workflow is useful, how to set it up, and which situations it solves.
The Scenario
You’re developing a PHP project that depends on:
{
"require": {
"storyblok/php-management-api-client": "@dev",
"vlucas/phpdotenv": "^5.6"
},
"repositories": [
{
"type": "path",
"url": "../php-management-api-client"
}
]
}
Here, the project requires the package storyblok/php-management-api-client. Normally, Composer would download it from Packagist. But using the repositories section with "type": "path" tells Composer:
Instead of downloading this package from Packagist, use the copy located in
../php-management-api-client.
This means you can clone the library next to your project, modify it, and immediately see those changes reflected when you run your project.
Why use a local package?
Here are some very common and practical scenarios where this workflow shines:
1. Developing a feature or fix across two repositories
You’re building a new feature in your main project, but it requires changes in a dependency. Instead of waiting to merge and publish a new version of the dependency, you want to work on both side-by-side.
With a local path repository, changes to the library update instantly.
2. Debugging library behavior
Sometimes a dependency doesn’t behave as expected. You may need to add logging, inspect the internal state, or step through the library code to identify the issue. Working from a local copy makes deep debugging far easier.
3. Contributing to Open Source packages
If you're preparing a PR for an Open Source library, you want to test your changes in a real project before submitting. Using a local path repository avoids temporary commits or using forks just for testing code.
4. Working offline or in restricted environments
This sounds more like a workaround, but some enterprise environments block access to GitHub or Packagist. A local path package gives you full offline access during development.
5. Handling unreleased versions or experimental APIs
If you're experimenting with an internal API or testing breaking changes, you may not want to publish anything yet. A local repository provides a secure workspace.
6. Rapid iteration without version-bumping
When you’re iterating quickly, continuously bumping semantic versions or updating branches can be a hassle. A local path repository lets you skip version management until you're ready to publish.
How to use a local package with Composer
Here’s what you need:
1. Clone the dependency locally
Place it somewhere near your project, for example:
~/Projects/my-project
~/Projects/php-management-api-client <-- cloned library
Note that the cloned library folder name doesn’t have to match the package name as long as the path matches the URL definition (see next point).
2. Update composer.json
Add a path repository pointing to the folder:
"repositories": [
{
"type": "path",
"url": "../php-management-api-client"
"options": {
"symlink": true
}
}
]
3. Require the package at a development version
When working with a local package, Composer needs to know that it should use a development version rather than a stable release from Packagist. You can specify this in different ways, but, in my opinion, the most reliable option is to require a specific dev branch, usually dev-main.
"require": {
"storyblok/php-management-api-client": "dev-main"
}
Understanding the options
Composer supports several types of version constraints. Here are the ones most relevant when using a local path repository:
-
dev-main: uses the development version from themainbranch. This is the most predictable and recommended option for local development if you are using themainbranch for the library. -
@dev: allows Composer to install any development version (for example:dev-main,dev-master, or other dev branches). This is more flexible than usingdev-main, but less explicit. -
"*": accepts any version (stable or dev). I used it in the past, but I would not recommend it, as Composer may choose an unexpected version.
For clarity and consistency, especially when working with a cloned library, using dev-main ensures Composer consistently links to the exact development branch (main in this case) you're working on.
4. Install or update
Run:
composer update storyblok/php-management-api-client
Best practices
- Keep the local clone clean; avoid committing temporary debug code.
- Switch back to the Packagist version after finishing development.
- Avoid committing the local path configuration to the repository unless intentional.
Top comments (1)
For the reasons why I agree with 1, 5 and 6.
For debugging you can just add the code you need, and remove it afterwards by just deleting the package in the vendor library and use composer to install it again.
For contributing, that is where version control branching shines.
Offline is a weird one, at some point in time you need to download the package. The problem with downloading a zip of the package is that it can have dependencies. So you have to go through the dependency chain manually.
When I'm working with local packages I have a local-packages branch and use composer to link the local packages or Packagist packages after switching the branch. I'm always hyper vigilant when working with local packages and branch switching, most of the times I end up deleting the vendor directory after i worked with local packages to avoid getting skewed results in production code.
A little nitpicking: Composer is a PHP only tool, so it is not needed to add for PHP developers in the title. It is like saying PIP for Python developers or NPM for JavaScript developers.