Strict types, everywhere
Spec: ISO 32000-2, §7.5.5ISO 32000-2 §7.5.5PHPStan: Level 10, no src baseline
At a glance
Section titled “At a glance”NextPDF runs PHPStan at Level 10 over the engine source with no suppression baseline. This page explains why “no baseline” is a design decision rather than a tooling detail, and what that strictness actually buys a pipeline whose job is to avoid mishandling data silently.
Why this matters
Section titled “Why this matters”In most applications, strict typing is hygiene. In a PDF engine, it is closer
to a correctness mechanism. The format is unforgiving. A reader is expected
to locate content by reading the file from its end, through the trailer and
the cross-reference table, so a writer’s byte offsets must be exact. Consider
a type that quietly widens to mixed, an int that silently becomes a
string, or a nullable that is dereferenced unchecked. Any of these can
produce a file that opens fine in one viewer and fails validation in
another, weeks later, with no stack trace pointing back at the cause.
The expensive failures in this domain are the silent ones. Strict typing plus a strict analyzer is how the engine converts a class of silent runtime failures into loud build-time ones.
The short version
Section titled “The short version”- The engine source is analyzed at PHPStan Level 10 — the strictest
level — verified in
phpstan.neon.dist. - There is no source suppression baseline. The configuration locks the source analysis at zero errors. A regression fails the build rather than being absorbed into a growing ignore file.
- The few
ignoreErrorsentries that exist are narrow, identifier- and path-scoped, and individually justified in the config (cross-package soft-dependency boundaries and reflection-target test seams) — not a bulk baseline. - A separate strict profile runs
level: maxand forbids any new ignore entries, so new code is held to an even tighter standard. - The intended effect is design pressure: code that cannot be expressed type-honestly does not pass, so it gets redesigned instead of suppressed.
How NextPDF approaches it
Section titled “How NextPDF approaches it”The difference between “we use a strict analyzer” and “we use a strict analyzer with no baseline” is the whole point, so it is worth being precise.
A baseline records every existing violation and tells the analyzer to ignore exactly those. It is a pragmatic way to adopt static analysis on a legacy codebase, but it has a cost. The baseline becomes a quiet ledger of debt that the type system has agreed not to look at. New violations of the same kind can slip in next to existing ones. The analyzer’s promise weakens from “this code is type-clean” to “this code is no worse than it was.”
NextPDF does not make that trade for the engine source. The configuration
fixes the source analysis at zero errors and turns on
reportUnmatchedIgnoredErrors, so even a stale suppression — one that no
longer matches anything — fails the build. The narrow ignores that remain
are scoped to a specific error identifier and file. Each one carries an inline
explanation of why the boundary is intentional (for example, core
programming to a Pro/Enterprise interface it deliberately does not depend on
concretely). A reviewer can read each one and judge it. There is no opaque
list to lose track of.
The flow that keeps this honest:
- Change proposedNew or modified engine code.
- Level 10 analysisStrictest PHPStan level over src/, treatPhpDocTypesAsCertain on.
- Zero-error gateNo source baseline; unmatched ignores also fail.
- Strict profilelevel: max; no new ignore entries permitted.
- Redesign, not suppressIf it cannot be expressed honestly, the design changes.
treatPhpDocTypesAsCertain is part of this. PHPDoc annotations are treated
as ground truth, so a @param list<T> or @return non-empty-string is not a
comment the analyzer politely ignores. It is a checked promise. Annotation
and runtime type are forced to agree.
Practical example
Section titled “Practical example”Strict typing is most visible where a domain rule is encoded as a type
instead of a runtime check. The conformance discriminator answers
spec-level questions with exhaustive match, so an unhandled case is a
type error, not a wrong PDF:
declare(strict_types=1);
enum ConformanceMode: string{ case Plain = 'plain'; case PdfUa2 = 'pdfua2'; case PdfA4 = 'pdfa4';
/** @return 2|3|4|null */ public function pdfaPart(): ?int { return match ($this) { self::PdfA4 => 4, default => null, }; }}The @return 2|3|4|null is not documentation. Under
treatPhpDocTypesAsCertain, it is checked. A caller that assumes the result
is always an int is told so at analysis time, before a single byte of a
non-conforming PDF/A part number is ever written.
Common misconception
Section titled “Common misconception”The trap is reading “no baseline” as “the code happens to have no violations.” That is backwards. The absence of a baseline is the cause, not a lucky outcome. Because there is nowhere to park a violation, code that would produce one has to be written differently. Level 10 with no source baseline is a constraint that shapes the design, not a report card that describes it after the fact.
A second misconception: that the handful of ignoreErrors entries are a
baseline by another name. They are not. A baseline is bulk-generated and
opaque. These are individually written, identifier-scoped, explained, and
guarded by reportUnmatchedIgnoredErrors so they cannot rot unnoticed.
Limits and boundaries
Section titled “Limits and boundaries”This page is about the engine source analysis. The test suite is
analyzed under a separate, deliberately distinct scope and configuration;
“no baseline” here is a statement about src/, not a claim that every
auxiliary analysis in the repository is baseline-free. PHPStan proves type
soundness, not behavioral correctness. It does not replace the testing
pyramid, only removes a category of failure the tests would otherwise have to
chase. The exact level, flags, and ignore set are accurate as of this page’s
review date. The authoritative source is always phpstan.neon.dist and
phpstan-strict.neon.dist in the core repository.
Edition does not change this discipline. Every edition is built from the same Level 10 source:
| Edition | Availability |
|---|---|
| Core | Core source is analysed at Level 10 with no source baseline. |
| Pro | Pro is built on the same Level 10 source discipline. |
| Enterprise | Enterprise is built on the same Level 10 source discipline. |
Related docs
Section titled “Related docs”- The PHP 8.4 foundations — the language features the type system relies on.
- Errors as a feature — what happens to the failures that strict typing surfaces.
- The pipeline model — the architecture this discipline protects.
Glossary
Section titled “Glossary”- PHPStan Level 10 — the strictest analysis level, treating untyped and loosely typed values as errors rather than warnings.
- Baseline — a generated record of existing violations the analyzer is told to ignore. NextPDF uses none for the engine source.
treatPhpDocTypesAsCertain— a PHPStan setting that treats PHPDoc type annotations as checked facts, not advisory comments.reportUnmatchedIgnoredErrors— a setting that fails the build when an ignore entry no longer matches anything, preventing stale suppressions.- Design pressure — the effect of a constraint that forces code to be written a certain way, as opposed to a check that only measures it.