A Better `composer-link` for Local Package Development
If you develop Laravel packages, you know the friction: you're working on a package locally and need to pull it into a project to test. Composer supports path repositories for exactly this, but the setup is manual and easy to fumble.
A few years ago, Caleb Porzio (creator of Livewire and Alpine.js) shared a handy shell function that simplifies this:
function composer-link() {
composer config repositories.local '{"type": "path", "url": "'$1'", "symlink": true}' --file composer.json
}One command, and your local package is registered as a path repository. Simple and effective.
But after using it for a while, I thought, this could be better!
The repository is always keyed as
local— meaning you can only link one package at a time before it gets overwritten.You still have to manually
composer requirethe package after linking it.
So I improved it.
The Improved composer-link
function composer-link() {
local pkg_path="$1"
if [[ -z "$pkg_path" ]]; then
echo "Usage: composer-link /path/to/package"
return 1
fi
if [[ ! -f "$pkg_path/composer.json" ]]; then
echo "Error: No composer.json found at $pkg_path"
return 1
fi
local pkg_name
pkg_name=$(php -r "echo json_decode(file_get_contents('$pkg_path/composer.json'))->name;")
if [[ -z "$pkg_name" ]]; then
echo "Error: Could not read package name from $pkg_path/composer.json"
return 1
fi
local repo_key="${pkg_name//\//-}"
echo "Linking $pkg_name from $pkg_path..."
composer config "repositories.$repo_key" '{"type": "path", "url": "'"$pkg_path"'", "symlink": true}' --file composer.json
composer require "$pkg_name" @dev
}What changed?
It reads the package name automatically. The function looks at the local package's composer.json and extracts the name field using PHP (which is already on your machine if you're using Composer).
Each package gets its own repository key. Instead of a generic local key, the repository is keyed by the package name. A package like acme/widgets gets registered as repositories.acme-widgets. This means you can link multiple packages simultaneously without conflicts.
It requires the package for you. After registering the repository, it runs composer require with @dev automatically — because that's what you were going to do next anyway.
Usage
composer-link ../my-package
# Linking acme/my-package from ../my-package...
# ./composer.json has been updated
# Running composer update acme/my-packageThat's it. One command, and your local package is symlinked and required.
Bonus: composer-unlink
What goes in must come out. Here's a companion function to cleanly remove a linked package:
function composer-unlink() {
local input="$1"
if [[ -z "$input" ]]; then
echo "Usage: composer-unlink /path/to/package OR composer-unlink vendor/package"
return 1
fi
local pkg_name
if [[ -f "$input/composer.json" ]]; then
pkg_name=$(php -r "echo json_decode(file_get_contents('$input/composer.json'))->name;")
elif [[ "$input" == *"/"* ]]; then
pkg_name="$input"
else
echo "Error: '$input' is not a valid path or package name (expected vendor/package)"
return 1
fi
local repo_key="${pkg_name//\//-}"
echo "Unlinking $pkg_name..."
composer remove "$pkg_name"
composer config --unset "repositories.$repo_key"
}This one is flexible — you can pass either the path to the package or just the package name directly:
bash
# Either of these work:
composer-unlink ../my-package
composer-unlink acme/my-packageIt removes the package from require and cleans up the repository entry from composer.json.
Installation
Drop both functions into your shell aliases file (~/.bashrc, ~/.zshrc, or wherever you keep your aliases).
Then reload your shell and you're good to go.
Why Not Just Use Composer's Built-in Commands?
You absolutely can. But if you're linking local packages regularly — especially when building and testing Laravel packages — shaving off the boilerplate adds up. These two functions turn a multi-step process into a single command with proper error handling and cleanup.
Credit to Caleb Porzio for the original composer-link idea. This is just a small iteration that makes it a little more robust for daily use.