
Php
- 3 installs
- 19 repo stars
- Updated August 1, 2026
- xobotyi/cc-foundry
Helps with ai & agent building tasks.
About
php is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- php
- AI & Agent Building
- AI-coding skill
Php by the numbers
- 3 all-time installs (skills.sh)
- Ranked #13,677 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/xobotyi/cc-foundry --skill phpAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 19 |
| Last updated | August 1, 2026 |
| Repository | xobotyi/cc-foundry ↗ |
What it does
Helps with ai & agent building tasks.
Files
PHP
Strict types, explicit contracts, no magic. If a class needs a docblock to explain what its properties do, the properties are named wrong.
PHP 8.5+ is the baseline. Use modern syntax unconditionally — union types, enums, readonly classes, property hooks, named arguments, match, pipe operator. No backward compatibility with older PHP versions unless the project explicitly requires it.
Every PHP file starts with declare(strict_types=1).
References
| Topic | Reference | Contents |
|---|---|---|
| Type system | ${CLAUDE_SKILL_DIR}/references/typing.md | Union/intersection/DNF types, nullable patterns, typed properties and constants, coercion rules, variance |
| OOP patterns | ${CLAUDE_SKILL_DIR}/references/oop.md | Interfaces, traits, readonly, property hooks, enums, constructor promotion, lazy objects, magic methods |
| Concurrency | ${CLAUDE_SKILL_DIR}/references/concurrency.md | Fiber API, generator coroutines, comparison table, async library guidance |
| Packaging | ${CLAUDE_SKILL_DIR}/references/packaging.md | composer.json templates, version constraints, project layouts, namespace conventions |
Naming
| Entity | Style | Examples |
|---|---|---|
| Classes, interfaces, traits, enums | PascalCase | UserService, Renderable, Status |
| Methods, functions | camelCase | findById, getFullName |
| Properties, variables | camelCase | $userName, $isActive |
| Constants (class and global) | UPPER_SNAKE_CASE | MAX_RETRIES, DEFAULT_LOCALE |
| Namespaces | PascalCase segments | App\Http\Controller |
| Enum cases | PascalCase | Status::Active, Suit::Hearts |
- Descriptive names.
$userCountnot$n. Short names ($i,$k,$v) only in
tiny scopes (loops, array operations).
- No redundant context.
$car->makenot$car->carMake. - Boolean names:
is/has/can/shouldprefix:$isValid,$hasAccess. - Abbreviations as words.
HttpClientnotHTTPClient,JsonParsernotJSONParser.
Treat abbreviations and acronyms as regular words — uppercase first letter only (PER-CS).
- No underscore prefix for protected/private visibility. Visibility modifiers exist
for that.
Type Declarations
PHP 8.5+ provides a complete type system. Use it everywhere.
Core Rules
- Type all public API boundaries — function parameters, return types, class properties,
class constants. Internal/private code benefits from types too.
- `declare(strict_types=1)` in every file. No exceptions.
- Short type names:
bool,int,float,string. Neverboolean,integer,
double.
- Union types with `|`:
string|int,Foo|null. Prefer?Tfor single-type nullable. - Intersection types with `&`:
Countable&Traversable. Class/interface types only. - DNF types:
array|(ArrayAccess&Traversable)— union of intersections in parentheses. - `void` return: annotate on functions that return nothing.
- `never` return: functions that always throw or exit.
- Avoid `mixed` — it disables type safety. Use
objectwhen you mean "any object."
Use mixed only at true interop boundaries with untyped code.
- `null` last in unions:
string|int|null, notnull|string|int.
Typed Properties
- Every class property gets a type declaration.
- Typed properties must be initialized before access — use constructor promotion,
default values, or constructor assignment.
callablecannot be used as a property type. UseClosureinstead.
Typed Constants (8.3+)
class Config
{
public const int MAX_RETRIES = 3;
public const string DEFAULT_LOCALE = 'en';
protected const float TAX_RATE = 0.21;
}- Type all class constants. Interface constants benefit especially — they enforce the
contract at compile time.
Variance
- Parameters are contravariant — child class can accept wider types.
- Return types are covariant — child class can return narrower types.
mixedreturn can be narrowed to any type in a subclass.
See ${CLAUDE_SKILL_DIR}/references/typing.md for the complete type system reference.
Enumerations
- Use enums for categorical constants. Never bare strings or ints as pseudo-enums.
- Backed enums (
stringorint) when the value must interoperate with external
systems (JSON, database, API).
- `from()` throws on invalid value; `tryFrom()` returns `null`. Choose based on
whether invalid input is a caller error or expected.
- Enums can implement interfaces and define methods. Use this for behavior tied to
the enum's domain.
- Enums cannot have state (no properties), cannot be extended, cannot be
new'd. - Dynamic access:
Status::{$name}(8.3+) for variable-based case resolution.
enum Status: string
{
case Active = 'active';
case Inactive = 'inactive';
case Suspended = 'suspended';
public function label(): string
{
return match ($this) {
self::Active => 'Active',
self::Inactive => 'Inactive',
self::Suspended => 'Suspended',
};
}
}Classes
Properties and Visibility
- Explicit visibility on everything — properties, methods, constants.
- Constructor promotion for data-carrying classes:
class User
{
public function __construct(
public readonly string $name,
private string $email,
protected int $age = 0,
) {}
}- Readonly properties (8.1+) for immutable state. Must have a type declaration.
- Readonly classes (8.2+) — all properties implicitly readonly, no dynamic properties.
- Asymmetric visibility (8.4+) —
public protected(set)for publicly readable,
internally writable properties.
- Property hooks (8.4+) —
get/setlogic on properties. Use instead of trivial
getter/setter methods. Incompatible with readonly.
Inheritance and Composition
- Composition over inheritance. Use inheritance only for true "is-a" relationships.
- Interfaces for contracts. All interface methods are public. As of 8.4, interfaces
can declare property requirements.
- Abstract classes when you need shared implementation alongside a contract.
- Traits for horizontal reuse. Never use traits as a substitute for interfaces.
One use statement per trait, each on its own line.
- `#[Override]` (8.3+) on every method that overrides a parent or implements an
interface method. Catches signature drift at compile time.
- `super()` equivalent: always use
parent::method(). Never hardcode grandparent
class names.
Magic Methods
- Avoid property overloading (
__get,__set) in new code. Typed properties with
hooks are strictly better.
- `__toString()` — define when string conversion has meaningful semantics.
- `__invoke()` — for single-method objects that act as callables.
- `__serialize()` / `__unserialize()` — prefer over
__sleep()/__wakeup().
Object Patterns
- Value objects —
readonly classwith constructor promotion. Immutable by default. - DTOs — readonly classes with public properties. No behavior.
- Service classes — constructor injection for dependencies, no public state.
- Lazy objects (8.4+) — defer initialization via
ReflectionClass::newLazyGhost().
Functions
- Early return. Guard clauses first, happy path flat. Reduce nesting.
- One function, one job. If the name contains "and", split it.
- Type all parameters and return types on public functions.
- Named arguments for functions with boolean flags or many optional parameters:
createUser(name: 'John', admin: true).
- `match` over `switch` —
matchis an expression, uses strict comparison, and
does not fall through.
- Pipe operator (8.5+) for functional chaining:
$result = $input
|> trim(...)
|> strtolower(...)
|> ucfirst(...);- First-class callables with
...syntax:array_map(strlen(...), $strings). - Arrow functions for short closures:
fn($x) => $x * 2. Arrow functions capture
by value, not by reference.
- Closures for multi-statement callbacks. Use
useto capture outer variables.
Prefer static function / static fn when $this is not needed.
- Closures in constants (8.5+) — static closures and first-class callables are
valid in constant expressions, default values, and attributes.
Error Handling
- Be specific. Catch the narrowest exception type:
catch (InvalidArgumentException)
not catch (Exception).
- Never bare `catch (\Throwable)` at arbitrary depths. Use at application boundaries
only (controllers, CLI entry points, queue workers).
- `throw` is an expression (8.0+):
$value ?? throw new InvalidArgumentException(). - Chain exceptions.
throw new AppException('context', previous: $e)preserves the
original cause.
- Custom exception hierarchy:
class AppException extends \RuntimeException {}
class NotFoundException extends AppException {}
class ValidationException extends AppException {}- Prefer `\RuntimeException` subtree for application errors.
\LogicExceptionsubtree
for programming errors (wrong arguments, unimplemented methods).
- Error strings: lowercase, no trailing punctuation. They compose in chains:
"user not found: invalid ID format".
- `#[Deprecated]` attribute (8.4+) on functions, methods, and constants to emit
E_USER_DEPRECATED when called.
- `#[NoDiscard]` attribute (8.5+) on functions whose return value must be consumed.
Use (void) cast to intentionally suppress.
- `finally` for unconditional cleanup. But prefer RAII-style patterns (destructors,
resource wrappers) when possible.
Strings
- Double-quoted interpolation for simple variables:
"Hello, {$name}". - `sprintf()` for complex formatting:
sprintf('Item %d: %s', $id, $name). - Heredoc for multiline strings. Nowdoc (
<<<'EOT') when no interpolation needed. - `str_contains()`, `str_starts_with()`, `str_ends_with()` (8.0+) — never
strpos
with === false for substring checks.
- `mb_trim()`, `mb_ltrim()`, `mb_rtrim()` (8.4+) for multibyte-safe trimming.
- `mb_ucfirst()`, `mb_lcfirst()` (8.4+) for multibyte-safe case conversion.
- `"".join()` equivalent:
implode(', ', $parts)for building strings from arrays.
Arrays
- Short syntax:
$arr = [1, 2, 3]. Neverarray(). - `array_map()`, `array_filter()`, `array_reduce()` for functional transforms.
- `array_find()`, `array_find_key()` (8.4+) — find first element matching a callback.
- `array_any()`, `array_all()` (8.4+) — existence/universal checks.
- `array_first()`, `array_last()` (8.5+) — get first/last value without resetting
the internal pointer.
- Spread operator for merging:
$merged = [...$defaults, ...$overrides]. - Trailing commas on multi-line arrays (last element gets a comma).
- Destructuring:
[$first, $second] = $arrayor['key' => $value] = $assoc.
Match Expression
$result = match ($status) {
Status::Active => 'active',
Status::Inactive, Status::Suspended => 'inactive',
default => throw new \UnexpectedValueException("Unknown status: {$status->value}"),
};- `match` is an expression — it returns a value. Use instead of
switch. - Strict comparison (
===) — no type coercion. - No fallthrough — each arm is isolated.
- Multiple conditions per arm with commas.
- Exhaustiveness — always include
defaultunless provably exhaustive. Unmatched
value throws UnhandledMatchError.
Closures and Callables
- First-class callable syntax (8.1+):
strlen(...),$obj->method(...),
ClassName::method(...).
- Arrow functions (
fn) for single-expression closures — auto-captures by value. - Static closures (
static fn,static function) when$thisis not needed —
saves memory, prevents accidental binding.
- `Closure::bind()` and
Closure::fromCallable()for advanced callable manipulation. - Type hint callables as `Closure` in property types (not
callable).
Packaging and Toolchain
Composer
- `composer.json` is the single source of truth for project metadata, dependencies,
autoloading, and scripts.
- Caret `^` constraints for dependencies:
"vendor/package": "^2.0". - Lock file: commit for applications, skip for libraries.
- PSR-4 autoloading: map namespace prefixes to directories.
- Separate `autoload-dev` for test namespaces.
Project Structure
my-project/
├── composer.json
├── composer.lock
├── src/
│ └── ... (PSR-4: App\)
├── tests/
│ ├── Unit/
│ ├── Integration/
│ └── bootstrap.php
├── config/
├── public/
│ └── index.php
└── var/
├── cache/
└── log/- `src/` — application code, one class per file, PSR-4 mapped.
- `tests/` — mirrors
src/structure withUnit/andIntegration/separation. - `public/` — web root, single entry point (
index.php). - `var/` — generated files (cache, logs). Git-ignored.
- `vendor/` — Composer dependencies. Git-ignored.
File Header
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\User;
use App\Repository\UserRepository;
use Psr\Log\LoggerInterface;Order: <?php tag, blank line, declare(strict_types=1), blank line, namespace, blank line, use imports (classes, then functions, then constants), blank line, code. No leading backslash on imports.
See ${CLAUDE_SKILL_DIR}/references/packaging.md for composer.json templates and PSR-4 mapping.
Formatting (PER-CS)
PER Coding Style is the baseline. These are conventions, not tool configuration.
- 4-space indentation. No tabs.
- Opening braces on their own line for classes, interfaces, traits, enums, methods.
- Opening braces on the same line for control structures (
if,for,while, etc.). - One statement per line. No multi-statement lines.
- Soft line limit: 120 characters. Prefer 80 for readability.
- Trailing commas on multi-line argument lists, arrays,
matcharms,uselists. - No trailing commas on single-line constructs.
- Visibility on everything — properties, methods, constants.
- Modifier order:
abstract/final, visibility,static,readonly. - `new Foo()` — always use parentheses when instantiating (even without arguments),
unless immediately chaining: new Foo()->method().
- Compound types: no spaces around
|and&. Parentheses for DNF without internal
spaces.
- Empty exception classes on one line:
class NotFoundException extends AppException {}
Application
When writing PHP code: apply all conventions silently — don't narrate each rule. If an existing codebase contradicts a convention, follow the codebase and flag the divergence once.
When reviewing PHP code: cite the specific violation and show the fix inline. Don't lecture — state what's wrong and how to fix it.
Bad: "According to PHP best practices, you should use strict_types
declaration at the top of every file..."
Good: "Missing declare(strict_types=1)."Code Navigation — LSP Required
An Intelephense LSP server is configured for .php and .phtml files. Always use LSP tools for code navigation instead of Grep or Glob. LSP understands PHP's namespace system, type inference, scope rules, and Composer autoload boundaries — text search does not.
Tool Routing
| Task | LSP Operation | Why LSP over text search |
|---|---|---|
| Find where a function/class is defined | goToDefinition | Resolves use statements, aliases, namespace paths |
| Find all usages of a symbol | findReferences | Scope-aware, no false positives from string matches |
| Get type signature or docs | hover | Instant type info without reading source files |
| List all symbols in a file | documentSymbol | Structured output — classes, methods, constants |
| Find a symbol by name across project | workspaceSymbol | Searches all namespaces and Composer dependencies |
| Find concrete classes implementing an interface | goToImplementation | Knows the type hierarchy |
| Find what calls a function | incomingCalls | Precise call graph across namespace boundaries |
| Find what a function calls | outgoingCalls | Structured dependency map |
Grep/Glob remain appropriate for: text in comments, string literals, log messages, TODO markers, config values, env vars, file name patterns, URLs, error message text — anything that isn't a PHP identifier.
When spawning subagents for PHP codebase exploration, instruct them to use LSP tools. Subagents have access to the same LSP server.
Integration
The coding skill governs workflow (discovery, planning, verification); this skill governs PHP implementation choices. The phpunit skill governs testing conventions — both are active simultaneously when writing PHP tests.
Strict types everywhere. Types on everything. If PHP can check it at compile time, make it do so.
{
"sources": {
"PHP Type Declarations": "https://www.php.net/manual/en/language.types.declarations.php",
"PHP Union Types": "https://www.php.net/manual/en/language.types.type-system.php",
"PHP Enumerations": "https://www.php.net/manual/en/language.enumerations.php",
"PHP Fibers": "https://www.php.net/manual/en/language.fibers.php",
"PHP OOP Basics": "https://www.php.net/manual/en/language.oop5.php",
"PHP Interfaces": "https://www.php.net/manual/en/language.oop5.interfaces.php",
"PHP Traits": "https://www.php.net/manual/en/language.oop5.traits.php",
"PHP Readonly Properties": "https://www.php.net/manual/en/language.oop5.properties.php",
"PHP Error Handling": "https://www.php.net/manual/en/language.exceptions.php",
"PHP 8.5 Migration": "https://www.php.net/manual/en/migration85.new-features.php",
"PHP 8.4 Migration": "https://www.php.net/manual/en/migration84.new-features.php",
"PHP 8.3 Migration": "https://www.php.net/manual/en/migration83.new-features.php",
"PSR-1 Basic Coding": "https://www.php-fig.org/psr/psr-1/",
"PSR-4 Autoloading": "https://www.php-fig.org/psr/psr-4/",
"PSR-12 Extended Coding Style": "https://www.php-fig.org/psr/psr-12/",
"PER Coding Style 2.0": "https://www.php-fig.org/per/coding-style/",
"Composer Documentation": "https://getcomposer.org/doc/04-schema.md"
},
"lastFetched": "2026-03-02T11:53:57.420Z"
}
PHP Concurrency
PHP is fundamentally single-threaded. Concurrency primitives provide cooperative multitasking within a single request, not parallelism.
Fibers (8.1+)
Full-stack, interruptible functions. Unlike generators, fibers can suspend from anywhere in the call stack — not just at yield points.
Core API
$fiber = new Fiber(function (): void {
$value = Fiber::suspend('paused');
echo "Resumed with: {$value}\n";
});
$result = $fiber->start(); // "paused"
$fiber->resume('continue'); // "Resumed with: continue"Fiber class methods:
new Fiber(callable $callback)— create fiber$fiber->start(mixed ...$args): mixed— start execution, returns first suspend value$fiber->resume(mixed $value = null): mixed— resume suspended fiber$fiber->throw(Throwable $exception): mixed— throw into suspended fiberFiber::suspend(mixed $value = null): mixed— suspend current fiber (static)$fiber->isStarted(),$fiber->isSuspended(),$fiber->isRunning(),
$fiber->isTerminated() — state checks
$fiber->getReturn(): mixed— get return value after termination
Exceptions:
FiberError— invalid operation (e.g., resuming a running fiber)- Fibers can be suspended inside
array_map(),foreachon iterators, and other
callbacks (as of 8.4, also during object destructors)
When to Use Fibers
- I/O-bound concurrency — suspend while waiting for network, filesystem, database
- Building async libraries — event loops (ReactPHP, Revolt, AMPHP)
- Batch processing — interleave multiple long-running operations
When NOT to Use Fibers
- CPU-bound work — no parallelism, only one fiber runs at a time
- Simple sequential code — fibers add complexity with no benefit
- Direct application code — prefer async libraries built on fibers over raw Fiber API
Practical Pattern: Event Loop
$scheduler = [];
function async(Fiber $fiber): void
{
global $scheduler;
$scheduler[] = $fiber;
}
function run(): void
{
global $scheduler;
while ($scheduler) {
$fiber = array_shift($scheduler);
if (!$fiber->isStarted()) {
$fiber->start();
} elseif ($fiber->isSuspended()) {
$fiber->resume();
}
if (!$fiber->isTerminated()) {
$scheduler[] = $fiber;
}
}
}Generators as Coroutines
Generators provide lightweight cooperative multitasking via yield, but are stack-less — suspension only at yield points, not in nested calls.
function fibonacci(): Generator
{
[$a, $b] = [0, 1];
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
// Bidirectional communication
function accumulator(): Generator
{
$total = 0;
while (true) {
$value = yield $total;
$total += $value;
}
}
$acc = accumulator();
$acc->current(); // 0
$acc->send(10); // 10
$acc->send(20); // 30Generator methods:
current(),key(),next(),rewind()— Iterator interfacesend(mixed $value)— send value into generator, resume executionthrow(Throwable)— throw exception at current yieldgetReturn()— get return value after generator completesyield from $iterable— delegate to sub-generator/iterable
Fibers vs Generators
| Aspect | Generators | Fibers |
|---|---|---|
| Suspension point | Only at yield | Anywhere in call stack |
| Return type change | Must return Generator | No signature change |
| Bidirectional data | Via send()/yield | Via suspend()/resume() |
| Use case | Lazy sequences, simple coroutines | Async I/O, event loops |
| Overhead | Very low | Low (own call stack) |
Async Libraries
For real async PHP, use libraries built on Fibers:
- Revolt — event loop standard for PHP
- AMPHP — async framework using Revolt
- ReactPHP — event-driven non-blocking I/O
These libraries handle the scheduling complexity so application code uses async/await style patterns without managing fibers directly.
PHP OOP Patterns
Interfaces
All interface methods must be public. Interfaces share a namespace with classes, traits, and enums.
- Interfaces can extend multiple interfaces:
interface C extends A, B {} - As of 8.4, interfaces can declare property requirements with
get/sethooks - Interface constants exist and are overridable since 8.1
- Avoid constructors in interfaces — they reduce flexibility and are not enforced by
inheritance rules
interface Renderable
{
public function render(): string;
}
// PHP 8.4+ interface property
interface HasName
{
public string $name { get; }
}Abstract Classes
Use abstract classes when you need shared implementation alongside interface contracts.
abstract class Repository
{
abstract protected function findById(int $id): ?Entity;
public function findOrFail(int $id): Entity
{
return $this->findById($id)
?? throw new NotFoundException("Entity {$id} not found");
}
}Traits
Traits provide horizontal code reuse. They cannot be instantiated directly.
- Each trait
usestatement goes on its own line - Conflict resolution:
A::method insteadof BandB::method as aliasedMethod - The
finalmodifier can be applied when using a trait method (8.3+) - Traits can declare abstract methods, constants, and properties
Prefer interfaces over traits for defining contracts. Use traits for implementation sharing that does not fit a class hierarchy.
Readonly Properties (8.1+)
class Money
{
public function __construct(
public readonly float $amount,
public readonly string $currency,
) {}
}- Must have a type declaration (
mixedif truly unconstrained) - No default values allowed (use constants for that)
- Can only be initialized once, from the declaring scope
- Static readonly properties are not supported
- As of 8.3, readonly properties can be reinitialized during
__clone() - As of 8.4, readonly properties are implicitly
protected(set)(wasprivate(set))
Readonly Classes (8.2+)
All declared properties of a readonly class are implicitly readonly. Dynamic properties are forbidden.
readonly class Point
{
public function __construct(
public float $x,
public float $y,
) {}
}Property Hooks (8.4+)
Attach get and set logic directly to properties, replacing manual getter/setter boilerplate.
class Temperature
{
public float $celsius {
get => ($this->fahrenheit - 32) * 5 / 9;
set => $this->fahrenheit = $value * 9 / 5 + 32;
}
public function __construct(
private float $fahrenheit,
) {}
}- Virtual properties have hooks but no backing storage
- Backed properties have hooks plus a backing value (
$this->propertyName) - Property hooks are incompatible with
readonly— use asymmetric visibility instead - Set hooks receive value via
$valueparameter - Interface properties can require
get,set, or both
Asymmetric Visibility (8.4+)
Separate read and write visibility for properties:
class User
{
public protected(set) string $name;
public private(set) int $loginCount = 0;
public function __construct(string $name)
{
$this->name = $name;
}
public function incrementLoginCount(): void
{
$this->loginCount++;
}
}- Get-visibility must not be narrower than set-visibility
- Combines with property hooks
- Options:
private(set),protected(set),public(set) - Static properties support asymmetric visibility as of 8.5
Lazy Objects (8.4+)
Defer object initialization until first property/method access:
$reflector = new ReflectionClass(HeavyService::class);
$proxy = $reflector->newLazyGhost(function (HeavyService $ghost): void {
$ghost->__construct(expensiveDataFetch());
});
// $proxy is now a HeavyService — initialization runs on first accessTwo strategies: newLazyGhost() (initializer fills the object in place) and newLazyProxy() (initializer returns a fully initialized instance).
Constructor Promotion
class User
{
public function __construct(
public readonly string $name,
private string $email,
protected int $age = 0,
) {}
}- Visibility modifier on constructor parameter promotes it to a property
- Works with
readonly, typed properties, default values - Trailing comma after last promoted parameter on multi-line constructors
Clone
PHP 8.5 makes clone a function and supports property reassignment during cloning:
$updated = clone($original, ['name' => 'New Name']);This is especially useful for readonly/value objects where properties cannot normally be reassigned after initialization.
Enumerations (8.1+)
Enums are special classes with a fixed set of possible values.
Pure Enums
enum Suit
{
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}Backed Enums
enum Status: string
{
case Active = 'active';
case Inactive = 'inactive';
case Suspended = 'suspended';
}
// Instantiate from value
$status = Status::from('active'); // Status::Active
$status = Status::tryFrom('unknown'); // null- Backed by
stringorintonly from()throwsValueErroron invalid value;tryFrom()returnsnull- Enums can implement interfaces, use traits, define methods and constants
- Enums cannot be instantiated with
new, extended, or have state (properties) - Dynamic member access:
Status::{$name}(8.3+) cases()returns array of all cases- Enum constants can have attributes (8.5+)
Magic Methods
__construct()/__destruct()— lifecycle__get()/__set()/__isset()/__unset()— property overloading__call()/__callStatic()— method overloading__toString()— string conversion__invoke()— calling object as function__clone()— clone customization__serialize()/__unserialize()— custom serialization (prefer over__sleep/__wakeup)__debugInfo()— customizevar_dump()output
The #[Override] Attribute (8.3+)
Marks a method (or property in 8.5+) as intentionally overriding a parent/interface member. Compilation fails if no parent declaration exists.
class Child extends Parent
{
#[\Override]
public function process(): void
{
// ...
}
}PHP Packaging and Project Structure
Composer
Composer is the standard dependency manager for PHP. The composer.json file is the single source of truth for project metadata, dependencies, and autoloading.
composer.json Essentials
{
"name": "vendor/package",
"description": "Package description",
"type": "project",
"license": "MIT",
"require": {
"php": ">=8.5",
"vendor/dependency": "^2.0"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
}
}Key Fields
- name —
vendor/packageformat, lowercase, hyphens for word separation - type —
library(default),project,metapackage,composer-plugin - require — production dependencies with version constraints
- require-dev — development-only dependencies (testing, linting, analysis)
- autoload — PSR-4 namespace-to-directory mappings
- autoload-dev — development-only autoload mappings
- scripts — custom commands (
"test": "phpunit","lint": "php-cs-fixer fix") - config —
sort-packages: true,optimize-autoloader: true
Version Constraints
^2.0— compatible with 2.x (>=2.0, <3.0) — preferred for most dependencies~2.1— next significant release (>=2.1, <2.2 for patch, or >=2.1, <3.0 for minor)>=2.0 <3.0— explicit range2.0.*— wildcarddev-main— branch reference (use only in root package)
Prefer ^ (caret) for dependencies — it allows minor and patch updates while preventing breaking changes.
Commands
composer install— install from lock file (CI, production)composer update— update dependencies, regenerate lock file (development)composer require vendor/package— add dependencycomposer require --dev vendor/package— add dev dependencycomposer dump-autoload -o— regenerate optimized autoloadercomposer run-script test— run defined script
Lock File
composer.lockMUST be committed for applications/projectscomposer.lockSHOULD NOT be committed for librariescomposer installreads the lock file for reproducible buildscomposer updateregenerates the lock file
PSR-4 Autoloading
The standard autoloading mechanism. Maps namespace prefixes to base directories.
Fully Qualified Class Name: App\Http\Controller\UserController
Namespace Prefix: App\
Base Directory: src/
Resulting File Path: src/Http/Controller/UserController.phpRules:
- Namespace separators map to directory separators
- Filename must match class name exactly (case-sensitive)
- One class per file
- File extension is
.php
Project Layout
Application
my-project/
├── composer.json
├── composer.lock
├── src/
│ ├── Controller/
│ ├── Service/
│ ├── Repository/
│ ├── Entity/
│ └── Exception/
├── tests/
│ ├── Unit/
│ ├── Integration/
│ └── bootstrap.php
├── config/
├── public/
│ └── index.php
├── var/
│ ├── cache/
│ └── log/
└── vendor/Library
my-library/
├── composer.json
├── src/
│ └── MyLibrary/
│ ├── Client.php
│ ├── Exception/
│ └── ...
├── tests/
│ └── ...
└── vendor/File Header Order (PER-CS)
Every PHP file follows this structure:
1. Opening <?php tag (own line) 2. File-level docblock (optional) 3. declare(strict_types=1); 4. Namespace declaration 5. Class-based use imports 6. Function-based use imports 7. Constant-based use imports 8. Code
Each block separated by a single blank line. Import statements must be fully qualified (no leading backslash).
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\User;
use App\Repository\UserRepository;
use Psr\Log\LoggerInterface;
class UserService
{
// ...
}Namespace Conventions
- Vendor namespace as top level:
Vendor\Package\... - One class per file, class name matches filename
- Directory structure mirrors namespace structure
- Use compound
usestatements sparingly — max 2 sub-namespace levels in groups - Never
usewith a leading backslash — imports are always fully qualified
Scripts and Automation
Define common tasks in composer.json scripts:
{
"scripts": {
"test": "phpunit",
"test:coverage": "phpunit --coverage-html var/coverage",
"lint": "php-cs-fixer fix --dry-run --diff",
"lint:fix": "php-cs-fixer fix",
"analyse": "phpstan analyse"
}
}Run with composer run-script test or shorthand composer test.
PHP Type System
PHP uses a nominal type system with behavioral subtyping checked at compile time and type verification at runtime. PHP 8.5+ provides a rich type declaration system covering parameters, return types, properties, and class constants.
Strict Typing
By default, PHP coerces values to the expected scalar type. Enable strict mode per-file with declare(strict_types=1) at the top of the file. In strict mode, only exact type matches are accepted (exception: int passes float checks).
Strict typing applies to calls made from within the declaring file — the caller's strict_types setting governs coercion behavior, not the callee's.
<?php
declare(strict_types=1);
function add(int $a, int $b): int
{
return $a + $b;
}
add(1, 2); // OK
add(1.5, 2.5); // TypeErrorScalar Types
Use short forms: bool, int, float, string. Long forms (boolean, integer, double) are treated as class names and will cause errors.
Union Types (8.0+)
Multiple types joined with |: int|string, Foo|Bar|null.
nullmust be last in a union:string|int|null- Cannot combine
trueandfalse— usebool - Cannot combine
objectwith class types - Cannot combine
iterablewitharrayorTraversable
Intersection Types (8.1+)
Class types joined with &: Countable&Traversable. Only class/interface types allowed (no scalars, no self/parent/static).
DNF Types (8.2+)
Disjunctive Normal Form — union of intersections in parentheses:
function process(array|(ArrayAccess&Traversable) $input): void {}Each intersection group must be parenthesized. The overall structure is ORed unions of ANDed intersections.
Nullable Types
?Tis syntactic sugar forT|null- Prefer
?Tfor single-type-plus-null; use full union syntax for multi-type nullables:
int|string|null
Standalone Types
null— standalone since 8.2true,false— standalone singleton types since 8.2void— return-only, function returns nothingnever— return-only (8.1+), function never returns (throws or exits)mixed— equivalent toobject|resource|array|string|float|int|bool|nullstatic— return-only, returns instance of the called class (8.0+)self,parent— relative class types
Typed Properties (7.4+)
All property types except callable are supported. Typed properties must be initialized before access or an Error is thrown.
class User
{
public int $id;
public ?string $name;
public string $email = '';
}Typed Class Constants (8.3+)
class Config
{
public const int MAX_RETRIES = 3;
public const string DEFAULT_LOCALE = 'en';
}
interface HasVersion
{
public const string VERSION = '1.0';
}Variance Rules
- Parameter types are contravariant — a child class can widen (accept more general types)
- Return types are covariant — a child class can narrow (return more specific types)
mixedreturn type can be narrowed to any type in a subclass
Type Juggling Pitfalls
0 == ""isfalseas of PHP 8.0 (wastruebefore)0 == "foo"isfalseas of PHP 8.0- String-to-number comparison uses numeric comparison only if both are numeric strings
- Use
===and!==for strict comparison (no coercion) is_numeric()returnstruefor numeric strings including hex and scientific notation
callable Type
- Valid as parameter type, NOT as property type
- Cannot specify the callable's signature in the type declaration
- For strict callable typing, use
Closuretype or interface with__invoke()