Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

PHP Static Analysis with PHPStan Level 9

By Kokil Thapa | Last reviewed: September 2026

Most teams stall at PHPStan level 5 because phpstan level 9 surfaces thousands of legacy violations at once. That stall costs you. Undefined array keys, nullable leaks, and untyped collections still reach production. In my experience maintaining Laravel and Symfony apps since 2010, max strictness only works with framework extensions, a baseline file, and CI rules that protect new code. This guide shows the exact config, workflow, and type-narrowing patterns that make Laravel API best practices genuinely type-safe without freezing feature work.

What makes phpstan level 9 different from lower PHPStan levels?

Level 9 is not just stricter syntax. It changes how PHPStan tracks type flow through your application. Levels 0–4 catch obvious mistakes like undefined variables. Level 9 demands proof at every branch.

At this level, bare array return types fail. You must declare array<string, int> or a typed collection. Every conditional path must return a consistent type. A function that returns string in one branch and null in another triggers an error unless the signature allows it.

Level 5getUser(): arrayPasses — generic array OKif ($user) return $userPasses — null path ignoredcollect($data)Passes — mixed collection OKLevel 9getUser(): arrayFails — need array key typesif ($user) return $userFails — missing else returncollect($data)Fails — need Collection type
PHPStan level 9 rejects generic arrays and implicit null returns that lower levels accept silently

Level 9 catches three bug classes that lower levels miss entirely:

  • Collection type erosion: When you map an Eloquent collection, PHPStan verifies the closure return matches the declared generic. A docblock saying @return Collection<User> fails if the closure can return null.
  • Unsafe array access: Reading $array['key'] without proof the key exists triggers an error. Prior validation or type narrowing must establish the key.
  • Exhaustiveness gaps: Match and switch on enums or unions must cover every case. Incomplete coverage becomes a hard error, not a silent fallthrough.

This strictness pays off on long-lived systems. On legal-tech portals like Court Marriage In Nepal, level 9 caught edge cases in attestation status handlers that manual review had missed for months. The upfront cost is real. The reduction in production debugging time justifies it for any codebase expected to survive beyond two years.

French-speaking teams searching for phpstan analyse statique php follow the same workflow. PHPStan is language-agnostic. Only your config files and error messages differ. The official PHPStan getting started guide documents level definitions in English regardless of locale.

How do you configure PHPStan level 9 for Laravel and Symfony?

Vanilla PHPStan cannot understand framework magic. Without extensions, level 9 flags every Facade call, dynamic relation, and container resolution as an error. Install framework adapters first.

Laravel with Larastan

For Laravel 13.x on PHP 8.3 or higher, use larastan/larastan 3.x. Laravel 12 projects on PHP 8.2 can use the same Larastan major version with adjusted framework constraints.

composer require --dev larastan/larastan:^3.0 phpstan/phpstan:^2.0
# phpstan.neon
includes:
    - vendor/larastan/larastan/extension.neon

parameters:
    level: 9
    paths:
        - app/
        - config/
        - routes/
    excludePaths:
        - app/Legacy/*
    scanFiles:
        - app/Support/helpers.php
    checkMissingIterableValueType: true
    checkGenericClassInNonGenericObjectType: true
    reportUnmatchedIgnoredErrors: false

The two check* flags enforce generic annotations that separate level 9 semantics from level 9 syntax alone. Without them, you run max level but skip the strictest iterable checks.

Pair this setup with Laravel feature testing best practices so runtime tests confirm what static analysis proves at compile time.

Symfony with phpstan-symfony

For Symfony 8.1 on PHP 8.4.1 or higher, install the Symfony and Doctrine extensions together:

composer require --dev phpstan/phpstan-symfony phpstan/phpstan-doctrine

Symfony DI containers need the extension to resolve service types. Custom compiler passes often require stub files in a stubs/ directory referenced via scanDirectories. See the phpstan-symfony repository for extension-specific neon examples.

Symfony 7.4 LTS teams on PHP 8.2 follow the same pattern. Pin extension versions in composer.lock and run analysis inside CI with Composer 2.10.

Baseline management without losing momentum

Never fix all level 9 errors in one sprint. Generate a baseline, then enforce strictness on new and changed code only:

vendor/bin/phpstan analyse --generate-baseline=phpstan-baseline.neon

Include the baseline in your neon config. Set reportUnmatchedIgnoredErrors: false during initial adoption so deleted legacy files do not break CI. Regenerate the baseline monthly to track debt reduction.

InstallLarastan / SymfonyBaselineCapture legacy errorsCI GateBlock new violationsShrinkMonthly regenBaseline lifecycle rulesNew code must pass level 9 cleanlyLegacy fixes reduce baseline countNo new ignores without a ticketRegenerate baseline each sprint
Sustainable PHPStan level 9 adoption uses baselines to protect velocity while shrinking debt over time

Wire analysis into your pipeline with GitLab CI/CD for PHP projects or GitHub Actions for Laravel testing and deploy. A typical job runs in under a minute on medium apps when you cache Composer dependencies.

# .gitlab-ci.yml excerpt
phpstan:
  stage: test
  script:
    - composer install --no-interaction --prefer-dist
    - vendor/bin/phpstan analyse --memory-limit=1G
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

On a recent e-commerce migration, baseline entries dropped from 1,400 to under 200 in six months. New features had to pass level 9 clean. Legacy refactors happened incrementally alongside normal tickets.

Which type-narrowing patterns satisfy PHPStan level 9 strictness?

Level 9 forces you to prove types explicitly. Three patterns handle most adoption errors.

Assert helpers for guard clauses

Reusable assertion functions narrow types for both PHPStan and runtime:

final class Assert
{
    /** @throws InvalidArgumentException */
    public static function nonNull(?object $value, string $msg = ''): object
    {
        if ($value === null) {
            throw new InvalidArgumentException($msg ?: 'Expected non-null value');
        }
        return $value;
    }

    /**
     * @template T
     * @param array<T> $array
     * @return non-empty-array<T>
     */
    public static function nonEmptyArray(array $array, string $msg = ''): array
    {
        if ($array === []) {
            throw new InvalidArgumentException($msg ?: 'Expected non-empty array');
        }
        return $array;
    }
}

After Assert::nonNull($user), PHPStan treats $user as non-null for the rest of the scope. This beats repetitive inline null checks.

Generic annotations for collections

Document collection contents precisely. Eloquent repositories should always declare the model type:

/**
 * @return Collection<int, User>
 */
public function getActiveUsers(): Collection
{
    return User::query()
        ->where('is_active', true)
        ->get();
}

When transforming collections, chain the generic through the operation. If map() returns a different type, declare it explicitly. Level 9 validates closure return types against the declared output generic.

This pairs well with advanced Eloquent techniques where complex query builders need explicit return documentation.

Match expression exhaustiveness

Replace switch statements with match backed by enums. Always include a default that throws:

enum DocumentStatus: string
{
    case Draft = 'draft';
    case Pending = 'pending';
    case Approved = 'approved';
}

function getStatusLabel(DocumentStatus $status): string
{
    return match ($status) {
        DocumentStatus::Draft => 'Draft',
        DocumentStatus::Pending => 'Under Review',
        DocumentStatus::Approved => 'Approved',
        default => throw new LogicException("Unhandled status: {$status->value}"),
    };
}

The default throw satisfies exhaustiveness checks. It also surfaces a clear error when someone adds an enum case without updating the match. This pattern prevented production incidents on legal document workflows where status rules change frequently.

For bulk type declaration on legacy code, run PHP Rector for automated refactoring first. Then let PHPStan level 9 validate the result. Rector adds types; PHPStan proves they hold.

How does PHPStan level 9 compare to Psalm and Rector?

Each tool serves a distinct role. They complement each other rather than compete.

CriteriaPHPStan Level 9Psalm (Max Strictness)Rector
Primary focusBug detection via type flowType inference and taint analysisAutomated code transformation
Laravel supportExcellent via Larastan 3.xGood via psalm-plugin-laravelModerate via rector-laravel
False positive rateLow with proper extensionsModerate with aggressive inferenceN/A — transforms, does not validate
Config formatNEONXMLPHP config with presets
CI speed (medium app)Fast, roughly 30 secondsSlower due to deeper inferenceSlowest — AST rewriting
Best forRuntime bug prevention gateSecurity and taint trackingLegacy modernization sprints

I use PHPStan level 9 as the primary CI gate because error messages map directly to runtime failures. Psalm supplements security-sensitive modules like payment processing where taint analysis adds value. Rector handles bulk upgrades when moving from PHP 8.2 to 8.5 or adding return types to untyped legacy classes.

Type safety goal?Runtime bugsPHPStan level 9Taint / securityPsalm max levelLegacy upgradeRector presetsUse whenNew feature developmentCritical path refactorsPre-deploy CI gateTeam coding standardsUse whenUser input flowsPayment modulesSQL injection pathsAlongside PHPStanUse whenPHP version migrationAdding return typesFramework upgradesOne-time transforms
Choose PHPStan level 9 for daily bug prevention; add Psalm and Rector for security and legacy modernization

Combine static analysis with code coverage gates in CI and shift-left security in CI/CD pipelines for a complete pre-merge quality stack.

What are common pitfalls when adopting PHPStan level 9 in production?

Three mistakes derail most adoption attempts. Avoid them to keep team momentum.

  1. Enabling level 9 globally on day one. This floods the team with thousands of legacy errors. Start with a baseline. Enforce strictness only on new and modified files. Incremental adoption preserves velocity.
  2. Skipping framework extensions. Vanilla PHPStan on Laravel or Symfony produces noise that hides real bugs. Install Larastan or phpstan-symfony before writing a single ignore comment.
  3. Treating ignores as permanent. Every @phpstan-ignore or baseline entry needs a ticket or TODO with context. Schedule quarterly reviews to convert ignores into proper fixes.

Third-party packages without type declarations cause another subtle problem. Do not ignore entire vendor directories. Create stub files in stubs/ with minimal signatures for the methods you actually call. This keeps level 9 coverage in your code while acknowledging external gaps.

Level 9 adoption gotchasDay-one global enableThousands of errorsTeam burnoutFeature freezeBaseline firstLegacy capturedNew code enforcedDebt shrinks monthlyVanilla PHPStanFacade false positivesIgnored real bugsDeveloper distrustLarastan / Symfony extFramework-aware typesActionable errors onlyTeam trusts the tool
Avoid common PHPStan level 9 pitfalls by using baselines and framework extensions from day one

For developers evaluating hiring web developers in Nepal, familiarity with stub creation and baseline management signals genuine PHPStan experience. Superficial tool usage shows up quickly when legacy codebases resist level 9.

On production Laravel apps I maintain, PHPStan runs alongside Pest testing in CI/CD and Laravel Pint for coding standards. Static analysis catches logic bugs tests miss. Tests catch integration failures analysis cannot see.

How do you integrate PHPStan level 9 into a full quality workflow?

Static analysis alone does not ship reliable software. It belongs inside a broader quality stack that runs before every merge.

A practical pipeline for a Laravel 13 project looks like this:

  • Pre-commit: Run Pint and a quick PHPStan pass on staged files via git hooks.
  • CI merge request: Full PHPStan level 9 analysis, Pest test suite, and dependency vulnerability scan.
  • Pre-deploy: Regenerate baseline if legacy fixes landed. Confirm baseline count did not increase.
  • Post-deploy: Monitor error logs for type-related runtime failures PHPStan cannot predict, like external API shape changes.

For business-critical systems, engage a team that treats analysis as architecture, not an afterthought. Our testing and optimization services include static analysis setup, baseline strategy, and CI integration for Laravel and Symfony codebases.

Projects like Mijar Law Associates and Adventure Third Pole Trek benefit from strict typing because their booking and document workflows involve complex state machines. Level 9 catches branch gaps before they become support tickets.

Use the regex tester when writing custom PHPStan ignore patterns. Misconfigured regex in baseline files silently stop matching errors you intended to suppress.

PHP 8.5 readonly classes and property hooks reduce some annotation boilerplate. See PHP 8.4 property hooks practical guide for patterns that interact cleanly with static analysis. Stronger native types mean fewer docblock generics PHPStan must infer.

WordPress plugin teams can adopt incremental analysis too. See WordPress plugin development guide for where typed PHP fits alongside dynamic CMS APIs. Core WordPress 7.1 still mixes typed and untyped code, so baselines matter even more there.

Security-sensitive modules benefit from layering. Run PHPStan level 9 for logic bugs. Add OWASP Top 10 checks for PHP developers and optional Psalm taint analysis for input-to-sink tracking on auth and payment paths.

Key Takeaways

  • PHPStan level 9 requires generic array types, exhaustive conditionals, and proven null safety — not just return type declarations.
  • Install Larastan for Laravel or phpstan-symfony for Symfony before enabling max level; vanilla analysis produces unusable noise.
  • Generate a baseline for legacy debt, enforce level 9 on all new code in CI, and regenerate the baseline monthly to shrink violations.
  • Use assert helpers, typed collection docblocks, and exhaustive match expressions to satisfy strictness without excessive boilerplate.
  • Pair PHPStan with Rector for legacy modernization and Psalm for taint analysis — keep level 9 as your primary runtime bug gate.
  • Wire analysis into GitLab CI or GitHub Actions alongside Pest tests and Pint for a complete pre-merge quality stack.

People Also Ask

What PHPStan level should I start with?

Start at level 5 or 6 on existing codebases. Fix the most critical errors, then generate a baseline. Move to level 9 only after framework extensions are installed and CI enforces clean analysis on new files. Greenfield modules can target level 9 from the first commit.

How long does PHPStan level 9 take to run in CI?

A medium Laravel application with 500–800 PHP files typically completes in 20–40 seconds on a modern CI runner. Cache Composer dependencies and enable PHPStan result caching to keep merge request pipelines fast. Memory limit of 1G handles most apps.

Can PHPStan level 9 replace unit tests?

No. Static analysis proves type consistency and catches logic paths tests often miss. Unit and feature tests prove runtime behavior, database interactions, and external API contracts. Use both. PHPStan runs first because it is faster and catches entire bug classes tests never cover.

Does PHPStan work with PHP 8.5 and Laravel 13?

Yes. Larastan 3.x supports Laravel 12 and 13 on PHP 8.2 through 8.5. PHPStan 2.x understands readonly classes, enums, intersection types, and property hooks introduced in recent PHP versions. Pin versions in composer.lock and run analysis in CI on the same PHP version as production.

Build type-safe PHP that survives production

Adopting phpstan level 9 is a commitment to long-term code health, not a one-time cleanup sprint. Start with framework extensions, generate a baseline, and enforce strictness incrementally on new features. Use assert helpers, precise generics, and exhaustive match expressions to satisfy level 9 without drowning in boilerplate.

The discipline pays compounding dividends. Systems maintained at max strictness show fewer production incidents, faster onboarding, and safer refactors. For business-critical applications — from legal-tech portals to e-commerce platforms — type safety directly reduces maintenance cost and operational risk.

Need help integrating PHPStan into an existing Laravel or Symfony project? Contact us to discuss your codebase and adoption plan. You can also reach out directly for a quick technical assessment. Practical implementation beats theoretical perfection every time.

Frequently Asked Questions

PHPStan Level 9 is the strictest analysis setting, enforcing precise type declarations, null safety, and return types across your entire codebase. It catches subtle bugs that lower levels miss by treating every missing type hint as an error.

Budget Rs 40,000–80,000 (USD 300–600) for a mid-sized legacy app. Most time goes into adding return types and fixing false positives rather than actual bug fixes. In my experience, this investment prevents costly production debugging later.

Avoid Level 9 on projects with heavy magic methods, outdated third-party packages lacking stubs, or teams new to static analysis. Start at Level 5 or 6 first. Jumping straight to max strictness often causes developer fatigue without proportional quality gains.

Install larastan/larastan via Composer and set level: 9 in phpstan.neon. Add paths to app/, config/, routes/, and database/. Include bootstrapFiles pointing to vendor/autoload.php and a custom Laravel bootstrap file if needed. Configure ignoreErrors for known framework quirks like Facade proxies or dynamic container bindings that Larastan cannot fully resolve. Always run vendor/bin/phpstan analyse --memory-limit=1G initially to establish your baseline error count before tightening rules incrementally.

Missing return type declarations on controllers and services top the list. Nullable parameters without explicit ?Type syntax trigger failures. Generic array types like array instead of array cause issues. Dynamic property access on stdClass objects fails without proper casting. In legal-tech portals I have built, document processing classes often lacked precise return types for PDF generation methods, which Level 9 immediately flagged. Fix these systematically using PHPDoc @return annotations first, then refactor to native PHP 8.2+ union and intersection types where possible.

Both enforce similar strictness but differ in ecosystem support. PHPStan has superior Laravel integration via Larastan with active maintenance through 2026. Psalm offers more granular taint analysis for security-focused projects. PHPStan reports tend to be more actionable for business logic errors. For Nepal-based teams already using Laravel, PHPStan Level 9 provides faster ROI due to better framework awareness and community stubs. I recommend PHPStan unless your primary concern is SQL injection or XSS vulnerability detection specifically.

Not directly. Level 9 enforces type safety which indirectly prevents some injection vectors by requiring explicit string handling. For actual security scanning, combine PHPStan with Rector for automated fixes and dedicated tools like SonarQube or Snyk. Type precision reduces runtime surprises but does not replace OWASP-focused auditing. On client projects handling sensitive legal documents, I always pair Level 9 analysis with manual code review for authorization checks and input sanitization patterns that static analyzers cannot understand contextually.

Expect two to four weeks for experienced developers working part-time on a medium codebase. The jump from 5 to 7 involves adding parameter and return types. Levels 8 and 9 require resolving generic types and conditional returns. Progress slows exponentially at higher levels. Track metrics weekly: aim to reduce errors by ten percent per sprint. Rushing causes technical debt in suppression comments. On one Laravel eCommerce migration, we spent three weeks reaching Level 9 across forty thousand lines while maintaining feature delivery velocity.

Yes, but requires significant configuration. Use szepeviktor/phpstan-wordpress extension for core function stubs. Set level: 9 only after establishing baselines at lower levels. WordPress globals and hook-based architecture generate many false positives. Create custom ignoreErrors entries for do_action and apply_filters calls. Define stub files for third-party plugins like WooCommerce that lack type declarations. In practice, Level 6 or 7 often provides better value for WordPress projects due to the platform's inherent dynamism. Reserve Level 9 for isolated plugin modules with modern PHP patterns rather than entire themes.

PHP 8.2 minimum is strongly recommended for Laravel 12 and Symfony 7.x projects targeting Level 9. Native union types, intersection types, readonly properties, and DNF types available in 8.2+ make compliance achievable without excessive PHPDoc. Running analysis on PHP 8.1 works but forces reliance on docblock annotations that drift from runtime behavior. PHP 8.4 adds further type refinements but is not yet widely adopted in production as of mid-2026. Match your analysis runtime to your deployment target to avoid discrepancies between CI results and live environment behavior.

Use Larastan's built-in facade resolution which covers most standard facades in Laravel 11 and 12. For custom facades or edge cases, add @mixin annotations to your facade class pointing to the underlying service. Create phpstan-baseline.neon using vendor/bin/phpstan analyse --generate-baseline to suppress known safe errors temporarily. Review baselines quarterly; permanent suppressions indicate architectural problems. Never globally ignore Facade-related errors. On legal service portals using custom document facades, I found targeted mixin annotations resolved ninety percent of false positives while preserving genuine error detection for misconfigured bindings.

For brochure sites under five thousand lines, probably not. Level 5 or 6 catches major issues without overhead. For transactional systems processing payments via eSewa or Khalti, managing bookings, or handling legal documents, Level 9 pays off quickly. The cost of a missed null check in payment confirmation logic far exceeds setup time. Consider project lifespan: short-term marketing sites benefit less than platforms maintained for years. Many Nepal SMBs operate lean teams; investing in prevention now reduces emergency debugging during Dashain peak seasons when developer availability drops and business impact multiplies.

Add a phpstan job in .gitlab-ci.yml running vendor/bin/phpstan analyse --error-format=gitlab --no-progress. Cache vendor/ and composer cache directories between runs. Fail pipeline on any new errors by comparing against committed baseline file. Set memory limit to 2G for large codebases. Run analysis on merge requests before main branch merges. On sister sites sharing Deployer 7 pipelines, we added PHPStan as a mandatory gate after PHPUnit tests. This caught type regressions introduced during dependency updates before deployment. Total CI overhead adds thirty to ninety seconds depending on codebase size and runner specifications.

Psalm offers comparable strictness with stronger security taint tracking. Rector automates refactoring toward higher PHP versions and type coverage. Exakat provides broader architectural insights beyond type checking. IntelliJ IDEA and VS Code extensions offer real-time feedback during development. However, none match PHPStan plus Larastan combination for Laravel-specific accuracy in 2026. For Symfony projects, PHPStan remains dominant. Magento 2 has specialized tooling but benefits from PHPStan for custom module validation. Choose based on framework alignment and team familiarity rather than theoretical superiority. Combining PHPStan Level 9 with Rector for automated upgrades delivers best practical outcomes.

Make analysis mandatory in CI; never allow merging code that introduces new errors. Update baselines only with documented justification in commit messages. Schedule monthly reviews of suppressed errors to identify systemic improvements. Train new developers on type declaration standards during onboarding. Keep Larastan and PHPStan updated alongside framework upgrades. Document project-specific conventions in CONTRIBUTING.md. On long-running legal-tech platforms, we treat type coverage like test coverage: non-negotiable quality metric. Compliance degrades without active enforcement. Budget quarterly maintenance windows specifically for addressing accumulated technical debt in type annotations and baseline exceptions.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: