Installation:
composer require --dev sweetchuck/robo-composer
Load the Task Trait:
In your RoboFile.php, add the trait to your class:
use Sweetchuck\Robo\Composer\ComposerTaskLoader;
First Use Case:
Run the composer:package-paths command to list all installed Composer packages and their paths:
vendor/bin/robo composer:package-paths
taskComposerLockDiffer(): Compare two composer.lock files (useful for CI/CD pipelines or pre-deploy checks).taskComposerPackagePaths(): Retrieve paths of installed packages (useful for debugging or custom scripts).Use taskComposerLockDiffer() to detect changes between composer.lock in branches or environments:
/**
* @command composer:ci-lock-check
* @validateArgumentFileName oldLock,newLock
*/
public function ciLockCheck(string $oldLock, string $newLock) {
return $this
->collectionBuilder()
->addTask($this->taskComposerLockDiffer()
->setLockA(json_decode(file_get_contents($oldLock), true))
->setLockB(json_decode(file_get_contents($newLock), true))
)
->addCode(function (\Robo\State\Data $data): int {
$diff = $data['composer.lockDiff'];
if (!empty($diff)) {
$this->output()->error('Lock file changes detected!');
$this->output()->writeln(Yaml::dump($diff));
return 1;
}
$this->output()->success('Lock files match.');
return 0;
});
}
Usage:
vendor/bin/robo composer:ci-lock-check <(git show 'origin/main:composer.lock') ./composer.lock
Use taskComposerPackagePaths() to fetch paths for custom logic (e.g., symlinking or backup scripts):
/**
* @command composer:backup-package
* @arg packageName
*/
public function backupPackage(string $packageName) {
return $this
->collectionBuilder()
->addTask($this->taskComposerPackagePaths())
->addCode(function (\Robo\State\Data $data) use ($packageName): int {
if (!isset($data['composer.packagePaths'][$packageName])) {
$this->output()->error("Package '$packageName' not found.");
return 1;
}
$path = $data['composer.packagePaths'][$packageName];
$this->taskExecStack()->run("cp -r $path /backups/{$packageName}-$(date +%s)");
return 0;
});
}
Chain tasks for multi-step operations (e.g., validate + backup):
public function validateAndBackup() {
return $this->collectionBuilder()
->addTask($this->taskComposerLockDiffer()
->setLockA($this->getOldLock())
->setLockB($this->getNewLock())
)
->addTask($this->backupPackage('symfony/console'))
->run();
}
Leverage Robo Hooks:
Use @hook annotations to validate inputs before task execution (e.g., file existence checks).
/**
* @hook validateFileExists
*/
public function validateFileExists(\Consolidation\AnnotatedCommand\CommandData $commandData) {
$files = $commandData->annotationData()->getList(__FUNCTION__);
foreach ($files as $file) {
assert(file_exists($file), "File '$file' not found.");
}
}
Customize Output:
Format composer.lockDiff data for readability (e.g., using Symfony\Component\Yaml\Yaml or League\CLImate).
Environment Awareness:
Use getcwd() or config files to resolve paths dynamically:
$lockPath = $this->getConfig('composer.lock_path') ?? 'composer.lock';
JSON Parsing Errors:
composer.lock files must be valid JSON. Use json_decode() with JSON_THROW_ON_ERROR for strict validation:
$lock = json_decode(file_get_contents($lockFile), true, 512, JSON_THROW_ON_ERROR);
Path Resolution Issues:
realpath() or getcwd() to handle relative paths:
$absolutePath = realpath($relativePath) ?: throw new \RuntimeException("Path not found.");
Task State Dependencies:
->run() explicitly for clarity:
return $this->collectionBuilder()
->addTask($this->taskComposerPackagePaths())
->addCode(fn($data) => $this->processData($data))
->run(); // Explicit execution
Inspect Task Data:
Dump the $data object in addCode to verify outputs:
->addCode(function (\Robo\State\Data $data): int {
$this->output()->writeln(print_r($data->all(), true));
return 0;
})
Validate Lock File Structure:
Use composer validate composer.lock as a sanity check before scripting.
Handle Large Lock Files:
For CI/CD, stream git show output to avoid memory issues:
vendor/bin/robo composer:lock-diff <(git show 'HEAD^:composer.lock' | jq '.') ./composer.lock
Custom Diff Logic:
Extend the ComposerLockDiffer class to add custom comparison rules (e.g., ignore dev dependencies):
class CustomLockDiffer extends \Sweetchuck\Robo\Composer\ComposerLockDiffer {
protected function shouldComparePackage(array $package) {
return !isset($package['require-dev']);
}
}
Then use it via dependency injection or a wrapper task.
Add New Tasks:
The package is minimal; fork and extend it to add tasks like taskComposerUpdate or taskComposerAudit.
Configuration:
Load composer.json dynamically for runtime flexibility:
$composerJson = json_decode(file_get_contents('composer.json'), true);
$this->taskComposerLockDiffer()->setConfig($composerJson);
setLockA()). For reusable setups, wrap tasks in a service class.setLockA()/setLockB() receive properly decoded JSON arrays, not raw strings.composer.lock files in memory if used frequently:
private $lockCache = [];
protected function getParsedLock(string $path) {
return $this->lockCache[$path] ??= json_decode(file_get_contents($path), true);
}
How can I help you explore Laravel packages today?