Guide
PHP fundamentals explained
A regional news publisher runs dozens of city sites on a shared CMS. Editors upload
articles, schedule publishes, and syndicate headlines to partner feeds — all
through PHP request handlers behind nginx. That is not a relic of the 2000s:
PHP still powers a large share of the public web, from WordPress and
Drupal to Laravel and Symfony APIs. Modern PHP 8.x adds strict types,
union and intersection types, attributes, enums, and a JIT compiler in the
Zend engine. The language that earned a reputation for
mysql_query spaghetti can now ship typed services with Composer
autoloading, PHPUnit tests, and PDO prepared statements — if you adopt the
patterns frameworks have standardized over the last decade. This guide covers the
runtime and request lifecycle, syntax and type system, OOP and namespaces, Composer,
database access, error handling, concurrency basics, a Harbor Media content API worked
example, a language decision table, common pitfalls, and a production checklist
— alongside our
Python,
Node.js, and
Java language guides.
What PHP is: Zend, FPM, and the request lifecycle
PHP is a server-side scripting language designed for HTML generation
and HTTP workloads. Source files (.php) are parsed and executed by the
Zend engine on each request (unless opcode caching is enabled).
In production, PHP-FPM (FastCGI Process Manager) runs worker pools
behind nginx or Apache; each worker handles one request at a time, then resets state.
A typical flow: nginx receives GET /articles/slug, forwards to FPM via a
Unix socket, PHP bootstraps autoloaders, runs your front controller
(public/index.php in Laravel/Symfony), dispatches to a route handler,
queries the database, renders JSON or HTML, and returns. Stateless handlers scale
horizontally by adding FPM workers and app servers behind a load balancer.
PHP 8.x features that matter
- JIT compiler — optional opcode JIT for CPU-heavy loops; most web apps benefit more from OPcache than JIT tuning.
- Named arguments —
createArticle(title: 'Ship log', status: 'draft')improves readability of wide constructors. - Match expressions — exhaustive branching without fall-through bugs of legacy
switch. - Attributes —
#[Route('/api/articles')]metadata replaces docblock annotations in modern frameworks. - Enums and readonly classes — model domain states without string constants scattered across files.
Pin your production version to a supported release (8.2 or 8.3 as of 2026). PHP 7.x is end-of-life; running it exposes unpatched security holes on any internet-facing host.
Syntax, types, and the array-centric data model
PHP variables start with $ and are loosely typed by default. Enable
strict types at the top of each file for production code:
<?php
declare(strict_types=1);
function slugify(string $title): string {
return strtolower(preg_replace('/[^a-z0-9]+/i', '-', $title));
}
Scalar types include int, float, string,
bool, and null. PHP 8 adds mixed,
union types (int|string), and
intersection types (Countable&Iterator). Return type
declarations and parameter types catch bugs at call sites instead of deep in business logic.
Arrays: lists, maps, and gotchas
PHP arrays are ordered hash maps that double as lists and dictionaries:
['id' => 42, 'title' => 'Harbor briefing']. They are copy-on-write
and passed by value unless you prefix with & for references. For typed
collections in larger codebases, prefer value objects or DTO classes over nested array
shapes that IDEs cannot validate.
The null coalescing operator (??) and
nullsafe operator (?->) reduce boilerplate when
reading optional nested fields from API payloads or ORM results.
Object-oriented PHP: classes, interfaces, and traits
Modern PHP is fully object-oriented. Classes support visibility modifiers
(public, protected, private),
abstract methods, interfaces, and
traits for horizontal reuse without deep inheritance trees.
readonly class ArticleDraft {
public function __construct(
public string $id,
public string $title,
public ArticleStatus $status,
) {}
}
enum ArticleStatus: string {
case Draft = 'draft';
case Published = 'published';
case Archived = 'archived';
}
Readonly classes (PHP 8.2+) freeze properties after construction
— ideal for immutable domain records passed between layers.
Enums replace magic strings for status fields and make
match expressions exhaustive.
Namespaces and autoloading
Namespaces (namespace Harbor\Media;) prevent class name
collisions and map to directory structure via PSR-4 autoloading. Never rely on
require_once chains in application code; Composer generates an autoloader
that loads classes on first use.
Dependency injection is standard in Laravel and Symfony: type-hint interfaces in constructors, bind implementations in a service container, and let the framework resolve graphs. Even in plain PHP, manual constructor injection keeps tests fast (swap repositories with in-memory fakes).
Composer: dependencies, scripts, and project layout
Composer is PHP’s package manager. composer.json
declares dependencies; composer.lock pins exact versions for reproducible
deploys. Run composer install --no-dev --optimize-autoloader in production
images; commit the lockfile to version control.
- PSR-4 autoload — map
Harbor\\Media\\tosrc/so class files load automatically. - Scripts — hook
post-install-cmdfor cache warmup or migration checks in CI. - Platform config — set
"platform": {"php": "8.3.0"}so local Composer resolves compatible package versions. - Security — run
composer auditin CI; subscribe to advisories for Symfony, Laravel, and Guzzle.
A sensible layout separates public/ (web root with only
index.php and assets) from src/ (application code),
config/, tests/, and var/ (cache and logs
outside the web root). Never expose vendor/ or .env files
directly to HTTP.
Database access with PDO
PDO (PHP Data Objects) is the portable database abstraction layer in the standard library. Always use prepared statements with bound parameters — never interpolate user input into SQL strings.
$stmt = $pdo->prepare(
'SELECT id, title, published_at FROM articles WHERE slug = :slug LIMIT 1'
);
$stmt->execute(['slug' => $slug]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
Configure PDO with ERRMODE_EXCEPTION so failed queries throw catchable
exceptions instead of returning false silently. For larger apps, ORMs
(Eloquent in Laravel, Doctrine in Symfony) add migrations, relationship mapping, and
query builders — trade some complexity for productivity on CRUD-heavy domains.
Connection pooling in PHP is per-request: open late, close early, or reuse a single PDO instance per worker. For read-heavy APIs, route analytics queries to a PostgreSQL replica; keep writes on the primary with explicit transaction boundaries.
Error handling, logging, and debugging
PHP distinguishes errors (engine-level issues) from
exceptions (throwable objects). In production, set
display_errors=Off and log to structured files or syslog. Use
try/catch around I/O boundaries (database, HTTP clients, filesystem) and
map low-level failures to domain exceptions with context.
try {
$article = $repository->findBySlug($slug);
} catch (PDOException $e) {
$logger->error('article lookup failed', ['slug' => $slug, 'err' => $e->getMessage()]);
throw new ArticleNotFoundException($slug, previous: $e);
}
Frameworks provide exception handlers that convert uncaught errors to JSON problem
details or HTML error pages. Register a global handler in plain PHP with
set_exception_handler and register_shutdown_function for
fatal errors. Never return stack traces to public API clients.
Local debugging uses Xdebug step debugging or Ray/spatie packages; production relies on correlation IDs in logs and APM tools (Blackfire, Tideways) for slow query traces.
HTTP, routing, and framework landscape
Raw PHP can serve APIs with $_SERVER, $_GET, and
file_get_contents('php://input'), but frameworks eliminate boilerplate:
- Laravel — batteries-included: routing, Eloquent ORM, queues, Horizon, Sanctum auth. Best for greenfield APIs and admin portals.
- Symfony — component-based; HTTP kernel, Messenger, Validator. Strong for enterprise apps needing long-term flexibility.
- WordPress — hook-driven CMS; extend with themes/plugins. Know when you are building content sites vs custom APIs.
For JSON APIs, return consistent envelopes, validate input with Symfony Validator or Laravel Form Requests, and document routes with OpenAPI. Follow our REST API design guide for status codes, pagination, and idempotency keys on write endpoints.
Compare framework depth in our Django and Flask guides when choosing between PHP and Python for a new backend.
Concurrency, queues, and background work
PHP-FPM workers are single-threaded per request. Long tasks (image resizing, PDF generation, bulk email) should not block HTTP responses. Offload to:
- Queue workers — Laravel Horizon or Symfony Messenger consumers process jobs from Redis or RabbitMQ.
- Cron — scheduled
php artisan schedule:runor systemd timers for nightly syndication pulls. - ReactPHP / Swoole / FrankenPHP — async and long-lived workers for WebSockets or high-concurrency edge cases; operational complexity rises.
Design handlers to finish within your nginx fastcgi_read_timeout. If
p95 latency exceeds two seconds, split read and write paths or cache hot article lists
in Redis with explicit TTL and cache-bust on publish events.
Worked example: Harbor Media content API
Harbor Media syndicates articles to partner sites. Editors need a JSON API to create drafts, schedule publishes, and emit webhooks when stories go live.
- Domain layer —
ArticleDraftreadonly class andArticleStatusenum;ArticleRepositoryinterface withsave,findBySlug, andlistPublishedmethods. - Persistence — PDO repository against
PostgreSQL
with a
slugunique index andpublished_attimestamptz column stored in UTC. - HTTP layer — front controller routes
POST /api/v1/articlesandGET /api/v1/articles/{slug}; validate JSON body with typed DTOs; return 201 withLocationheader on create. - Auth — Bearer token per partner, hashed in database; rate-limit by API key at nginx or middleware.
- Webhooks — on publish, enqueue a Messenger job that POSTs signed payloads to subscriber URLs with retry and dead-letter queue.
- Deploy — PHP 8.3 FPM image in
Docker, nginx
terminating TLS, OPcache enabled,
composer install --no-dev, migrations run once per release, horizontal scale behind a load balancer with shared Redis for sessions and queues.
Result: a typed, testable API that PHPUnit covers at the repository and HTTP layers, survives traffic spikes during breaking news, and keeps WordPress or static frontends decoupled from syndication logic.
Language decision table
| Need | Prefer PHP | Consider instead |
|---|---|---|
| WordPress or Drupal CMS site | Yes — native ecosystem | Headless CMS + static site for simpler scale |
| Laravel/Symfony JSON API with hiring pool | Yes — mature packages and docs | Django or Node.js if team already standardized |
| Shared hosting with only PHP available | Yes — lowest friction deploy | VPS with Docker for any language |
| CPU-heavy data science or ML | No | Python |
| Real-time WebSocket game server | Rarely — Swoole/FrankenPHP add ops cost | Node.js, Go, or dedicated game backend |
| Android or JVM enterprise microservices | No | Java / Kotlin |
| Single static binary CLI tool | No | Go or Rust |
| Existing PHP monolith with Composer and tests | Yes — incremental modernization beats rewrite | Strangler pattern to extract hot paths only when metrics justify |
Common pitfalls
- SQL injection — string-concatenated queries with user input; always bind parameters through PDO or the ORM.
- Missing strict_types — silent coercion turns
"0"into surprising truthiness bugs in payment and inventory code. - Exposing vendor/ and .env — misconfigured nginx document roots leak secrets; web root must be
public/only. - Global state — mutating
$_GLOBALSor static properties makes tests order-dependent; inject dependencies. - N+1 queries — looping ORM results and lazy-loading relations; eager-load with
with()or JOIN queries. - Opcode cache disabled — OPcache off in production wastes CPU re-parsing identical files every request.
- Running end-of-life PHP — PHP 7.x on the public internet is an open CVE buffet.
- Blocking the worker — synchronous cURL to slow third parties inside HTTP handlers; queue outbound calls.
- serialize() for untrusted data — object injection vulnerabilities; use JSON for caches and cookies.
Production checklist
- Pin PHP 8.2+ in Docker, CI, and hosting; enable OPcache with validated
opcache.validate_timestamps=0in immutable deploys. - Composer lockfile committed;
composer auditin CI on every pull request. - Web root limited to
public/; secrets in environment variables, not committed.env. - PDO
ERRMODE_EXCEPTION; database migrations versioned and run once per release. - PHPUnit (or Pest) unit tests plus HTTP feature tests for critical API routes.
- Structured JSON logs with request IDs; centralize via Loki or CloudWatch.
- Rate limiting and request size caps at nginx; validate Content-Type on JSON endpoints.
- Queue workers supervised by systemd or Kubernetes; alert on queue depth growth.
- Health check endpoint (
/healthz) that verifies database connectivity. - Load test publish and webhook paths before major news events; profile slow queries with EXPLAIN.
Key takeaways
- Modern PHP 8.x is a typed, object-oriented language with enums, attributes, and readonly classes — not the procedural scripts of PHP 4.
- Composer and PSR-4 autoloading are mandatory for maintainable projects; commit lockfiles and audit dependencies.
- PDO prepared statements and framework validation layers prevent the injection bugs that tarnished PHP’s reputation.
- PHP-FPM scales stateless HTTP handlers; offload slow work to queues instead of blocking workers.
- Choose PHP when CMS ecosystems, shared hosting, or an existing Laravel/Symfony codebase are the constraint; prefer other runtimes for ML, real-time games, or greenfield teams standardized elsewhere.
Related reading
- Django fundamentals explained — batteries-included Python web framework for comparison
- PostgreSQL fundamentals explained — PDO-backed persistence for PHP APIs and CMS extensions
- REST API design explained — routing, status codes, and pagination patterns for JSON services
- Docker fundamentals explained — PHP-FPM and nginx container images for production deploys