joshdifabio/composed
Composed is a Composer plugin for projects split across multiple composer.json files. It merges their dependency requirements into a single graph, so you can install and autoload packages from separate directories without juggling each manifest by hand.
joshdifabio/composed gives you a read-only runtime view of a project's composer.json and the composer.json of everything installed via composer.lock. It is useful when your application needs to introspect itself or its dependencies at runtime without shelling out to composer show.
Install
composer require joshdifabio/composed
The last tagged release is from February 2016, so treat this as a legacy dependency. Check the package's composer.json constraints and run your test suite on the PHP version you deploy before adopting it. If it conflicts with your Composer version or PHP 8, isolate it behind your own small adapter interface.
Where to look first
README.md for the current entry-point class and method names — they are the most likely thing to have drifted from older blog examples.src/ for the package/loader class. The central concept is a Package object representing a directory containing a composer.json.tests/ for working examples of parsing both root and dependency packages; the fixtures show exactly how lock data is interpreted.First use case: identify the host application
use Composed\Package;
$root = Package::fromDirectory(__DIR__ . '/..');
$name = $root->getConfig('name'); // e.g. "acme/shop"
$version = $root->getConfig('version');
$extra = $root->getConfig('extra', []); // custom runtime configuration
A realistic first win is reading a value from the root package's extra block instead of duplicating it in .env or a separate config file.
Use the root package as the single source of truth for values that already belong in composer.json:
$root = Package::fromDirectory(base_path());
return [
'name' => $root->getConfig('name'),
'version' => $root->getConfig('version', 'dev'),
'extra' => $root->getConfig('extra', []),
];
Good candidates: feature flags in extra, support/contact metadata, build identifiers, and package-name-based cache key prefixes.
Iterate the resolved dependency set (from composer.lock) to find packages that advertise capabilities in extra:
$root = Package::fromDirectory(base_path());
foreach ($root->getInstalledPackages() as $package) {
$config = $package->getConfig();
if (isset($config['extra']['my-app']['plugin'])) {
$plugins[] = [
'name' => $config['name'],
'class' => $config['extra']['my-app']['plugin'],
'version' => $package->getVersion(),
];
}
}
This is the classic plugin-registry pattern: packages opt in via extra, and your application discovers them at boot. It avoids hard-coding a list of first-party packages and works with third-party packages installed later.
Because the package exposes dependency configs, you can inspect require / require-dev across installed packages to explain transitive dependencies:
$target = 'psr/log';
$dependents = array_filter(
$root->getInstalledPackages(),
function ($package) use ($target) {
$requires = $package->getConfig('require', []);
return array_key_exists($target, $requires);
}
);
Useful in an internal diagnostic command or a CI audit step, not in a hot request path.
Wrap the library in a console command rather than calling it from controllers:
class ComposerDiagnoseCommand extends Command
{
protected function configure()
{
$this->setName('app:composer:diagnose');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$root = Package::fromDirectory(base_path());
$output->writeln(sprintf(
'<info>%s</info> @ %s',
$root->getConfig('name'),
$root->getConfig('version', 'unknown')
));
foreach ($root->getInstalledPackages() as $package) {
$output->writeln(sprintf(
' - %s %s',
$package->getConfig('name'),
$package->getVersion()
));
}
}
}
Because the library reads from disk and is unmaintained, keep it behind a narrow interface:
interface ProjectMetadata
{
public function rootConfig(string $key, $default = null);
public function installedPackages(): array;
}
Implement it once with Composed, and cache the parsed result in your DI container or a PSR-16 cache with a key derived from the composer.lock hash. The composer configuration does not change during a request, so parse it once.
$lockHash = md5_file(base_path('composer.lock'));
$metadata = $cache->remember("composer-meta:$lockHash", 3600, function () {
return (new ComposedProjectMetadata(base_path()))->all();
});
Use dependency presence as a feature switch instead of hard-coding environment checks:
$installed = collect($metadata->installedPackages())->pluck('name');
if ($installed->contains('laravel/horizon')) {
$this->app->register(HorizonServiceProvider::class);
}
Keep this to optional integrations; core application wiring should still be explicit.
In a monorepo, each package directory has its own composer.json. Instantiate one Package per directory rather than assuming a single root:
foreach (glob(__DIR__ . '/packages/*/composer.json') as $file) {
$package = Package::fromDirectory(dirname($file));
// ...
}
Do not walk up from getcwd() implicitly — always pass an explicit absolute directory.
composer.json/composer.lock decoding later. The fallback is ~30 lines of json_decode if needed.composer.lock must exist. Installed-package introspection depends on lock data. In libraries consumed by other projects, the "root" is the host application, not your package. Guard against a missing lock file and degrade gracefully.packages / packages-dev structure, but content-hash and metadata differ. If you compare or cache on lock contents, hash the raw file rather than trusting parsed fields.require-dev packages appearing in the installed set, test against composer install --no-dev — those entries will not be present at runtime.replace, provide, and platform packages are not dependency resolution. The library parses configuration; it does not resolve versions or explain conflicts. For "who requires X", inspect declared require entries and accept that aliases/replacements can make the answer approximate.Package::fromDirectory(...) with a path derived from __DIR__, base_path(), or a configured project root avoids surprises when the working directory changes (queue workers, PHP-FPM, cron).fromDirectory, getConfig, installed-package accessors) is small but old; IDE completion and composer show -a joshdifabio/composed are more reliable than copied snippets. Wrap access in your adapter on day one.composer.json can surface as an exception. Catch parse failures per package when scanning third-party packages so one malformed dependency does not break your whole registry.config/repositories. Return only the fields your application intentionally consumes.version field (it comes from VCS tags at install time); fall back to dev or your own build identifier.How can I help you explore Laravel packages today?