PHP Guidelines
Overview
Modern PHP guidelines based on the PHP Manual covering PHP 8.x idioms. PHP has evolved dramatically — write modern, type-safe PHP, not legacy PHP 5 patterns.
Core Principles
- Always use
declare(strict_types=1) — first line of every file
- Type everything — parameters, returns, properties, constants
- Use
=== not == — loose comparison causes security bugs
- Errors are exceptions — catch them, don't suppress with
@
- Composition over inheritance — interfaces + traits over deep hierarchies
Strict Types
<?php
declare(strict_types=1);
function add(int $a, int $b): int {
return $a + $b;
}
add("1", "2");
| Mode |
Behavior |
| Coercive (default) |
"123" silently becomes 123, truncates floats |
Strict (strict_types=1) |
TypeError on any mismatch (except int to float) |
| Scope |
Per-file — each file must declare independently |
Type System
Scalar Types
| Type |
Falsy values |
Gotchas |
bool |
false |
-1 is truthy; "0" is falsy but "false" is truthy |
int |
0 |
Overflow silently becomes float; use PHP_INT_MAX |
float |
0.0, -0.0 |
Never compare for equality — use epsilon; NAN != NAN |
string |
"", "0" |
No native Unicode — use mbstring; "0" is falsy |
null |
null |
isset() returns false for null; use ?Type or Type|null |
Type Declarations
function handle(int|string $value): string|false { }
function process(Countable&ArrayAccess $data): void { }
function route((Logger&Handler)|NullHandler $h): void { }
function get(?int $id): ?string { }
function loop(): never { while(true) {} }
function fire(): void { }
| Type |
Use for |
mixed |
Accepts anything (avoid — be specific) |
void |
Function returns nothing visible |
never |
Function never returns (exit, throw, infinite loop) |
iterable |
array|Traversable |
callable |
Parameters/returns only — cannot type properties |
self, static, parent |
Class context references |
Type Juggling Pitfalls
0 == "a"
"0" == false
"" == null
"0" == null
"123" == "123.0"
0 === "a"
"0" === false
0.1 + 0.7 == 0.8
floor((0.1 + 0.7) * 10)
OOP
Rules Summary
| Rule |
Detail |
| Always declare property types |
Untyped properties are error-prone |
Use readonly for immutable data |
Can only be set once (PHP 8.1+) |
| Constructor promotion |
Reduces boilerplate — use for simple DTOs |
private(set) |
Read public, write private (PHP 8.4+) |
| Dynamic properties deprecated |
PHP 8.2+ — declare all properties explicitly |
| Reading uninitialized typed property |
Throws Error |
| Interface methods must be public |
All of them |
| Small interfaces |
1-3 methods — compose larger ones |
| Abstract for shared behavior |
Interfaces for contracts |
final prevents extension |
Use when inheritance isn't intended |
final class constants (8.1+) |
Prevent override in children |
| Use enums over class constants |
Type-safe, exhaustive matching (PHP 8.1+) |
from() throws on invalid |
tryFrom() returns null |
| Enums can have methods and interfaces |
But no state (properties) |
Detailed OOP code examples (classes, interfaces, traits, enums, visibility, magic methods): see resources/oop-patterns.md
Dependency Injection & SOLID
| Principle |
Rule |
| S — Single Responsibility |
One class = one reason to change |
| O — Open/Closed |
Open for extension, closed for modification — use interfaces |
| L — Liskov Substitution |
Subtypes must be substitutable for their base types |
| I — Interface Segregation |
Many small interfaces > one large interface |
| D — Dependency Inversion |
Depend on abstractions (interfaces), not concretions |
| Rule |
Detail |
| Constructor injection |
Preferred — makes dependencies explicit |
| Type-hint interfaces |
Not concrete classes — enables swapping |
| DI container ≠ DI |
Containers are optional convenience; DI is the pattern |
| Avoid Service Locator |
Hiding dependencies inside a container = anti-pattern |
final by default |
Mark classes final unless designed for extension |
DI code examples and namespace patterns: see resources/oop-patterns.md
PSR Standards & Composer
PSR Standards
| Standard |
Purpose |
| PSR-1 |
Basic coding standard — <?php tag, UTF-8, class naming |
| PSR-4 |
Autoloading — namespace maps to directory structure |
| PSR-12 / PER |
Extended coding style — indentation, braces, spacing |
| PSR-3 |
Logger interface (Psr\Log\LoggerInterface) |
| PSR-7 |
HTTP message interfaces (request/response) |
| PSR-11 |
Container interface (Psr\Container\ContainerInterface) |
| PSR-15 |
HTTP handlers and middleware |
Composer
composer init
composer require monolog/monolog
composer install --no-dev --optimize-autoloader
composer update
require 'vendor/autoload.php';
| Rule |
Detail |
Commit composer.lock |
Ensures identical versions across team/environments |
composer install in production |
Never composer update — use lock file |
--no-dev in production |
Exclude dev dependencies |
--optimize-autoloader / -o |
Converts PSR-4/PSR-0 to classmap for speed |
| PSR-4 autoloading |
Namespace App\ -> directory src/ |
composer dump-autoload -o |
Regenerate optimized autoload after changes |
| Security auditing |
composer audit checks for known vulnerabilities |
Modern PHP 8.x Patterns
Key features by version:
| Version |
Key Features |
| 8.0 |
Match, named args, union types, constructor promotion, nullsafe ?->, attributes |
| 8.1 |
Enums, readonly, fibers, intersection types, never, first-class callables |
| 8.2 |
Readonly classes, DNF types, true/false/null types, trait constants |
| 8.3 |
Typed class constants, #[Override], json_validate() |
| 8.4 |
Property hooks, asymmetric visibility, #[Deprecated], lazy objects |
| 8.5 |
Pipe operator |>, #[NoDiscard], (void) cast |
Detailed code examples for all features, functions, generators, fibers, and attributes: see resources/modern-php.md
Error Handling
try {
$result = riskyOperation();
} catch (NotFoundException $e) {
return defaultValue();
} catch (ValidationException | AuthException $e) {
log($e->getMessage());
throw $e;
} finally {
cleanup();
}
| Rule |
Detail |
Catch \Throwable for everything |
Exception + Error |
| Use union catches |
catch (TypeA | TypeB $e) (PHP 8.0+) |
| Catch without variable |
catch (SpecificException) (PHP 8.0+) |
Call parent::__construct() |
In custom exception classes |
| Log exceptions, don't display |
Never show stack traces to users |
throw is an expression (8.0+) |
$x ?? throw new Exception() |
Never use @ suppression |
Hides real problems; custom handlers still fire |
PHP 7+ throws Error |
Not Exception — use \Throwable |
Error Configuration
| Setting |
Development |
Production |
error_reporting |
E_ALL |
E_ALL |
display_errors |
On |
Off |
log_errors |
On |
On |
error_log |
stderr |
syslog or file |
Arrays & Strings
Arrays
$map = ['key' => 'value', 'other' => 42];
$list = [1, 2, 3];
['name' => $name, 'age' => $age] = $userData;
[$first, , $third] = $list;
$merged = [...$array1, ...$array2];
| Rule |
Detail |
| Null coalescing |
$map['key'] ?? $default |
Keys are int|string only |
Objects/arrays cannot be keys |
| Iteration order preserved |
Arrays are ordered maps |
| Empty array is falsy |
if ($arr) checks non-empty |
Strings
$msg = "Hello {$user->name}, you have {$count} items";
$html = <<<HTML
<div class="card">
<h1>{$title}</h1>
</div>
HTML;
$sql = <<<'SQL'
SELECT * FROM users WHERE id = :id
SQL;
| Rule |
Detail |
Use {$var} in double-quoted |
Not ${var} (deprecated PHP 8.2) |
| Single-byte encoding |
Use mb_strlen(), mb_substr() for Unicode |
=== for string comparison |
== does numeric coercion on numeric strings |
| Negative offset (7.1+) |
$str[-1] for last character |
JSON
$json = json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
if (json_validate($json)) { }
$data = json_decode($json, true, 512, JSON_BIGINT_AS_STRING);
| Flag |
Effect |
JSON_THROW_ON_ERROR |
Throw JsonException instead of returning false/null |
JSON_UNESCAPED_UNICODE |
Don't escape UTF-8 chars — smaller output |
JSON_UNESCAPED_SLASHES |
Don't escape / — cleaner URLs |
JSON_PRESERVE_ZERO_FRACTION |
Keep 10.0 instead of 10 |
JSON_BIGINT_AS_STRING |
Decode large ints as strings (prevent precision loss) |
JSON_NUMERIC_CHECK |
Convert numeric strings to numbers — use cautiously (phone numbers!) |
JSON_INVALID_UTF8_SUBSTITUTE |
Replace broken UTF-8 with U+FFFD (PHP 7.2+) |
JSON_PRETTY_PRINT |
Human-readable output — dev/debug only |
| Rule |
Detail |
Always JSON_THROW_ON_ERROR |
Never check json_last_error() manually |
| Input must be UTF-8 |
mb_convert_encoding() first if unsure |
json_validate() (8.3+) |
Faster than decode when you just need validity |
| Assoc arrays over objects |
json_decode($json, true) — faster property access |
Testing
| Rule |
Detail |
assertSame() over assertEquals() |
Strict comparison (type + value) |
| Data providers for table-driven tests |
#[DataProvider('method')] attribute |
expectException() before the call |
Not after |
| Test naming |
*Test.php, method test* |
| PHPStan |
Levels 0-9 strictness; start low, increase per sprint |
| Psalm |
Adds taint analysis for security |
| CI gate |
Fail build on any new error — never ignore regressions |
Full testing examples, static analysis setup, and version migration reference: see resources/testing-migration.md
Build & Deploy
Always Use Makefile
Before running composer install or any build command, check if a Makefile exists. If it does, use it.
| Situation |
Action |
| Makefile exists with relevant target |
make deploy, make build, make test |
| Makefile exists, no matching target |
List targets, pick closest |
| No Makefile |
composer install, php artisan, etc. |
Permissions & Ownership
stat -c '%U:%G' * | sort | uniq -c | sort -rn | head -5
chown -R <user>:<group> .
Temporary Files
| File type |
Location |
| Build intermediates |
/tmp — never the project directory |
| Dependencies |
vendor/ (Composer standard) |
Test File Placement
| Test type |
Location |
| Temporary |
/tmp |
| Permanent, dir exists |
tests/ (follow existing structure) |
| Permanent, no dir |
Create tests/ at project root |
Anti-Pattern Quick Reference
| Anti-Pattern |
Better Alternative |
No strict_types |
declare(strict_types=1) in every file |
== comparison |
=== everywhere |
@ error suppression |
Try-catch or proper validation |
| No type declarations |
Type params, returns, properties, constants |
catch (Exception) only |
catch (\Throwable) for Error too |
| Dynamic properties |
Declare explicitly (#[AllowDynamicProperties] if must) |
| Class constants for finite sets |
enum (PHP 8.1+) |
| Deep inheritance |
Interfaces + traits composition |
__sleep() / __wakeup() |
__serialize() / __unserialize() |
${var} interpolation |
{$var} |
get_class() no arg |
$obj::class |
switch fall-through |
match expression |
| Manual null chain |
Nullsafe ?-> |
array_key_exists + access |
?? null coalescing |
| String constants |
Backed enums |
Float == |
Epsilon: abs($a - $b) < PHP_FLOAT_EPSILON |
global $var |
Dependency injection |
extract() |
Explicit assignment |
| Implicit nullable param |
Explicit ?Type |
(boolean) cast |
(bool) |
json_last_error() checking |
JSON_THROW_ON_ERROR flag |
Resources
Detailed code examples and extended references are organized in resource files:
resources/oop-patterns.md — OOP detailed code (classes, interfaces, traits, enums, visibility, magic methods), dependency injection examples, and namespace patterns
resources/modern-php.md — Modern PHP 8.x feature code (match, named args, readonly, pipe operator, deprecated/nodiscard), functions (arrow functions, closures, variadic, generators, fibers), and attributes
resources/testing-migration.md — Testing examples with PHPUnit and data providers, static analysis setup (PHPStan, Psalm), and PHP version migration reference (8.0-8.5)