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
6 changes: 6 additions & 0 deletions .ddev/web-build/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
RUN composer global config --no-plugins allow-plugins.ion-bazan/composer-diff true; \
composer global require ion-bazan/composer-diff --no-interaction;
#RUN python -m pip install --upgrade pip
# The Python 3.13 base image ships an empty `setuptools` namespace package with no
# RECORD file, so `pip install -U setuptools` fails (uninstall-no-record-file) and
# legacy setup.py builds (e.g. the mkdocs-edit-url git package below) die with
# "module 'setuptools' has no attribute 'setup'". Install a real setuptools + wheel
# into /usr/local (earlier on sys.path) with --ignore-installed so it shadows the stub.
RUN pip install --break-system-packages --ignore-installed "setuptools<81" wheel
RUN pip install --break-system-packages \
mkdocs==1.5.3 \
mkdocs-material \
Expand Down
111 changes: 111 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

`digitalpolygon/polymer` is a **Composer plugin** that ships a Robo-based CLI (`bin/polymer`) for WebOps tooling — primarily artifact building/deployment for PHP/Drupal projects. It is intended to be extended by other packages (e.g. `polymer-drupal`, `polymer-pantheon-drupal`), not used as a standalone application.

Package autoload (note the `Core` segment): `DigitalPolygon\Polymer\Core\` → `src/`. Tests autoload: `DigitalPolygon\PolymerTest\` → `tests/`.

## Common commands

All Composer scripts are defined in `composer.json`:

| Command | What it does |
| --- | --- |
| `composer cs` | PHP_CodeSniffer against `phpcs.xml.dist` (PSR-12 over `src/` and `bin/`) |
| `composer lint` | Parallel `php -l` syntax check |
| `composer sa` | PHPStan level 6 against `src/` and `bin/` (config: `phpstan.neon`) |
| `composer validations` | Runs `lint`, `cs`, and `sa` in sequence — the CI gate |
| `composer nuke` / `composer nuke-install` | Wipe `vendor/` + lockfile (and reinstall) |

Per the user's global rules: after modifying PHP, run it through **PHPStan** (`composer sa`) before declaring the change done.

### Tests

PHPUnit must be invoked **from `tests/phpunit/`** because `phpunit.xml.dist` references `unit/` relative to that directory:

```bash
cd tests/phpunit && ../../vendor/bin/phpunit # full suite
cd tests/phpunit && ../../vendor/bin/phpunit --filter testMethodName # single test
cd tests/phpunit && ../../vendor/bin/phpunit-watcher watch # watch mode
```

CI matrix runs against PHP 8.1, 8.2, 8.3 (`.github/workflows/code_standards.yml`, `phpunit.yml`).

### DDEV (development environment)

Documentation work happens inside DDEV (Python/`mkdocs` toolchain is in the web container):

- `ddev init` — full reset, `composer install`, build docs, open browser
- `ddev build-docs` — runs `polymer mk:docs` then `mkdocs build --clean`; view at `:444`
- `ddev dev-docs` — live `mkdocs serve` at `:445` (preferred while editing docs)

When command-class docblocks/attributes change, regenerate the per-command `docs/commands/*.md` with `polymer mk:docs` before building the site.

## Architecture

### Bootstrap flow (`bin/polymer` → `Polymer::run()`)

1. `bin/polymer` → `bin/polymer-robo-run.php` walks up from CWD looking for a `.polymer/` directory; this is the **repo root** (everything is anchored to it).
2. `Polymer::boot()` runs five phases in order: `setupBootContainer` → `createApplication` → `discoverExtensions` → `setupContainer` → `configureRunner`.
3. `RoboRunner::run()` is handed the merged `[commands + hooks]` array; Robo + Symfony Console take over.

The two-container split (`bootContainer` then full `Container`) exists so extension discovery can run before the main DI container is finalized — extensions contribute service providers that need to register during container setup.

### Extension model (the core extensibility surface)

An external Composer package becomes a Polymer extension by:

1. Having a directory under `<repoRoot>/.polymer/plugins/<name>/` containing a `<name>.poly_info.yml` marker file, **and** being listed in `<repoRoot>/.polymer/config.yml` under `enabled_extensions`.
2. Exposing a `src/` directory; its PSR-4 namespace is **forced** to `DigitalPolygon\Polymer\<ExtensionId>\` (see `ExtensionDiscovery::registerExtensionNamespaces()` — the class loader is mutated at runtime).
3. Providing an `ExtensionInfo.php` implementing `PolymerExtensionInterface` at the root of that namespace.
4. Optionally providing a service provider next to `ExtensionInfo.php`, named `<CamelCaseExtensionId>ServiceProvider`, implementing `League\Container\ServiceProvider\ServiceProviderInterface`.

Within an extension, commands live in `Plugin\Commands\*Commands.php` and hooks in `Plugin\Hooks\*Hook.php`. Polymer Core's own commands follow the same `*Command.php` / `*Hook.php` suffix convention but are discovered separately via `CoreClassDiscovery` and the `Robo\Commands\` / `Robo\Hooks\` relative namespaces.

The discovery suffix mismatch is intentional and load-bearing — **don't "normalize" it**:
- Core: files end in `Command.php` / `Hook.php`
- Extensions: files end in `Commands.php` / `Hook.php`

### Configuration system (`src/Robo/Config/`)

Config is **not** available during boot — it's compiled lazily on each command invocation. The flow:

1. `LoadConfiguration` event subscriber fires at command start.
2. It dispatches `CollectConfigContextsEvent`, allowing extensions to contribute named contexts (each a YAML-derived array).
3. Then `AlterConfigContextsEvent`, allowing late mutation.
4. `PolymerConfig` (extends `Robo\Config`) layers contexts; higher contexts override lower keys. `${tokens}` are resolved against the merged result by `YamlConfigProcessor`.
5. The `process` context is always top-of-stack — runtime `$config->set()` writes go there.

**Important quirk**: `commandInvoker->invokeCommand()` reloads config from scratch for the invoked command, so a `$this->getConfig()->set('foo', 'bar')` before invoking a sub-command will **not** propagate. Use `pinGlobalOption()` for parameter pass-through. See `docs/developing/command_invoker.md`.

### Commands

Built using `consolidation/annotated-command` with PHP attributes (`#[Command]`, `#[Argument]`, `#[Option]`, `#[Usage]`). All command classes extend `TaskBase` (`src/Robo/Tasks/TaskBase.php`).

Commands prefixed `internal:` are hidden from `list` output by default — this is enforced by `CommandInfoAlterer` running *before* Robo's annotation processing, so `#[Help(hidden: false)]` cannot un-hide them (see `config/default.yml` comment).

Notable namespaces under `src/Robo/Commands/`:
- `Artifact/` — `artifact:compile`, `artifact:deploy`, `artifact:build:prepare`, `artifact:build:sanitize`, `artifact:composer:install` (the deployment pipeline)
- `Source/` — `source:build:copy`, `source:build:frontend`
- `Docs/` — `mk:docs` (regenerates per-command markdown for the docs site)
- `Template/` — template generation via `TemplatePluginManager`
- `Validate/`, `Polymer/`

### Composer plugin (`src/Composer/Plugin.php`)

Implements `PluginInterface` + `EventSubscriberInterface`. On `post-install` / `post-update`, if `digitalpolygon/polymer` itself was just installed/updated **and** `<repoRoot>/polymer/polymer.yml` does not yet exist, it shells out to `vendor/bin/polymer polymer:init` to scaffold project files. This is the only thing the Composer plugin does at runtime.

## Conventions and gotchas

- **Namespace prefix is `DigitalPolygon\Polymer\Core\` in this package** (the `Core` segment is easy to miss when copying code from extensions, which use `DigitalPolygon\Polymer\<ExtensionId>\`).
- `test_package/` and `test_drupal10/` are fixture projects used to exercise the Composer plugin / commands end-to-end; they are excluded from PHPStan and PHPCS. Don't lint or analyze them.
- `src/Robo/Common/ArrayManipulator.php` is excluded from PHPStan — it's vendored utility code.
- Default branch for PRs is `0.x`, not `main` (see CI workflow triggers and `mkdocs.yml: edit_uri`).
- GrumPHP runs `phpcs`, `phpstan`, and `phplint` on commit via `phpro/grumphp-shim` — same checks as `composer validations`.

## Issue tracking

Polymer development work is tracked in the **PWT** Jira project: https://digitalpolygon.atlassian.net/jira/software/projects/PWT/boards/96 — use the `acli` CLI (ADF format) for Jira interactions. The `composer.json` `support.issues` link points to GitHub issues, but PWT is the active board.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
See @AGENTS.md.
2 changes: 1 addition & 1 deletion bin/polymer
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env php
<?php

require_once __DIR__ . '/polymer-robo.php';
require_once __DIR__ . '/polymer-robo-run.php';
50 changes: 46 additions & 4 deletions bin/polymer-robo-run.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,36 @@
* Execute Polymer commands via Robo.
*/

use DigitalPolygon\Polymer\Robo\Polymer;
use DigitalPolygon\Polymer\Core\Robo\Polymer;
use Robo\Common\TimeKeeper;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;

$cwd = isset($_SERVER['PWD']) && is_dir($_SERVER['PWD']) ? $_SERVER['PWD'] : getcwd();
Comment thread
lpeabody marked this conversation as resolved.
if ($cwd === false) {
throw new \RuntimeException("Could not determine the current working directory.");
}

$autoloadFile = false;
// Set up autoloader
$candidates = [
$_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php', // https://getcomposer.org/doc/articles/vendor-binaries.md#finding-the-composer-autoloader-from-a-binary
__DIR__ . '/vendor/autoload.php', // For development of Polymer itself.
];
foreach ($candidates as $candidate) {
if (file_exists($candidate)) {
$autoloadFile = $candidate;
break;
}
}
if (!$autoloadFile) {
throw new \Exception("Could not locate autoload.php. cwd is $cwd; __DIR__ is " . __DIR__);
}
$classLoader = include $autoloadFile;
if (!$classLoader) {
throw new \Exception("Invalid autoloadfile: $autoloadFile. cwd is $cwd; __DIR__ is " . __DIR__);
}

// Start Timer.
$timer = new TimeKeeper();
$timer->start();
Expand All @@ -24,12 +49,29 @@
}

// Initialize configuration.
/** @var string $repoRoot */
$repoRoot = find_repo_root();
/** @var string|null $repoRoot */
$repoRoot = null;
// Walk up the directory tree until we find .polymer or reach the filesystem root.
while (true) {
if (file_exists($cwd . '/.polymer')) {
$repoRoot = $cwd;
break;
}
$parent = dirname($cwd);
if ($parent === $cwd) {
// We have reached the root of the filesystem.
break;
}
$cwd = $parent;
}

if (!$repoRoot) {
throw new \Exception("Could not find .polymer directory in this or any parent directory. cwd is $cwd; __DIR__ is " . __DIR__);
}

// Execute command.
// @phpstan-ignore variable.undefined
$polymer = new Polymer($repoRoot, $input, $output, $classLoader);
$polymer->boot();
$status_code = (int) $polymer->run($input, $output);

// Stop timer.
Expand Down
13 changes: 9 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
},
"require": {
"php": ">=8.1",
"oomphinc/composer-installers-extender": "*",
"mnsami/composer-custom-directory-installer": "*",
"composer-plugin-api": "^2.0",
"composer-runtime-api": "^2.0",
"consolidation/robo": "^4 || ^5",
Expand All @@ -22,19 +24,22 @@
"prefer-stable": true,
"autoload": {
"psr-4": {
"DigitalPolygon\\Polymer\\": "src/",
"DigitalPolygon\\Polymer\\Core\\": "src/",
"DigitalPolygon\\PolymerTest\\": "tests/"
}
},
"config": {
"sort-packages": true,
"allow-plugins": {
"phpstan/extension-installer": true,
"phpro/grumphp-shim": true
"composer/installers": true,
"mnsami/composer-custom-directory-installer": true,
"oomphinc/composer-installers-extender": true,
"phpro/grumphp-shim": true,
"phpstan/extension-installer": true
}
},
"extra": {
"class": "DigitalPolygon\\Polymer\\Composer\\Plugin"
"class": "DigitalPolygon\\Polymer\\Core\\Composer\\Plugin"
},
"bin": [
"bin/polymer"
Expand Down
3 changes: 3 additions & 0 deletions config/deploy-exclude.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@ local.site.yml
node_modules
/vendor
local.polymer.yml
/scratch
/.polymer/core
/.polymer/plugins/contrib
2 changes: 1 addition & 1 deletion src/Composer/Plugin.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace DigitalPolygon\Polymer\Composer;
namespace DigitalPolygon\Polymer\Core\Composer;

use Composer\Composer;
use Composer\IO\IOInterface;
Expand Down
4 changes: 2 additions & 2 deletions src/Environment/AcquiaEnvironmentDetector.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?php

namespace DigitalPolygon\Polymer\Environment;
namespace DigitalPolygon\Polymer\Core\Environment;

use DigitalPolygon\Polymer\Environment\EnvironmentDetectorBase;
use DigitalPolygon\Polymer\Core\Environment\EnvironmentDetectorBase;

/**
* Class AcquiaEnvironmentDetector
Expand Down
2 changes: 1 addition & 1 deletion src/Environment/DDEVEnvironment.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace DigitalPolygon\Polymer\Environment;
namespace DigitalPolygon\Polymer\Core\Environment;

use Consolidation\Config\Loader\YamlConfigLoader;

Expand Down
4 changes: 2 additions & 2 deletions src/Environment/EnvironemntDetectorInterface.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?php

namespace DigitalPolygon\Polymer\Environment;
namespace DigitalPolygon\Polymer\Core\Environment;

use DigitalPolygon\Polymer\Robo\Exceptions\PolymerException;
use DigitalPolygon\Polymer\Core\Robo\Exceptions\PolymerException;

interface EnvironemntDetectorInterface
{
Expand Down
2 changes: 1 addition & 1 deletion src/Environment/EnvironmentDetectorBase.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace DigitalPolygon\Polymer\Environment;
namespace DigitalPolygon\Polymer\Core\Environment;

/**
* Class EnvironmentDetectorBase
Expand Down
4 changes: 2 additions & 2 deletions src/Environment/PantheonEnvironmentDetector.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?php

namespace DigitalPolygon\Polymer\Environment;
namespace DigitalPolygon\Polymer\Core\Environment;

use DigitalPolygon\Polymer\Environment\EnvironmentDetectorBase;
use DigitalPolygon\Polymer\Core\Environment\EnvironmentDetectorBase;

/**
* Class PantheonEnvironmentDetector
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?php

namespace DigitalPolygon\Polymer\Polymer\Plugin\Template\GitHubWorkflows;
namespace DigitalPolygon\Polymer\Core\Plugin\Template\GitHubWorkflows;

use DigitalPolygon\Polymer\Robo\Template\GitHub\GitHubWorkflowTemplateBase;
use DigitalPolygon\Polymer\Core\Robo\Template\GitHub\GitHubWorkflowTemplateBase;

class ComposerDiff extends GitHubWorkflowTemplateBase
{
Expand Down
4 changes: 2 additions & 2 deletions src/Robo/Commands/Artifact/BuildPrepareCommand.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<?php

namespace DigitalPolygon\Polymer\Robo\Commands\Artifact;
namespace DigitalPolygon\Polymer\Core\Robo\Commands\Artifact;

use Consolidation\AnnotatedCommand\Attributes\Command;
use Consolidation\AnnotatedCommand\Attributes\Usage;
use DigitalPolygon\Polymer\Robo\Tasks\TaskBase;
use DigitalPolygon\Polymer\Core\Robo\Tasks\TaskBase;
use Robo\Contract\VerbosityThresholdInterface;
use Robo\Exception\TaskException;

Expand Down
4 changes: 2 additions & 2 deletions src/Robo/Commands/Artifact/BuildSanitizeCommand.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<?php

namespace DigitalPolygon\Polymer\Robo\Commands\Artifact;
namespace DigitalPolygon\Polymer\Core\Robo\Commands\Artifact;

use Consolidation\AnnotatedCommand\Attributes\Command;
use Consolidation\AnnotatedCommand\Attributes\Usage;
use DigitalPolygon\Polymer\Robo\Tasks\TaskBase;
use DigitalPolygon\Polymer\Core\Robo\Tasks\TaskBase;
use Robo\Exception\TaskException;

/**
Expand Down
4 changes: 2 additions & 2 deletions src/Robo/Commands/Artifact/CompileCommand.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<?php

namespace DigitalPolygon\Polymer\Robo\Commands\Artifact;
namespace DigitalPolygon\Polymer\Core\Robo\Commands\Artifact;

use Consolidation\AnnotatedCommand\Attributes\Argument;
use Consolidation\AnnotatedCommand\Attributes\Command;
use Consolidation\AnnotatedCommand\Attributes\Usage;
use DigitalPolygon\Polymer\Robo\Tasks\TaskBase;
use DigitalPolygon\Polymer\Core\Robo\Tasks\TaskBase;
use Robo\Exception\TaskException;
use Robo\Symfony\ConsoleIO;

Expand Down
4 changes: 2 additions & 2 deletions src/Robo/Commands/Artifact/ComposerInstallCommand.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<?php

namespace DigitalPolygon\Polymer\Robo\Commands\Artifact;
namespace DigitalPolygon\Polymer\Core\Robo\Commands\Artifact;

use Consolidation\AnnotatedCommand\Attributes\Command;
use Consolidation\AnnotatedCommand\Attributes\Usage;
use DigitalPolygon\Polymer\Robo\Tasks\TaskBase;
use DigitalPolygon\Polymer\Core\Robo\Tasks\TaskBase;
use Robo\Contract\VerbosityThresholdInterface;
use Robo\Exception\TaskException;

Expand Down
10 changes: 5 additions & 5 deletions src/Robo/Commands/Artifact/DeployCommand.php
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<?php

namespace DigitalPolygon\Polymer\Robo\Commands\Artifact;
namespace DigitalPolygon\Polymer\Core\Robo\Commands\Artifact;

use Consolidation\AnnotatedCommand\AnnotationData;
use Consolidation\AnnotatedCommand\Attributes\Hook;
use Consolidation\AnnotatedCommand\CommandData;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use DigitalPolygon\Polymer\Robo\ConsoleApplication;
use DigitalPolygon\Polymer\Robo\Exceptions\PolymerException;
use DigitalPolygon\Polymer\Robo\Tasks\TaskBase;
use DigitalPolygon\Polymer\Core\Robo\ConsoleApplication;
use DigitalPolygon\Polymer\Core\Robo\Exceptions\PolymerException;
use DigitalPolygon\Polymer\Core\Robo\Tasks\TaskBase;
use Robo\Contract\VerbosityThresholdInterface;
use Robo\Symfony\ConsoleIO;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -40,7 +40,7 @@ class DeployCommand extends TaskBase
/**
* This hook will fire for all commands in this command file.
*
* @throws \DigitalPolygon\Polymer\Robo\Exceptions\PolymerException
* @throws \DigitalPolygon\Polymer\Core\Robo\Exceptions\PolymerException
*/
#[Hook(type: HookManager::INITIALIZE)]
public function initialize(): void
Expand Down
Loading
Loading