Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ packages.json
results.sarif
infection.log
.churn.cache
tools/chorale/composer.lock
tools/chorale/.phpunit.cache/
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

This repository is a PHP monorepo containing many packages under `src/`. This guide provides consistent instructions for AI coding agents to work safely and effectively across the codebase.

- Use clear variable names and keep code well documented.
- Run tests relevant to the areas you change.
- For changes under `tools/chorale`, run `composer install` and `./vendor/bin/phpunit` in that directory before committing.
- Chorale is the monorepo management CLI using a plan/apply workflow; see `tools/chorale/AGENTS.md` for its roadmap and guidelines.

## Repo Layout

- Root: build tooling (`Makefile`, composer), shared configs, CI inputs.
Expand Down Expand Up @@ -56,5 +61,4 @@ This repository is a PHP monorepo containing many packages under `src/`. This gu
- Build passes: `make test` (optionally with coverage).
- Code quality passes: `make php-cs-fixer`, `make psalm`, and (if applicable) `make upgrade-code`.
- Docs updated where needed.
- No changes to `vendor/` or generated artifacts.

- No changes to `vendor/` or generated artifacts.
19 changes: 19 additions & 0 deletions chorale.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: 1
repo_host: [email protected]
repo_vendor: SonsOfPHP
repo_name_template: '{name:kebab}.git'
default_repo_template: '{repo_host}:{repo_vendor}/{repo_name_template}'
default_branch: main
splitter: splitsh
tag_strategy: inherit-monorepo-tag
rules:
keep_history: true
skip_if_unchanged: true
require_files:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to have the ability to replace variables in the files.

- composer.json
- LICENSE
patterns:
-
match: 'src/**'
include:
- '**'
4 changes: 4 additions & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
* [Overview](bard/overview.md)
* [Commands](bard/commands.md)

## 🔧 Tools

* [Chorale](tools/chorale.md)

## Symfony Bundles

* [Feature Toggle](symfony-bundles/feature-toggle.md)
Expand Down
37 changes: 37 additions & 0 deletions docs/tools/chorale.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Chorale

Chorale is a CLI tool for managing PHP monorepos. It uses a plan/apply workflow to keep package metadata and the root package in sync.

## Installation

```bash
cd tools/chorale
composer install
```

## Usage

Run the commands from the project root:

```bash
# create chorale.yaml by scanning packages
php bin/chorale setup

# preview changes without modifying files
php bin/chorale plan --json > plan.json

# apply an exported plan
php bin/chorale apply --file plan.json

# build and apply a plan in one go
php bin/chorale run
```

Chorale automatically merges all package `composer.json` files into the root `composer.json` so the monorepo can be installed as a single package. Any dependency conflicts are recorded under the `extra.chorale.dependency-conflicts` section for review.

## Commands

- `setup` – generate configuration and validate required files.
- `plan` – build a plan for splitting packages and root updates.
- `run` – build and immediately apply a plan.
- `apply` – execute steps from a JSON plan file.
2 changes: 1 addition & 1 deletion rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
earlyReturn: true,
strictBooleans: true,
phpunitCodeQuality: true,
phpunit: true,
//phpunit: true,
)
->withImportNames(
importShortClasses: false,
Expand Down
14 changes: 14 additions & 0 deletions tools/chorale/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# AGENTS

Chorale is a CLI tool maintained in this repository.

- Use descriptive variable names and document public methods.
- Add unit tests for new features in `src/Tests`.
- Run `composer install` and `./vendor/bin/phpunit` in this directory before committing changes.

## Roadmap

- Implement executors for remaining plan steps such as composer root rebuild and metadata sync.
- Improve conflict resolution strategies for dependency merges.
- Enhance documentation with more real-world examples as features grow.

139 changes: 139 additions & 0 deletions tools/chorale/bin/chorale
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/usr/bin/env php
<?php declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use Symfony\Component\Console\Application;
use Chorale\Console\Style\ConsoleStyleFactory;
use Chorale\Console\SetupCommand;
use Chorale\Repo\TemplateRenderer;
use Chorale\Util\PathUtils;
use Chorale\Util\Sorting;
use Chorale\Discovery\PackageIdentity;
use Chorale\Config\ConfigDefaults;
use Chorale\Config\SchemaValidator;
use Chorale\IO\BackupManager;
use Chorale\IO\JsonReporter;
use Chorale\Telemetry\RunSummary;
use Chorale\Config\ConfigLoader;
use Chorale\Config\ConfigWriter;
use Chorale\Config\ConfigNormalizer;
use Chorale\Discovery\ComposerMetadata;
use Chorale\Discovery\PackageScanner;
use Chorale\Discovery\PatternMatcher;
use Chorale\Repo\RepoResolver;
use Chorale\Rules\RequiredFilesChecker;
use Chorale\Rules\ConflictDetector;
use Chorale\Composer\ComposerJsonReader;
use Chorale\Composer\DependencyMerger;
use Chorale\Composer\RuleEngine;
use Chorale\Split\ContentHasher;
use Chorale\Split\SplitDecider;
use Chorale\State\FilesystemStateStore;
use Chorale\Util\DiffUtil;
use Chorale\Plan\PlanBuilder;
use Chorale\Console\PlanCommand;
use Chorale\Console\ApplyCommand;
use Chorale\Console\RunCommand;
use Chorale\Run\Runner;
use Chorale\Run\StepExecutorRegistry;
use Chorale\Run\PackageVersionUpdateExecutor;
use Chorale\Run\RootDependencyMergeExecutor;
use Chorale\Run\ComposerRootUpdateExecutor;

$paths = new PathUtils();
$renderer = new TemplateRenderer();
$sorting = new Sorting();
$identity = new PackageIdentity();
$defaults = new ConfigDefaults();
$schema = new SchemaValidator();
$backup = new BackupManager();
$json = new JsonReporter();
$summary = new RunSummary();
$loader = new ConfigLoader();
$composerMeta = new ComposerMetadata();
$composerReader = new ComposerJsonReader();
$stateStore = new FilesystemStateStore();
$hasher = new ContentHasher();
$diffs = new DiffUtil();

$ruleEngine = new RuleEngine($renderer);
$writer = new ConfigWriter($backup);
$normalizer = new ConfigNormalizer($sorting, $defaults);
$scanner = new PackageScanner($paths);
$matcher = new PatternMatcher($paths);
$resolver = new RepoResolver($renderer, $paths);
$required = new RequiredFilesChecker();
$conflicts = new ConflictDetector($matcher);
$depMerger = new DependencyMerger($composerReader);
$splitDecider = new SplitDecider($stateStore, $hasher);

$planner = new PlanBuilder(
defaults: $defaults,
scanner: $scanner,
matcher: $matcher,
resolver: $resolver,
paths: $paths,
composerReader: $composerReader,
depMerger: $depMerger,
ruleEngine: $ruleEngine,
splitDecider: $splitDecider,
diffs: $diffs,
);
$executors = new StepExecutorRegistry([
new PackageVersionUpdateExecutor(),
new RootDependencyMergeExecutor(),
new ComposerRootUpdateExecutor(),
]);
$runner = new Runner(
configLoader: $loader,
planner: $planner,
executors: $executors,
);


// -----------------------------------------------------------------------------
$app = new Application('Chorale', '0.1.0');
// -----------------------------------------------------------------------------

// -----------------------------------------------------------------------------
$app->add(new SetupCommand(
styleFactory: new ConsoleStyleFactory(),
configLoader: $loader,
configWriter: $writer,
configNormalizer: $normalizer,
schemaValidator: $schema,
defaults: $defaults,
scanner: $scanner,
matcher: $matcher,
resolver: $resolver,
identity: $identity,
requiredFiles: $required,
//conflicts: $conflicts,
jsonReporter: $json,
summary: $summary,
composerMeta: $composerMeta,
));
// -----------------------------------------------------------------------------

// -----------------------------------------------------------------------------
$app->add(new PlanCommand(
styleFactory: new ConsoleStyleFactory(),
configLoader: $loader,
planner: $planner,
));
// -----------------------------------------------------------------------------
$app->add(new ApplyCommand(
styleFactory: new ConsoleStyleFactory(),
runner: $runner,
));
// -----------------------------------------------------------------------------
$app->add(new RunCommand(
styleFactory: new ConsoleStyleFactory(),
runner: $runner,
));
// -----------------------------------------------------------------------------

// -----------------------------------------------------------------------------
$app->run();
// -----------------------------------------------------------------------------
35 changes: 35 additions & 0 deletions tools/chorale/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "sonsofphp/chorale",
"description": "Chorale: a CLI tool to help manage PHP monorepos.",
"type": "project",
"license": "MIT",
"require": {
"php": "^8.3",
"ext-mbstring": "*",
"symfony/console": "^7.0",
"symfony/yaml": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"symfony/var-dumper": "^7.3"
},
"autoload": {
"psr-4": {
"Chorale\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Chorale\\Tests\\": "src/Tests/"
}
},
"bin": [
"bin/chorale"
],
"config": {
"sort-packages": true,
"preferred-install": "dist"
},
"minimum-stability": "stable",
"prefer-stable": true
}
47 changes: 47 additions & 0 deletions tools/chorale/phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
requireCoverageMetadata="true"
backupGlobals="false"
colors="true"
cacheResult="true"
executionOrder="defects"
beStrictAboutCoverageMetadata="true"
stopOnDefect="true"
stopOnError="true"
stopOnFailure="true"
stopOnWarning="true"
stopOnDeprecation="true"
stopOnNotice="true"
displayDetailsOnIncompleteTests="true"
displayDetailsOnSkippedTests="true"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnPhpunitDeprecations="true"
displayDetailsOnTestsThatTriggerErrors="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerWarnings="true"
>

<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Chorale Test Suite">
<directory>src/Tests</directory>
</testsuite>
</testsuites>

<coverage includeUncoveredFiles="true" pathCoverage="false" ignoreDeprecatedCodeUnits="true" disableCodeCoverageIgnore="false" />

<source>
<include>
<directory suffix=".php">src</directory>
</include>
<exclude>
<directory>src/Tests</directory>
</exclude>
</source>
</phpunit>

24 changes: 24 additions & 0 deletions tools/chorale/src/Composer/ComposerJsonReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Chorale\Composer;

final class ComposerJsonReader implements ComposerJsonReaderInterface
{
public function read(string $absolutePath): array
{
if (!is_file($absolutePath)) {
return [];
}

$raw = @file_get_contents($absolutePath);
if ($raw === false) {
return [];
}

$json = json_decode($raw, true);

return is_array($json) ? $json : [];
}
}
14 changes: 14 additions & 0 deletions tools/chorale/src/Composer/ComposerJsonReaderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Chorale\Composer;

interface ComposerJsonReaderInterface
{
/**
* @return array<string, mixed>
* if missing/invalid, it will return an empty array
*/
public function read(string $absolutePath): array;
}
Loading