This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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/.
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.
PHPUnit must be invoked from tests/phpunit/ because phpunit.xml.dist references unit/ relative to that directory:
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 modeCI matrix runs against PHP 8.1, 8.2, 8.3 (.github/workflows/code_standards.yml, phpunit.yml).
Documentation work happens inside DDEV (Python/mkdocs toolchain is in the web container):
ddev init— full reset,composer install, build docs, open browserddev build-docs— runspolymer mk:docsthenmkdocs build --clean; view at:444ddev dev-docs— livemkdocs serveat: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.
bin/polymer→bin/polymer-robo-run.phpwalks up from CWD looking for a.polymer/directory; this is the repo root (everything is anchored to it).Polymer::boot()runs five phases in order:setupBootContainer→createApplication→discoverExtensions→setupContainer→configureRunner.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.
An external Composer package becomes a Polymer extension by:
- Having a directory under
<repoRoot>/.polymer/plugins/<name>/containing a<name>.poly_info.ymlmarker file, and being listed in<repoRoot>/.polymer/config.ymlunderenabled_extensions. - Exposing a
src/directory; its PSR-4 namespace is forced toDigitalPolygon\Polymer\<ExtensionId>\(seeExtensionDiscovery::registerExtensionNamespaces()— the class loader is mutated at runtime). - Providing an
ExtensionInfo.phpimplementingPolymerExtensionInterfaceat the root of that namespace. - Optionally providing a service provider next to
ExtensionInfo.php, named<CamelCaseExtensionId>ServiceProvider, implementingLeague\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
Config is not available during boot — it's compiled lazily on each command invocation. The flow:
LoadConfigurationevent subscriber fires at command start.- It dispatches
CollectConfigContextsEvent, allowing extensions to contribute named contexts (each a YAML-derived array). - Then
AlterConfigContextsEvent, allowing late mutation. PolymerConfig(extendsRobo\Config) layers contexts; higher contexts override lower keys.${tokens}are resolved against the merged result byYamlConfigProcessor.- The
processcontext 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.
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:frontendDocs/—mk:docs(regenerates per-command markdown for the docs site)Template/— template generation viaTemplatePluginManagerValidate/,Polymer/
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.
- Namespace prefix is
DigitalPolygon\Polymer\Core\in this package (theCoresegment is easy to miss when copying code from extensions, which useDigitalPolygon\Polymer\<ExtensionId>\). test_package/andtest_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.phpis excluded from PHPStan — it's vendored utility code.- Default branch for PRs is
0.x, notmain(see CI workflow triggers andmkdocs.yml: edit_uri). - GrumPHP runs
phpcs,phpstan, andphplinton commit viaphpro/grumphp-shim— same checks ascomposer validations.
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.