Backend PHP
PHP services and APIs (Symfony, Laravel, Doctrine, PDO) with Composer-true autoloading and platforms, explicit dependencies, handled failures, safe input, SQL, shell and secrets, owned transactions and evidenced completion. Rule IDs PHP-001–PHP-080; NEED TO blocks.
Set this profile
ccpp cpo profile set backend-phpInside Claude Code, prefix the command with ! to run it in the session. Downloading a profile needs a Builder or Team plan; listing them does not.
Policies
78 policies, grouped by what they guard.
Secrets & private data 3
Credentials, keys and personal data stay out of files, logs, commands and the conversation.
php036-sensitive-loggingPHP-036 · Log calls never receive passwords, tokens, secrets, card data, authorization headers or whole request payloads
php044-weak-password-hashingPHP-044 · Passwords are hashed with password_hash/the framework hasher, never md5, sha1, a plain hash, crypt or reversible encoding
php047-credential-literalsPHP-047 · Real credentials (passwords, API keys, tokens, DSN passwords) are loaded from secret configuration, never written as literals
Authentication & authorization 4
Who may do what, from roles, privileges and permissions to CSRF, CORS and verified tokens.
php041-unprotected-actionsPHP-041 · Every public action of a declared protected controller checks authorization (off until protectedEndpointPaths is set)
php042-unscoped-object-lookupsPHP-042 · A controller loading an object by a client-supplied ID checks ownership or permission for that object (review)
php043-csrf-disabledPHP-043 · The user approves disabling CSRF protection (forms, framework config, middleware exceptions)
Untrusted input 8
Requests, downloads and tool output are validated before they reach SQL, a shell, eval or a deserializer.
php025-coercing-raw-inputPHP-025 · Request values are validated before a cast or intval() turns garbage into a valid-looking scalar
php026-assert-as-validationPHP-026 · Input and security checks are runtime guards, never assert() (disabled in production by zend.assertions=-1)
php039-unvalidated-payloadsPHP-039 · A controller that reads a raw request payload validates it server-side (validator, form, mapped DTO; review)
php040-mass-assignmentPHP-040 · Request data never populates models wholesale (create/fill/update with all input, empty $guarded, unguard, OBJECT_TO_POPULATE without groups)
php045-unserialize-external-inputPHP-045 · Request, cookie or other external data never reaches unserialize()
php046-shell-command-injectionPHP-046 · Shell commands are never built from runtime values; processes take an argv array (Symfony Process, proc_open array)
php050-sql-interpolationPHP-050 · SQL and DQL text never interpolates or concatenates runtime values; values are bound parameters
php051-dynamic-identifiersPHP-051 · Table, column and sort identifiers in SQL come from an allowlist, never from the request or an unchecked variable
Network & TLS 3
Outbound data, verified certificates, destinations taken from requests and services exposed to others.
php048-tls-verification-configPHP-048 · Production HTTP client configuration keeps TLS peer and hostname verification on (Symfony framework.http_client)
php048-tls-verification-disabledPHP-048 · Production HTTP clients keep TLS peer and hostname verification on (curl, Guzzle, Symfony HttpClient, streams, Laravel Http)
php049-request-controlled-destinationsPHP-049 · Outbound requests do not go to a URL taken straight from the request without a destination policy (SSRF; review)
Weakened checks 2
Suppressions, disabled lint or CI steps and relaxed strictness need a reason or the user's approval.
php077-unexplained-suppressionsPHP-077 · A new inline suppression names the rule and says why (@phpstan-ignore identifier (reason), @psalm-suppress Issue with a reason, phpcs:ignore Sniff)
php077-weakened-checksPHP-077 · The user approves changes that lower PHPStan/Psalm strictness, grow baselines or ignores, exclude tests or stop CI from failing on checks
Type safety 5
No escapes from the type checker (any, unchecked casts, non-null assertions) and explicit contracts.
php021-missing-native-typesPHP-021 · New functions, methods and properties declare native parameter, return and property types
php022-broad-typesPHP-022 · New public contracts narrow mixed and object when the accepted or returned values are known (review)
php023-untyped-collectionsPHP-023 · New public methods taking or returning array/iterable document element types or the shape (list<T>, array<K, V>, array{…})
php024-misleading-var-annotationsPHP-024 · An inline @var does not drop the null a call may return (intelephense resolves the call's type)
php031-loose-comparisonsPHP-031 · New comparisons with null, booleans, '' or 0 and new in_array/array_search calls are strict
Error handling 3
Failures are propagated or handled, never swallowed or turned into fake success.
php032-swallowed-failuresPHP-032 · No empty catch block and no catch that reports success; a failure is recovered, translated or propagated
php033-lost-failure-causesPHP-033 · A catch that throws a new exception passes the caught one as previous; a broad catch rethrows or logs (review)
php034-fabricated-resultsPHP-034 · A failed write, payment or lookup does not return a fabricated success, ID or default (review)
Async & concurrency 2
Owned tasks, awaited promises, preserved cancellation and no blocking on event loops.
php062-rich-message-payloadsPHP-062 · Async messages and jobs carry identifiers and serializable data, not entities, requests, resources or services
php065-discarded-failed-messagesPHP-065 · Async transports keep exhausted messages (a failure transport or failed-job store), instead of discarding them
Performance & resource bounds 2
Timeouts, bounded retries and fan-out, reused clients, closed resources, no N+1 queries.
php060-unbounded-outbound-ioPHP-060 · Outbound HTTP calls have a time limit (Guzzle timeout, curl CURLOPT_TIMEOUT, stream context timeout; review)
php061-unbounded-retriesPHP-061 · Retry loops are bounded (attempt counter, maximum, backoff) instead of retrying forever (review)
Architecture & boundaries 9
Declared layers and dependency directions, no import cycles, small modules, explicit dependencies.
php007-namespace-boundariesPHP-007 · Imports respect the project's declared module dependency directions (off until namespaceRules is set)
php008-framework-in-domainPHP-008 · Declared domain code does not depend on HTTP, routing, container or controller types (off until domainPaths is set)
php009-side-effects-in-business-codePHP-009 · Business code reaches databases, files, the network and processes through injected adapters, not raw sinks
php011-container-injectionPHP-011 · Business services take specific dependencies, not the general service container
php013-infrastructure-clients-in-codePHP-013 · Production classes inject database, HTTP, cache, queue and mail clients instead of constructing them
php018-application-bundlesPHP-018 · An application organizes features with namespaces; a new bundle class needs a reuse purpose (review)
php020-interface-per-classPHP-020 · A turn does not generate an interface for every new class (two or more <Name>Interface each with one new implementation; review, once per stop)
php067-wall-clock-in-business-codePHP-067 · Business time decisions read an injected clock (PSR-20 ClockInterface), not time(), date() or new DateTime()
Language idioms 4
The conventions of the language that keep code predictable for its readers.
php015-final-value-objectsPHP-015 · (suggestion) A new readonly class that is not designed for extension is final
php027-strict-types-conventionPHP-027 · New PHP files follow the project's declare(strict_types=1) convention (strictTypes parameter or the coding-standard config)
php028-promotable-constructorPHP-028 · (suggestion) A constructor that only copies parameters into properties can promote them (PHP 8.0+)
php059-float-moneyPHP-059 · New monetary amounts are not floats; they use minor units (int) or exact decimals with a currency and rounding rule (review)
HTTP & APIs 3
Request and response contracts, schemas, status codes, error bodies and published API surfaces.
php038-exception-details-in-responsesPHP-038 · HTTP responses never carry stack traces, file paths or the raw message of an unexpected exception
php078-public-contract-changesPHP-078 · The user approves removing or changing public signatures of @api code or declared public API paths
php079-api-schema-driftPHP-079 · A turn that adds, removes or changes HTTP routes also updates the published API schema (review, once per stop)
Databases & queries 5
Schemas, queries, transactions and the ORM used as intended.
php052-multi-write-without-transactionPHP-052 · A method making several writes that must succeed together owns a transaction (review)
php053-rollback-without-propagationPHP-053 · A catch that rolls back a transaction rethrows (or translates) the failure
php053-transaction-without-rollbackPHP-053 · A method that begins a transaction rolls it back on the exceptional path (catch or finally)
php054-flush-in-message-handlersPHP-054 · Message handlers do not flush or commit when the bus's doctrine_transaction middleware owns the unit of work
php066-side-effects-in-transactionsPHP-066 · Messages, e-mails and remote calls inside a database transaction wait for the commit (after-commit, outbox; review)
Schema migrations 4
Migrations come from the tool, applied history stays immutable and risky changes are rolled out safely.
php055-deleted-migrationsPHP-055 · The user approves deleting or renaming a committed migration
php055-migration-edit-ledgerPHP-055 · Records migrations Claude wrote with the user's approval this session, so the edit is asked once per file
php055-released-migrationsPHP-055 · The user approves changing a committed migration (it may already have run somewhere); new changes go in a new migration
php056-destructive-schema-changesPHP-056 · The user approves a migration that drops, renames or retypes schema in its up step (expand/contract for rolling deploys)
Framework conventions 1
Routing, server and client boundaries, hooks and plugins the way the framework expects them.
php001-autoload-mappingPHP-001 · A class, interface, trait or enum sits where Composer's PSR-4 mapping expects it (namespace and file name)
Versions & deprecated APIs 1
Code matches the installed version, without removed, renamed or deprecated APIs.
php002-platform-syntaxPHP-002 · New code uses no syntax newer than the PHP version composer.json declares (config.platform.php, else require.php)
Logging & observability 1
Structured logs, bounded metric labels, no debug prints left in production code.
php035-debug-outputPHP-035 · Reusable backend code has no var_dump/dd/dump/print_r, echo/print or exit/die; only entrypoints print or terminate
Dependencies & toolchain 7
Packages change through the package manager, lockfiles stay consistent and the project's tools are used.
php002-undeclared-extensionsPHP-002 · New calls into a PHP extension (mbstring, intl, bcmath, gmp, redis…) need ext-* or a polyfill in composer.json
php004-dev-dependencies-in-productionPHP-004 · Production code does not import namespaces that only require-dev packages or autoload-dev provide
php005-hand-edited-lockfilePHP-005 · composer.lock is written by Composer, never edited by hand (its content-hash must match composer.json)
php005-stale-lockfilePHP-005 · A turn that changes composer.json dependencies also updates composer.lock (application projects; once per stop)
php005-unlocked-deploysPHP-005 · Build and deployment scripts install from composer.lock; they never run an unconstrained composer update
php006-full-dependency-updatesPHP-006 · composer update names the packages it updates instead of re-resolving every dependency
php080-new-packagesPHP-080/PHP-006 · A new Composer package names the changed feature or tooling that needs it (review)
Tests 6
Tests assert behavior, stay deterministic and are not removed or skipped silently.
php068-untested-changesPHP-068/PHP-069/PHP-073 · A turn that changes production PHP code changes tests too, or says why none applies (review, once per stop)
php072-sql-choreography-mocksPHP-072 · Tests assert outcomes, not the SQL text a mocked connection or statement receives (review)
php074-tests-terminating-the-runnerPHP-074 · Tests never call exit or die (they end the PHPUnit run with a false result) unless run in a separate process
php075-meaningless-assertionsPHP-075 · Tests make assertions that can fail (no assertTrue(true), assertSame($x, $x) or bare assertion counters)
php077-removed-testsPHP-077 · The user approves edits that remove test methods or assertions from a test file
php077-skipped-testsPHP-077 · The user approves newly skipped, incomplete or todo tests
Completion evidence 5
A turn ends with its checks run and passing; claims of success need proof after the last edit.
php-turn-baselineRecords the work tree when a prompt starts, the baseline of the end-of-turn checks (PHP-003, PHP-005, PHP-068, PHP-079)
php003-unresolved-symbolsPHP-003 · Claude does not end a turn with syntax errors, undefined types, functions or variables, or calls with too many arguments it introduced (intelephense)
php075-placeholdersPHP-075 · Production code ships implemented behavior, not "not implemented" exceptions, TODO-only bodies or TODO stub returns
php076-completion-claimsPHP-076 · A final message claiming passing tests or checks needs a successful PHPUnit/Pest/PHPStan/Psalm/… run after Claude's last edit
php076-evidence-ledgerPHP-076 · Records, per session, Claude's edits and the PHP test, static-analysis and architecture runs that succeeded after them