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
118 changes: 117 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,122 @@ Initial packages:
- `stellarwp/foundation-log`
- `stellarwp/foundation-pipeline`

## Namespaces

Use package namespaces under `StellarWP\Foundation\<Package>\`.

TODO: finish this.
## Code Organization

Prefer feature-first organization, also known as vertical slice architecture or package-by-feature, when adding command/tooling features. Group a command and its private collaborators under the command feature namespace.

For example, use:

```text
Commands/
Package/
Contracts/
PackageRepositoryCreator.php
CreateCommand.php
PackageResolver.php
PackageFilesValidator.php
GitHubPackageRepositoryCreator.php
```

instead of splitting those private collaborators into broad technical folders too early.

If a collaborator is only useful for one command group, keep it under that command group's feature folder. If it becomes useful across multiple command groups, promote it to a broader domain or infrastructure namespace such as `Package/`, `GitHub/`, `Console/`, or `Process/`.

When it is very clear that a class will be reused by many similar features, promote it immediately instead of burying it in the first feature slice. This is especially true for command/tooling infrastructure where many commands will need the same capability, such as shell command formatting, process execution, console IO helpers, package discovery, or GitHub API clients.

Feature-local interfaces should live in a `Contracts/` folder inside the feature slice, for example `Commands/Package/Contracts/PackageRepositoryCreator.php`. Only promote contracts to a package-level `Contracts/` namespace when they are intended to be shared across multiple features or consumed as public extension points.

Shared infrastructure interfaces should live under that shared namespace's `Contracts/` folder, for example `Process/Contracts/ProcessRunner.php`.

## Container Providers

When writing providers or container registration code, prefer container-driven construction over inline factories with explicit `new` calls. Bind classes and interfaces directly when the container can autowire them.

Use contextual bindings with `$this->container->when()->needs()->give()` for scalar constructor arguments, command lists, or feature-specific substitutions. Use a factory closure only when the value must be computed or resolved from the container, and keep that closure focused on supplying the constructor dependency rather than constructing the full object.

## Split Packages

Split packages live in `src/<Package>/` and are split to read-only repositories named `stellarwp/foundation-<package>`.

When adding a new split package, set its package `composer.json` PHP constraint to `>=8.3` unless the user explicitly says otherwise. PHP 7.4 release compatibility will be handled later by an automated Rector downgrade workflow, not by lowering the package PHP constraint during development.

When adding external dependencies for split packages, choose version constraints whose package line supports PHP 7.4. Use `>=` constraints for those dependencies instead of caret constraints when preserving the PHP 7.4-compatible floor matters. For example, use a Symfony component version such as `>=5.4` rather than a newer line that requires PHP 8+.

Important exception: dependencies on this monorepo's own split packages, such as `stellarwp/foundation-container`, should use the correct Composer release constraint like `^1.0`. Do not use `>=` for internal Foundation package dependencies; Monorepo Builder commands such as `composer monorepo bump-interdependency` are expected to bump those constraints during releases.

### Required Files

Each split package should include:

- `composer.json`
- `README.md`
- `.gitattributes`
- `.gitignore`
- `.github/workflows/close-pull-request.yml`

Each split package `README.md` must include this warning immediately after the package heading:

```markdown
> [!WARNING]
> **This is a read-only repository!** For pull requests or issues, see [stellarwp/foundation](https://github.com/stellarwp/foundation).
```

### GitHub Repositories

When creating a new split repository on GitHub, use the description `[READ ONLY] Subtree split of the Foundation <Component> component (see stellarwp/foundation)` and disable wikis, issues, projects, and pull requests.

## PHP Feature Policy

Allowed for current PHP 8.3 source:

- constructor property promotion
- union types
- intersection types
- readonly properties/classes
- enums
- nullsafe operator
- match expressions
- named arguments
- first-class callables
- typed class constants

Avoid unless there is a clear reason:

- enums in public APIs
- reflection-heavy code
- attributes that affect runtime behavior
- DNF types
- `never` in public APIs

Banned while the project targets PHP 8.3:

- PHP 8.4 property hooks
- PHP 8.4 asymmetric visibility
- PHP 8.4 lazy objects API
- `#[Deprecated]`; use `@deprecated` PHPDoc instead
- PHP 8.4-only functions/classes/constants

## Monorepo Commands

After adding or changing split package dependencies, run `composer monorepo merge` and then `composer update` so root `composer.json`/lock state includes package dependency changes.

Use `composer monorepo list` to inspect available Monorepo Builder commands.

## Verification

When `composer lint` reports style-only issues, run `composer format` to let the project formatter fix them before making manual formatting edits.

Reusable test fixtures, sample classes, and test doubles should live under `tests/Support/Fixtures/<Namespace>/` instead of being declared inline in a test class file. Keep truly local one-off fakes inline only when they are not reusable and do not represent a domain/package fixture.

After completing a feature, run `composer test:coverage`, review `clover.xml` for missed source coverage, and add meaningful tests for uncovered behavior before considering the feature complete.

## Releases

- Adding a new split package is usually a minor SemVer release because it introduces new functionality without breaking existing packages. Use a major release only if the change also breaks an existing public API or package contract.
- Run `composer monorepo bump-interdependency <version>` when planning a major version release so Foundation packages that depend on each other require the new major line. It may also be useful for a minor release when one package must require APIs added in that new minor.
- Run `composer monorepo package-alias` when `dev-main` should move to a new development line, usually after a minor or major release. Do not run it for every patch release when the current branch alias is still correct.
- The monorepo split workflow deploys package code to each sub-repository after a GitHub release is drafted.
1 change: 1 addition & 0 deletions CLAUDE.md
29 changes: 25 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,38 @@ After any needed command, commit the updated `composer.json` files. Then draft a

The [monorepo split GitHub workflow](./.github/workflows/monorepo-split.yml) will deploy each project's code to their sub-repository.

Adding a new split package is usually a minor [semver](https://semver.org/) release because it adds new functionality without breaking existing packages. Use a major release only if the change also breaks an existing public API or package contract.

### Monorepo

This uses [Symplify's Monorepo Builder](https://github.com/symplify/monorepo-builder). There is a shortcut composer script you can
run to access their CLI: `composer monorepo list` to see the available commands.

#### Adding a New Package

1. Copy the composer.json from one of the packages and modify the `name` and `psr-4` autoload namespace.
2. Ensure you have the `close-pull-request.yml` GitHub workflow, a `.gitattributes`, `.gitignore` and `README.md` files.
3. Once you've added the specific dependencies your package needs to its composer.json, run `composer monorepo merge` and then `composer update` and commit the changes. This will merge the dependencies into the root composer.json.
4. Create your [new repository](https://github.com/organizations/stellarwp/repositories/new), add the description: `[READ ONLY] Subtree split of the Foundation <NEW_COMPONENT_NAME> component (see stellarwp/foundation)` and disable wikis, issues, projects and pull requests.
1. Run the package creation command:

```bash
composer run foundation -- package:create <Package>
```

The package argument can be a new package component such as `WPCli`, an existing package directory, short package name, or Composer package name, for example `Log`, `foundation-log`, or `stellarwp/foundation-log`.

If the package does not exist yet, the command asks whether to create the local scaffold in `src/<Package>` and asks for the Composer package name with a default such as `stellarwp/foundation-wpcli`. The scaffold includes the required `composer.json`, `README.md`, `.gitattributes`, `.gitignore`, and `close-pull-request.yml` files. After scaffolding, the command runs `composer monorepo merge` so the root `composer.json` includes the new package.

The command runs as a dry run by default. It validates the required split package files, prints the target repository name and description, and shows the GitHub CLI commands it will run.

2. Add source code, tests, and any package-specific dependencies to the new package.

3. Once you've added the specific dependencies your package needs to its composer.json, run `composer monorepo merge` again and then `composer update` and commit the changes. This will merge the dependency changes into the root composer.json.

4. Create and configure the read-only split repository:

```bash
composer run foundation -- package:create <Package> --apply
```

The command creates the `stellarwp/foundation-<package>` repository with the standard `[READ ONLY]` description, disables issues, wiki, and projects, and relies on the package's `close-pull-request.yml` workflow to close pull requests.

## License

Expand Down
11 changes: 9 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"monolog/monolog": "^2.11",
"psr/log": ">=1.0",
"stellarwp/container-contract": "^1.1",
"symfony/console": ">=5.4",
"vlucas/phpdotenv": ">=4.3"
},
"require-dev": {
Expand All @@ -26,20 +27,25 @@
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.2",
"phpunit/phpunit": "^12.5",
"wp-cli/wp-cli": ">=2.12",
"zenphp/pinte": "^1.2"
},
"replace": {
"stellarwp/foundation-cli": "self.version",
"stellarwp/foundation-container": "self.version",
"stellarwp/foundation-log": "self.version",
"stellarwp/foundation-pipeline": "self.version"
"stellarwp/foundation-pipeline": "self.version",
"stellarwp/foundation-wpcli": "self.version"
},
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
"StellarWP\\Foundation\\Cli\\": "src/Cli/",
"StellarWP\\Foundation\\Container\\": "src/Container/",
"StellarWP\\Foundation\\Log\\": "src/Log/",
"StellarWP\\Foundation\\Pipeline\\": "src/Pipeline/"
"StellarWP\\Foundation\\Pipeline\\": "src/Pipeline/",
"StellarWP\\Foundation\\WPCli\\": "src/WPCli/"
}
},
"autoload-dev": {
Expand All @@ -56,6 +62,7 @@
}
},
"scripts": {
"foundation": "@php src/Cli/bin/foundation",
"monorepo": "@php vendor/bin/monorepo-builder",
"test": "@php vendor/bin/phpunit -c phpunit.xml.dist --colors=always -d memory_limit=2G",
"test:unit": "@test --testsuite Unit",
Expand Down
6 changes: 6 additions & 0 deletions pinte.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
{
"preset": "laravel",
"indent": "\t",
"exclude": [
".agents",
".codex",
".claude"
],
"rules": {
"blank_line_after_opening_tag": false,
"linebreak_after_opening_tag": false,
Expand All @@ -13,6 +18,7 @@
"combine_consecutive_unsets": true,
"no_blank_lines_after_class_opening": false,
"use_arrow_functions": true,
"static_lambda": true,
"blank_line_between_import_groups": true,
"no_superfluous_phpdoc_tags": {
"allow_mixed": true
Expand Down
7 changes: 7 additions & 0 deletions src/Cli/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Path-based git attributes
# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html

# Ignore paths when git creates an archive of this package
.gitattributes export-ignore
.gitignore export-ignore
.github export-ignore
13 changes: 13 additions & 0 deletions src/Cli/.github/workflows/close-pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Close Pull Request

on:
pull_request_target:
types: [opened]

jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: "This is a read-only repository. Please submit your PR on the https://github.com/stellarwp/foundation repository.<br><br>Thanks!"
2 changes: 2 additions & 0 deletions src/Cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
vendor/
composer.lock
38 changes: 38 additions & 0 deletions src/Cli/Application.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php declare(strict_types=1);

namespace StellarWP\Foundation\Cli;

use StellarWP\Foundation\Cli\Contracts\CommandProvider;
use Symfony\Component\Console\Application as SymfonyApplication;
use Symfony\Component\Console\Command\Command;

/**
* Symfony Console application for Foundation tooling.
*
* Use this as the CLI entry point when commands should be assembled by the
* Foundation container, including commands contributed by command providers.
*/
final class Application extends SymfonyApplication
{
/**
* @param iterable<Command> $commands
* @param iterable<CommandProvider> $commandProviders
*/
public function __construct(iterable $commands = [], iterable $commandProviders = []) {
parent::__construct('Foundation');

foreach ($commands as $command) {
$this->addCommands([$command]);
}

foreach ($commandProviders as $commandProvider) {
$this->addCommandProvider($commandProvider);
}
}

public function addCommandProvider(CommandProvider $commandProvider): void {
foreach ($commandProvider->commands() as $command) {
$this->addCommands([$command]);
}
}
}
54 changes: 54 additions & 0 deletions src/Cli/CliProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php declare(strict_types=1);

namespace StellarWP\Foundation\Cli;

use lucatume\DI52\Container;
use StellarWP\Foundation\Cli\Commands\Package\Contracts\PackageRepositoryCreator;
use StellarWP\Foundation\Cli\Commands\Package\CreateCommand;
use StellarWP\Foundation\Cli\Commands\Package\GitHubPackageRepositoryCreator;
use StellarWP\Foundation\Cli\Commands\Package\PackageFilesValidator;
use StellarWP\Foundation\Cli\Commands\Package\PackageRepositoryPlanFactory;
use StellarWP\Foundation\Cli\Commands\Package\PackageResolver;
use StellarWP\Foundation\Cli\Commands\Package\PackageScaffolder;
use StellarWP\Foundation\Cli\Process\Contracts\ProcessRunner;
use StellarWP\Foundation\Cli\Process\ShellProcessRunner;
use StellarWP\Foundation\Container\Contracts\Provider;

/**
* Registers the default Foundation CLI application and command dependencies.
*
* Include this provider when booting the `foundation` executable so command
* slices can be autowired through the Foundation container.
*/
final class CliProvider extends Provider
{
public const string ROOT_PATH = 'foundation.cli.root_path';

public function register(): void {
$this->container->singleton(self::ROOT_PATH, getcwd() ?: dirname(__DIR__, 2));

$this->container->when(PackageResolver::class)
->needs('$rootPath')
->give(static fn (Container $c): string => $c->get(self::ROOT_PATH));

$this->container->when(PackageScaffolder::class)
->needs('$rootPath')
->give(static fn (Container $c): string => $c->get(self::ROOT_PATH));

$this->container->when(Application::class)
->needs('$commands')
->give(static fn (Container $c): array => [
$c->get(CreateCommand::class),
]);

$this->container->singleton(PackageResolver::class);
$this->container->singleton(PackageScaffolder::class);
$this->container->singleton(PackageFilesValidator::class);
$this->container->singleton(PackageRepositoryPlanFactory::class);
$this->container->singleton(ShellProcessRunner::class);
$this->container->bind(ProcessRunner::class, ShellProcessRunner::class);
$this->container->bind(PackageRepositoryCreator::class, GitHubPackageRepositoryCreator::class);
$this->container->singleton(CreateCommand::class);
$this->container->singleton(Application::class);
}
}
21 changes: 21 additions & 0 deletions src/Cli/Commands/Package/Contracts/PackageRepositoryCreator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);

namespace StellarWP\Foundation\Cli\Commands\Package\Contracts;

use StellarWP\Foundation\Cli\Commands\Package\PackageRepositoryPlan;

/**
* Creates or configures the external repository for a Foundation split package.
*
* Depend on this contract from package commands so repository providers can be
* replaced without changing package discovery or validation logic.
*/
interface PackageRepositoryCreator
{
/**
* @return list<list<string>>
*/
public function commands(PackageRepositoryPlan $plan): array;

public function create(PackageRepositoryPlan $plan): void;
}
Loading
Loading