Code Standards
The Static Checks job scans the package you changed. Each failure line names its rule:
::error::[switch-stmt] switch() found (use match)Find the rule below. Rules are blocking (CI fails), advisory (CI warns) or reviewed (a maintainer checks by hand).
What gets scanned
Section titled “What gets scanned”The whole package, except vendor/, .git/, .github/, coverage/, node_modules/, tests/,
Tests/, testing/, benchmarks/, composer.lock, CHANGELOG.md, KNOWN_ISSUES.md, UPGRADING.md
and SECURITY.md. A pattern that fails in src/ is fine in a test.
Comments
Section titled “Comments”ascii-divider
Section titled “ascii-divider”Blocking. No decorative comment bars in any syntax: a run of ten or more -, =, #, *, _ or
~ after //, #, --, /*, {# or <!--, or a box-drawing run such as ──, ══ or ━━. This
covers every file type, including CSS, SQL, XML and Twig.
Fix: delete the bar. If a file needs sections to be readable, split it or reorder its methods.
comment-paragraph
Section titled “comment-paragraph”Blocking. No two consecutive // comment lines in PHP. Files under a config/ directory are
exempt, because a published config template is documentation.
Fix: keep one line, or move the reasoning into the docblock.
commented-code
Section titled “commented-code”Blocking. No // line in PHP that starts like code: $var, return , if (, foreach ,
Something::, use , public , private or protected .
Fix: delete it.
history-note
Section titled “history-note”Blocking. No // comment in PHP containing changed in, was previously, used to be, as of v,
since v1, deprecated in, renamed from, formerly, prior to v, TODO, FIXME, HACK or XXX.
Fix: history belongs in the CHANGELOG; open an issue for a TODO.
A comment that only restates the next line is not caught by any scanner, but a reviewer will ask you to remove it.
codecov-ignore
Section titled “codecov-ignore”Blocking. No @codeCoverageIgnore.
Fix: write the test, stubbing anything external. Inject the dependency so a test can replace it:
public function __construct(private readonly RawHttpClient $http) {}
// in the test$http = $this->createMock(RawHttpClient::class);$http->method('post')->willReturn(['ok' => true]);Language
Section titled “Language”switch-stmt
Section titled “switch-stmt”Blocking. No switch in PHP. match is strict, returns a value and cannot fall through.
$url = match ($provider) { 'anthropic' => self::ANTHROPIC_URL, default => self::OLLAMA_URL,};
$tier = match (true) { $tokens > 100_000 => 'large', $tokens > 10_000 => 'medium', default => 'small',};strict-types
Section titled “strict-types”Blocking. Every PHP file contains declare(strict_types=1). Files under tmpl/, templates/,
template/, views/ and view/ are exempt, as are the excluded folders above. Without it, PHP
silently converts argument types instead of failing.
<?php
declare(strict_types=1);
namespace PhpClaw\Tools;final-class
Section titled “final-class”Advisory. Classes should be final. abstract classes are not reported, and in adapters neither
is a Magento class marked // non-final: Magento interceptor required.
A class that extends a framework or CMS base class may need to stay open; the warning still appears,
and that is fine. Do not add a // non-final: comment to silence it; that exemption is for Magento
interceptors only.
Putting extension points behind an interface, rather than inheritance, keeps the contract explicit.
Security
Section titled “Security”exception-leak
Section titled “exception-leak”Blocking. Never send an exception message to the user. Exception text can carry file paths, SQL and credentials.
- In adapters, CI flags
echoof->getMessage()and an array value set to$e->getMessage(). - In
core,cloudandmcp, CI flags theechoform only. Those packages pass tool and guard messages back to the model and to MCP error frames, which are not end users.
Passing the exception to a logger method (error, warning, info and so on) is allowed.
// Refusedecho $e->getMessage();return ['error' => $e->getMessage()];
// Allowed$this->logger->error('Provider call failed', ['exception' => $e]);return ['error' => 'The request could not be completed.'];eval-call
Section titled “eval-call”Blocking. No eval(). A quoted 'eval(', for example in a list of forbidden calls, is not a call
and is not flagged.
Fix: to choose behaviour by name, use a map:
$handlers = ['sum' => fn (array $n): int => array_sum($n)];$result = ($handlers[$op] ?? throw new ToolException('Unknown operation'))($args);no-guzzle
Section titled “no-guzzle”Blocking. No Guzzle in the package’s composer.json. Use core’s RawHttpClient, so installing a
phpClaw package never adds an HTTP stack, or a version conflict, to someone else’s application.
use PhpClaw\Http\RawHttpClient;
$data = $this->http->post($url, ['Content-Type' => 'application/json'], $payload);Database
Section titled “Database”table-prefix
Section titled “table-prefix”Blocking, CMS adapters. Never hardcode the prefix on a phpClaw table. In .php files CI rejects
wp_, jos_, oc_ and ps_ followed by phpclaw_conversations, phpclaw_messages or
phpclaw_memory, in the WordPress, Joomla, OpenCart and PrestaShop adapters respectively. Drupal and
Magento have no fixed prefix to scan for, but the rule is the same.
| Platform | Correct form |
|---|---|
| WordPress | $wpdb->prefix.'phpclaw_messages' |
| Joomla | '#__phpclaw_messages' |
| Drupal | $this->database->select('phpclaw_messages', 'm') |
| Magento | $this->resource->getTableName('phpclaw_messages') |
| OpenCart | DB_PREFIX.'phpclaw_messages' |
| PrestaShop | _DB_PREFIX_.'phpclaw_messages' |
Docblocks
Section titled “Docblocks”docblock-summary
Section titled “docblock-summary”Blocking. At most two description lines before the first @ tag, in every PHP docblock outside
tests. Docblocks with three or more Label: lines, or a ## heading, are skipped as command help.
Fix: trim to a one-line summary and keep every tag.
/** * Resolve the provider for this request. * * @return ProviderInterface */Reviewed: the summary must also be true of the body as it is now. A summary on an authorisation check, a scrub, a guard or an ownership rule that claims more than the code enforces is treated as a security bug.
// Wrong: the body filters by owner unless the caller holds manage-all/** * Delete all conversations in the given namespace. */
// Right/** * Delete the acting user's conversations in the namespace, or every user's with manage-all. */Docblock on every function
Section titled “Docblock on every function”Blocking. This is a separate CI job, not part of Static Checks. Outside tests/, Tests/ and
benchmarks/:
- every named function needs a docblock;
- every documented function needs a
@paramfor each parameter, no@paramfor a parameter that does not exist, and a@return, including@return void.
Constructors and destructors are skipped by the tag check. @throws is not checked, but document it.
For a generic type such as array, document its shape, for example array<int, array<string, mixed>>.
A failure names each function:
::error::2 function(s) missing a docblock: src/Admin/SettingsPage.php:212 resolveApiKey()and a docblock missing tags fails with ::error::N docblock(s) with incomplete tags:.
/** * Forget a memory entry. * * @param string $key Memory key to remove. * @param string $namespace Scoping namespace. * @return bool True when a row was deleted. */public function forget(string $key, string $namespace): boolConstants and properties carry no docblock, unless a bare type tag such as /** @var resource */ is
the only way to express the type.
Markup
Section titled “Markup”inline-css
Section titled “inline-css”Advisory, CMS adapters. No <style> or style= in .twig, .tpl, .phtml or .blade.php
files. A display:none toggle and a CSS custom property such as --phpclaw-accent are not flagged.
Fix: move the rule into the adapter’s stylesheet and load it the platform’s way, such as
wp_enqueue_style() in WordPress or a *.libraries.yml library in Drupal:
| Platform | Load it with |
|---|---|
| WordPress | wp_enqueue_style() |
| Joomla | the Web Asset Manager, $wa->registerAndUseStyle() |
| Drupal | a *.libraries.yml library attached with #attached['library'] |
| Magento | layout XML <css src="..."/> |
| OpenCart | the document’s addStyle() |
| PrestaShop | the controller’s addCSS() |
Inline styles cannot be overridden by a theme or allowed by a strict Content Security Policy.
Reviewed standards
Section titled “Reviewed standards”PHP version
Section titled “PHP version”Every package requires PHP ^8.1, so code must run on 8.1: no readonly classes or standalone
null, false or true types (8.2), no typed class constants (8.3), no property hooks or asymmetric
visibility (8.4). 8.1 features are fine: enums, readonly properties, intersection types, the never
return type, first-class callable syntax and new in initializers.
Not every workflow tests on 8.1. Drupal, Joomla, Magento, OpenCart, PrestaShop and WordPress run 8.1 to 8.4; core, cloud and MCP run 8.2 to 8.4, and core also runs a lowest-dependencies job on 8.1; Laravel and Symfony run 8.4 only. A green build is not proof of 8.1 compatibility.
Types and state
Section titled “Types and state”- Explicit return types everywhere.
- No static state, except core’s registries and catalogues (
ToolRegistry,GuardRegistry,HookRegistry,MemoryRegistry,ProviderRegistry,SkillRegistryand their*Catalogueclasses) and the cachesHookRunContext,RateLimitGuard,ComposerExtras,DiscoveryCacheandUlid.
Architecture
Section titled “Architecture”packages/corehas no framework dependencies.- Depend on interfaces such as
ToolInterface,MemoryInterfaceandProviderInterface.
Formatting
Section titled “Formatting”Every package’s pint.json uses Laravel Pint’s laravel preset with no_superfluous_phpdoc_tags
turned off, so Pint keeps @return void and undescribed @param tags. Run composer lint and commit
the result. CI runs pint --test in the Static Analysis job and fails on unformatted code.
Rules by package type
Section titled “Rules by package type”| Rule | Adapters | core, cloud, mcp |
|---|---|---|
| ascii-divider, comment-paragraph, commented-code, history-note | blocking | blocking |
| codecov-ignore, switch-stmt, strict-types, eval-call, no-guzzle | blocking | blocking |
| docblock-summary | blocking | blocking |
| Docblock on every function | blocking, own job | blocking, own job |
| exception-leak | blocking, both forms | blocking, echo only |
| final-class | advisory | advisory |
| table-prefix | blocking, CMS adapters | not run |
| inline-css | advisory, CMS adapters | not run |
The CMS adapters are Drupal, Joomla, Magento, OpenCart, PrestaShop and WordPress. For the other CI jobs, see Contributing.