ACTIVATE when designing PHP classes, value objects, collections, or when the user asks about object design, encapsulation, or 'Tell Don't Ask'. Covers: Tell Don't Ask with concrete PHP examples, collection over named properties, Whole Object pattern, IteratorAggregate, self-describing value objects. DO NOT use for: refactoring methodology (see php-refactoring), DDD domain modeling (see php-ddd-conventions).
From phpnpx claudepluginhub fabiensalles/claude-marketplace --plugin phpThis skill uses the workspace's default tool permissions.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Designs, audits, and improves analytics tracking systems using Signal Quality Index for reliable, decision-ready data in marketing, product, and growth.
Enforces A/B test setup with gates for hypothesis locking, metrics definition, sample size calculation, assumptions checks, and execution readiness before implementation.
Project-specific OOP conventions. These focus on patterns where Claude tends to produce "ask" code instead of "tell" code.
The object that owns the data exposes the behavior. Do not extract data to make decisions externally.
// AVOID: caller queries and decides
foreach ($identityDocument->getRequiredFieldNames() as $fieldName) {
$file = $identityDocument->getFileByFieldName($fieldName);
if ($file !== null) { continue; }
// ... re-download logic
}
// CORRECT: object exposes behavior
foreach ($identityDocument->getMissingExistingFiles() as $fieldName => $existingFile) {
// Object already determined which files are missing
}
When elements share the same type and processing, use an indexed collection — even if the count is known and fixed.
// AVOID: N separate properties = N identical code paths
final class FormData {
private ?File $frontFile = null;
private ?File $backFile = null;
private ?File $passportFile = null;
}
// CORRECT: one collection
final class FormData {
/** @param array<string, File> $files */
private array $files = [];
public function addFile(string $fieldName, File $file): void { ... }
}
When multiple parameters come from the same object, pass the object. Extracting primitives is feature envy.
// AVOID: caller destructures
$collection->add(documentType: $document->type, documentName: $document->originalFileName, downloadUrl: $url);
// CORRECT: pass whole object
$collection->addFromDocument(document: $document, downloadUrl: $url);
IteratorAggregate)Implement IteratorAggregate to allow foreach while keeping internals private.
/** @implements \IteratorAggregate<string, FileInfo> */
final class FilesCollection implements \IteratorAggregate
{
public function __construct(private array $files = []) {}
public function add(string $name, FileInfo $file): void { $this->files[$name] = $file; }
public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->files); }
}
// Usage: foreach ($collection as $name => $file) { ... }
Include the type/identity in the value object so consumers need no external mapping.
// AVOID: consumer needs external fieldName -> fileType mapping
final class UploadFile {
public function __construct(
public readonly string $content,
public readonly string $originalFileName,
) {}
}
// CORRECT: object carries its own type
final class UploadFile {
public function __construct(
public readonly FileTypeEnum $type,
public readonly string $content,
public readonly string $originalFileName,
) {}
}
| Rule | Principle |
|---|---|
| Tell Don't Ask | The object exposes behavior, not data to interpret |
| Collection > named properties | Same nature + same processing = indexed collection |
| Whole Object | Pass the entire object, not its extracted primitives |
IteratorAggregate | Make iterable, keep internals private |
| Self-describing value object | Include type/identity in the object |