Install
$ agentstack add skill-peterfox-agent-skills-php-type-safety ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
PHP Type Safety
Combine runtime assertions (webmozarts/assert) with static analysis annotations (PHPDoc shapes, templates) to write PHP code that is safe at runtime and satisfies PHPStan/Psalm at strict levels. The two approaches are complementary: PHPDoc narrows types for the analyser, assertions enforce them at runtime where PHP's type system cannot.
Setup
composer require webmozart/assert
composer require --dev phpstan/phpstan-webmozart-assert # PHPStan users only
The PHPStan extension translates Assert::*() calls into type-narrowing expressions so PHPStan refines variable types after each assertion. If you use phpstan/extension-installer it registers automatically; otherwise add it to phpstan.neon:
includes:
- vendor/phpstan/phpstan-webmozart-assert/extension.neon
Psalm users need no extra plugin — the Assert library ships with @psalm-assert annotations on every method and Psalm reads them natively.
Core workflow
- Annotate first — write the PHPDoc shape or type hint so the analyser knows the expected type.
- Assert at boundaries — use
Assert::*()at the entry point of any method that receives data from outside your control (user input, deserialized data, external APIs, mixed-typed collections). - Avoid redundant assertions — if PHP's native type system or a previous assertion already guarantees the type, a second assertion adds noise.
- Define type aliases for shapes used in more than one place.
PHPDoc type annotations
Scalar and object types
Prefer native type hints. Use PHPDoc only when native PHP cannot express the type:
/** @param non-empty-string $slug */
public function findBySlug(string $slug): void {}
/** @param positive-int $count */
public function paginate(int $count): void {}
/** @param class-string $class */
public function resolve(string $class): object {}
Array shapes — exact structure
Use array shapes for arrays with a known, fixed set of keys:
/**
* @param array{id: int, name: string, email?: string} $user
* @return array{token: string, expires_at: int}
*/
public function createSession(array $user): array {}
email?: string marks email as optional (may be absent).
Generic arrays — maps and lists
Use generic notation for arrays where all values share a type:
/** @param array $scores */ // map: string keys, int values
/** @param list $users */ // list: 0-indexed, sequential
/** @param non-empty-list $tags */ // list with at least one item
Use list (not array) for sequential integer-indexed arrays — it carries stronger guarantees that PHPStan/Psalm can leverage.
Type aliases — reusable shapes
Define shapes once in the class that owns the data, import elsewhere:
/**
* @phpstan-type UserData array{id: int, name: string, roles: list}
* @psalm-type UserData = array{id: int, name: string, roles: list}
*/
class User {}
// In another class:
/**
* @phpstan-import-type UserData from User
* @psalm-import-type UserData from User
*/
class UserRepository {
/** @return list */
public function findAll(): array {}
}
PHPStan and Psalm have slightly different syntax; include both tags for cross-tool compatibility.
Templates (generics)
/**
* @template T
* @param list $items
* @param callable(T): bool $predicate
* @return list
*/
function filter(array $items, callable $predicate): array {
return array_values(array_filter($items, $predicate));
}
/**
* @template T of \Throwable
* @param class-string $exceptionClass
* @param callable(): void $fn
* @throws T
*/
function rethrowAs(string $exceptionClass, callable $fn): void
{
try {
$fn();
} catch (\Throwable $e) {
throw new $exceptionClass($e->getMessage(), 0, $e);
}
}
Runtime assertions with webmozarts/assert
Basic usage
use Webmozart\Assert\Assert;
public function setAge(int $age): void
{
Assert::range($age, 0, 150);
$this->age = $age;
}
public function handle(mixed $payload): void
{
Assert::isArray($payload);
Assert::keyExists($payload, 'type');
Assert::string($payload['type']);
Assert::inArray($payload['type'], ['create', 'update', 'delete']);
}
Bulk assertions with all* and nullOr*
/** @param list $tags */
public function setTags(array $tags): void
{
Assert::allStringNotEmpty($tags); // every element is a non-empty string
$this->tags = $tags;
}
/** @param array $scores */
public function setScores(array $scores): void
{
Assert::allNullOrInteger($scores); // each value is int or null
}
Combining with PHPDoc shapes
Assertions narrow types for runtime; PHPDoc shapes narrow them for the analyser. Use both together at the boundary of untrusted data:
/**
* @phpstan-type RawConfig array{dsn: string, pool: int, timeout?: float}
* @param array $raw
* @return RawConfig
*/
private function validateConfig(array $raw): array
{
Assert::keyExists($raw, 'dsn');
Assert::string($raw['dsn']);
Assert::keyExists($raw, 'pool');
Assert::positiveInteger($raw['pool']);
if (isset($raw['timeout'])) {
Assert::float($raw['timeout']);
}
/** @var RawConfig $raw */
return $raw;
}
The @var cast is a forced assertion — it tells the analyser to treat $raw as RawConfig from that point on. The analyser trusts it; the assertions above are what actually enforce it at runtime.
Legacy inference mode
Use this mode when asked to "infer types", "annotate without changing the API", or improve type visibility in existing code where modifying method signatures would risk breaking callers.
The constraint: add type information only through PHPDoc and internal assertions. Never add or change native PHP type hints (string $name, : int, ?Foo), never alter a method or function signature in any way.
What you may add
| Addition | Example | |----------|---------| | @param / @return PHPDoc on methods | /** @param list $ids */ | | @var on class properties | /** @var array */ | | @var on local variables | /** @var non-empty-string $slug */ | | Assert::*() inside method bodies | After receiving or unpacking a value | | Type alias definitions (@phpstan-type) | On the class, not on the signature |
What you must not touch
- Native parameter types (
string $x,int $x,?Foo $x) - Native return types (
: string,: void,: ?int) - Anything else that changes the method or function signature
Inference workflow
- Read the method body — trace how each parameter and return value is actually used.
Assignment to a typed property, a method call that requires a specific type, a comparison — all give clues.
- Check callers — if accessible, read call sites to see what values are passed.
- Annotate conservatively — if you cannot determine the type with confidence, use
mixed or the broadest type you can prove. Do not guess.
- Add assertions where the inferred type is not guaranteed — if callers could pass
anything and the body assumes a string, guard it with Assert::string() at the top.
- Use
@varfor local ambiguity — when a variable is assigned from a source the
analyser cannot follow (e.g. array_shift, casting, unserialize), annotate inline.
Example
Before — legacy code, no type information:
class OrderService
{
private $repository;
private $items = [];
public function add($product, $qty)
{
$this->items[] = ['product' => $product, 'qty' => $qty];
}
public function total()
{
$sum = 0;
foreach ($this->items as $line) {
$sum += $line['product']->price * $line['qty'];
}
return $sum;
}
}
After — annotated in inference mode (signatures untouched):
/**
* @phpstan-type LineItem array{product: Product, qty: positive-int}
*/
class OrderService
{
/** @var ProductRepository */
private $repository;
/** @var list */
private $items = [];
/**
* @param Product $product
* @param positive-int $qty
*/
public function add($product, $qty)
{
Assert::isInstanceOf($product, Product::class);
Assert::positiveInteger($qty);
$this->items[] = ['product' => $product, 'qty' => $qty];
}
/**
* @return float
*/
public function total()
{
$sum = 0;
foreach ($this->items as $line) {
$sum += $line['product']->price * $line['qty'];
}
return $sum;
}
}
The public API is identical. PHPStan/Psalm now understands the property types, the expected parameter contracts, and the return type — and the assertions catch violations at runtime during development.
Decision rules
- Do not assert what PHP already enforces. If a parameter has
string $name, don't addAssert::string($name). - Assert at boundaries, not everywhere. Internal private methods working with already-typed data don't need assertions.
- Use
Assert::isInstanceOf()instead of a cast. It produces a cleaner error than a failed cast and narrows the type for PHPStan/Psalm. - Prefer specific assertions over generic ones.
Assert::positiveInteger()communicates intent better thanAssert::integer()+ a manual range check. - Open shapes for extensible structures. When a shape may contain extra keys you don't control, Psalm users can use
array{known: type, ...}; PHPStan users should document this in the PHPDoc prose or usearray. - In inference mode, annotate conservatively. A
mixedannotation you can prove is safer than a specific type you cannot. Incorrect PHPDoc is worse than missing PHPDoc — it misleads the analyser and future readers.
Reference files
references/assert-methods.md— Full categorised reference for all Assert methods; read when you need a specific assertion or thenullOr/allprefix variants.references/phpdoc-shapes.md— Deep reference for array shapes, list types, conditional types, and cross-tool differences between PHPStan and Psalm; read when annotating complex structures.references/phpstan-extension.md— Installation, configuration, and the exact list of assertions that cause PHPStan type narrowing; read when debugging why PHPStan is not narrowing a type after an assertion, or when verifying the extension is working.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: peterfox
- Source: peterfox/agent-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.