Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Composer Json Manipulator Laravel Package

symplify/composer-json-manipulator

Symplify ComposerJsonManipulator provides a PHP API for reading, modifying, and writing composer.json files safely. Update dependencies, scripts, config, and metadata, then save normalized JSON—ideal for tooling, migrations, and automated package maintenance.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a low-level utility, not a Composer replacement. It does three things: parse a composer.json into a ComposerJson value object, let you mutate the underlying array safely, and write it back out pretty-printed. That's it — no dependency resolution, no schema validation, no composer update.

Install

composer require symplify/composer-json-manipulator

Pin it. This package is an internal Symplify monorepo tool that gets refactored alongside their other packages; minor bumps have renamed classes (FileJsonPrinter → ComposerJsonPrinter) and moved namespaces. Treat it like a dev dependency, not a runtime one.

Where to look first

  • src/ComposerJsonFactory.php — entry point. createFromFilePath(), createFromArray().
  • src/ValueObject/ComposerJson.php — the object you actually work with. Look at getJsonArray(), getRequire(), getRequireDev(), getAutoload(), addItem(), setValue(), removeItem().
  • src/ValueObject/ComposerJsonSection.php — string constants (REQUIRE, REQUIRE_DEV, AUTOLOAD, SCRIPTS, REPOSITORIES, CONFIG, EXTRA, …). Use these instead of literals.
  • src/ComposerJsonPrinter.php (older versions: FileJsonPrinter) — writes back to disk.

First use case: bulk-bump a dev dependency across a monorepo

use Symplify\ComposerJsonManipulator\ComposerJsonFactory;
use Symplify\ComposerJsonManipulator\ComposerJsonPrinter;
use Symplify\ComposerJsonManipulator\ValueObject\ComposerJsonSection;

$factory = new ComposerJsonFactory();
$printer = new ComposerJsonPrinter();

foreach (glob(__DIR__ . '/packages/*/composer.json') as $path) {
    $composerJson = $factory->createFromFilePath($path);

    $composerJson->addItem(ComposerJsonSection::REQUIRE_DEV, 'phpstan/phpstan', '^1.10');

    $printer->printFile($composerJson);
}

That loop — read, mutate, print, per file — is 90% of real-world usage. Everything below is variations on it.


Implementation Patterns

Read-modify-write with an explicit diff guard

Because the printer rewrites the whole file, always be able to answer "what changed?" Compare against the original string before writing:

$original = file_get_contents($path);
$composerJson = $factory->createFromFilePath($path);
$composerJson->addItem(ComposerJsonSection::REQUIRE, 'psr/log', '^3.0');

// write to a temp location first, or print to string if your version supports it,
// then diff $original vs $new before committing

In CI, run the same code and fail the build if the on-disk file differs from the freshly printed one. That gives you a "composer.json is in sync" check without a custom format.

Batch over a Finder, not glob

For anything bigger than a handful of packages, wrap the loop in Symfony\Component\Finder so you can skip vendor/, follow symlinks deliberately, and exclude fixtures:

$finder = (new Finder())->files()->in(__DIR__ . '/packages')->name('composer.json')
    ->notPath('vendor')->notPath('fixtures');

Excluding fixtures/ matters — test fixtures with intentionally broken composer.json files are a classic source of surprise exceptions.

Section constants + explicit require/require-dev promotion

The most common real workflow is moving a package from require-dev to require (or the reverse) when a dev dependency leaks into production code:

$requireDev = $composerJson->getRequireDev();
if (isset($requireDev['symfony/console'])) {
    $composerJson->removeItem(ComposerJsonSection::REQUIRE_DEV, 'symfony/console');
    $composerJson->addItem(ComposerJsonSection::REQUIRE, 'symfony/console', $requireDev['symfony/console']);
}

Write this once and reuse it — it's the reason a lot of people pick up this package in the first place, since doing it by hand invites typos.

Merging monorepo root into leaf packages

createFromArray() is the escape hatch when you want to compute a composer.json rather than mutate one — e.g. generating per-package manifests from a root template:

$root = $factory->createFromFilePath(__DIR__ . '/composer.json');
$leaf = $factory->createFromFilePath(__DIR__ . '/packages/foo/composer.json');

$merged = $factory->createFromArray(
    array_merge_recursive($root->getJsonArray(), $leaf->getJsonArray())
);

Be aware array_merge_recursive will turn duplicate scalar values into arrays — for constraint conflicts you want array_replace_recursive or a hand-written merge with explicit precedence. Don't assume the package has an opinionated merger; check your version's src/ before relying on one.

Autoload path generation

Reading autoload.psr-4 off the value object is a clean way to build path maps for custom tooling (docs generators, static analysers, migration scripts) without re-parsing JSON yourself:

foreach ($composerJson->getAutoload()['psr-4'] ?? [] as $namespace => $paths) {
    // normalize to array, resolve against dirname($composerJson->getFilePath())
}

Note that PSR-4 values are string|array in the wild — always normalize before iterating.

Wrap it in a command

Since the classes are plain (no container magic required), the natural shape is a Console/Artisan command with --dry-run and --package= flags. Instantiate ComposerJsonFactory and ComposerJsonPrinter directly in the command; don't bother with DI wiring unless you're already inside a Symfony app that autowires the namespace.

Post-process with a formatter

If your repo runs composer normalize or a JSON formatter in CI, run this tool before the formatter. Otherwise you'll fight the printer's fixed 4-space indentation on every run.


Gotchas and Tips

ComposerJson is mutable despite the name. addItem() / setValue() / removeItem() change the internal array in place. If you cache instances across a loop, you'll get cross-contamination. Re-read from disk per file.

The printer can silently drop empty sections. Many versions clean empty arrays/null values before serializing, so a file with "autoload": {} or "scripts": {} comes back without them. Harmless functionally, noisy in a diff, and confusing the first time it happens. Check whether your version's cleaner is enabled and be prepared to accept the diff once.

Formatting is not configurable. Expect 4-space indent, JSON_PRETTY_PRINT, and a trailing newline. Files authored in 2-space style will produce a whole-file diff on the first run. Do a formatting-only commit first, then make behavioral changes in a second commit — otherwise your PR is unreviewable.

No schema validation. You can write "require": "not-an-object" and the package will happily serialize it. Composer will explode later. If you're generating manifests programmatically, validate against composer.json schema yourself (or just shell out to composer validate in CI).

No JSON comments. Standard JSON parser — a // in a hand-edited composer.json throws a decode exception. Same for trailing commas.

Key order is preserved from input. There's no automatic canonical ordering unless your version exposes a sorter. If you need require sorted alphabetically, sort the array before setValue(); don't assume it happens.

Missing file behavior is an exception, not null. createFromFilePath() throws on a nonexistent path. Guard with is_file() when walking directories, or use createFromArray([]) when bootstrapping a brand-new package.

Section constant typos fail silently. addItem('require_dev', ...) creates a junk top-level key rather than erroring. Always use ComposerJsonSection::* and grep your codebase for string literals in section position.

Version churn is the real risk. Class names and method signatures have moved between releases, and the package tracks Symplify's monorepo rather than semver-strict consumer expectations. Mitigations: pin an exact version in require-dev, wrap the factory/printer behind a tiny internal adapter class in your own codebase, and keep the call sites to createFromFilePath / getJsonArray / printFile — those three have been stable the longest.

It is not composer require. It won't resolve versions, check constraints, or touch composer.lock. If you write a constraint that conflicts with the lock, you've created a broken repo with no warning. Follow every run with composer update --lock (or composer validate at minimum) in the same script.

Extension points are thin by design. There's no event system or plugin interface. Customize by composing: subclass the printer if you need 2-space output or unescaped slashes, and write your own merge function instead of fighting a generic one. That's cheaper than reading the source every upgrade.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core