| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
The faithful assistant for your FrankenPHP Workers.
⭐️ If you find Igor useful, please consider leaving a star! It encourages us to keep maintaining and improving the project.
igor-php is an ultra-fast static linter written in Go that prepares your Symfony application for the persistent memory model of FrankenPHP.
Like the legendary assistant, igor checks every connection and part of your application to ensure it won't "blow up" when the lightning strikes (Worker Mode).
During its static analysis of shared/singleton services, Igor recursively scans the AST to detect and flag several dangerous patterns that can lead to memory leaks or state pollution across requests:
| Category | Pattern / Rule | Description | Impact Level | Code Example |
|---|---|---|---|---|
| Dependency Mutation | DetectSingletonMutation | Calling mutation methods (starting with set, add, push, register, append, disable, enable, clear, remove) on injected properties, chained method calls, or local references of shared services (with smart alias tracking). | 🔴 Critical | $this->googleTagManager->addPush($data); $entityManager->getFilters()->disable('softdeleteable'); |
| Resettable Bypass | (Bypass) | Igor automatically resolves Symfony autowire aliases and abstract interface/implementation chains to see if the dependency is marked as resettable, and ignores mutation warnings on it. | 🟢 Safe (Auto) | $this->translator->setLanguage($lang); (if translator is resettable) |
| Closure State Leak | DetectClosureStateLeak | Passing anonymous functions that capture local variables (use ($var)) to shared service dependencies. | 🔴 Critical | $this->dispatcher->addListener('response', function () use ($optin) {}); |
| Finally Cleanup | (Bypass) | Igor natively detects when a mutated state is guaranteed to be cleaned up inside a finally block (using array_pop, unset, or direct assignments), and automatically bypasses the error. | 🟢 Safe (Auto) | try { $this->stack[] = $item; } finally { array_pop($this->stack); } |
| State Mutation | StateMutation | Direct assignment or mutation of class properties or static variables at runtime. | 🔴 Critical | $this->count++; self::$cache[] = $val; |
| Reset Interface | IncompleteReset | Class implements ResetInterface but some of its mutated properties are not cleared inside reset(). | 🟡 Warning | public function reset() { // forgot to clear $count } |
| Process State Mutation | ProcessStateMutation | Functions modifying global PHP configuration or runtime behavior that persist across requests. | 🟡 Warning | date_default_timezone_set('UTC'); ini_set('memory_limit', '256M'); |
| Local Static Variable | LocalStaticVariable | Declaring local static variables inside methods, which persist across the PHP process lifecycle. | 🔴 Critical | static $counter = 0; |
| PHP Superglobals | SuperglobalsUsage | Direct access to legacy superglobals instead of injecting or using the framework's Request object. | 🟡 Warning | $_GET['id'] or $_POST['name'] |
| Process Termination | ExecutionTerminator | Standard PHP termination statements that crash the persistent worker. | 🔴 Critical | exit() or die() |
| Shared Service Lifecycle | MagicMethodDestruct | Declaring __destruct() magic method on shared/singleton services. | 🔴 Critical | public function __destruct() { ... } |
💡 Philosophy & False Positives: Igor's primary mission is to shield you as much as possible from dangerous code patterns that can pollute state or leak memory in persistent worker environments.
To achieve this, Igor is deliberately strict: we chose to report as many potential issues as possible to guide your eyes to where things might go wrong. Consequently, Igor may occasionally raise false positives. It remains your responsibility to analyze Igor's findings and decide if they can be safely ignored (e.g., using // @igor-ignore or the #[WorkerSafe] attribute).
Pro-Tip: Enabling the Symfony Bundle dramatically reduces false positives. It grants Igor direct visibility into Symfony's compiled container, allowing it to bypass warnings for transient (non-shared) services and automatically ignore mutations on services marked as resettable (tagged with kernel.reset).
Smart Alias & Interface Resolution: Igor is smart enough to traverse Symfony aliases and interface-to-implementation autowire chains. If your class property is type-hinted with an interface (e.g. TranslatorInterface), Igor will automatically resolve the alias chain in the container to find the concrete service definition (even if it uses decorated IDs like .abstract.instanceof...) to check if it's resettable, preventing false positive warnings.
🧠 Smart Stack Cleanup (Finally Blocks): If your service manages temporary state using a stack or push/pop pattern (like Symfony's AuthorizationChecker), Igor is smart enough to scan finally clauses. If it detects that a mutated property is guaranteed to be restored or cleaned up inside the finally block (using array_pop(), array_shift(), unset(), or direct resets), the mutation is marked as safe and the warning is automatically bypassed.
🧠 Infrastructure Taint Breakers: Igor's taint-tracking is incredibly powerful, but mutating query-scoped objects or transient builders (like Doctrine Queries, QueryBuilders, Symfony's ConstraintViolationBuilder from $this->context->buildViolation(...), or PSR-6 cache items via $this->cache->getItem(...)) is completely safe. Igor has built-in Taint Breakers for these standard design patterns: any chained calls or assignments coming from methods starting with find, create, build, or calling getItem/getItems are recognized as transient/ephemeral. This automatically halts taint propagation and eliminates noise on your repositories, cache services, and validator classes.
composer require --dev igor-php/igor-phpWhen running Igor, the included php bootstrapper will download the Go binary of Igor in the version matching the installed composer-package version.
You can override this behavior by setting the IGOR_VERSION environment variable to a version string (i.e. 0.8.9) to use a specific version or to latest to always get the latest version.
In case your autoload.php is not in the standard location vendor/autoload.php you have to set it via the IGOR_AUTOLOAD_LOCATION environment variable.
To make Igor even more reliable, you can enable the embedded PHP bundle. It generates a precise service map directly from your container, which Igor Go will use to audit your services.
Add the bundle to your config/bundles.php:
return [
// ...
IgorPhp\IgorBundle\IgorPhpBundle::class => ['dev' => true, 'test' => true],
];Or manually in your Kernel.php:
public function registerBundles(): iterable
{
// ...
if ($this->getEnvironment() === 'dev') {
yield new IgorPhp\IgorBundle\IgorPhpBundle();
}
}go install github.com/igor-php/igor-php@latestIgor can automatically detect your project type Symfony and generate a default configuration for you:
# Initialize igor.json
igor-php init
# Initialize with a custom name/path
igor-php init -c custom-igor.jsonOnce initialized (or using defaults), let Igor audit your project:
# Standard usage
igor-php .
# Generate a baseline to ignore existing errors
igor-php --generate-baseline .
# Custom configuration file
igor-php --config custom-igor.json .
# or shorthand
igor-php -c custom-igor.json .
# Custom console path, environment and verbose mode
igor-php --console app/console --env stage --verbose .
# Non-Symfony project or skip Symfony discovery
igor-php --no-agent .If you want to understand the exact sémantique and diagnostic criteria that led Igor to flag or approve a service, you can run the explain command:
# Display diagnostic matrix for all services
igor explain .
# Filter the matrix on a specific class or file name (case-insensitive)
igor explain . SuperServiceThis prints a tabular matrix summarizing which rules (Static mutations, Term terminators, Super globals, or Leak closure captures) are triggered on each class, alongside a detailed diagnosis explaining why each class is either safe (✅ OK) or dangerous (❌ KO):
🔍 Igor Explanation Matrix - Services Audit Diagnoses
=====================================================
+-----------------------------------------+--------+--------+-------+-------+-------+-------------------------+
| Service Class | Shared | Static | Term | Super | Leak | Verdict |
+-----------------------------------------+--------+--------+-------+-------+-------+-------------------------+
| App\Service\StatefulService | YES | NO | NO | NO | NO | ❌ KO (State Mutation) |
| App\Service\LocalStaticService | YES | YES | NO | NO | NO | ❌ KO (State Poison) |
| App\Controller\LeakDemoController | YES | YES | YES | YES | YES | ❌ KO (State Poison) |
| App\Service\TracerLeakDemoService | YES | NO | NO | NO | NO | ✅ OK (Stateless) |
+-----------------------------------------+--------+--------+-------+-------+-------+-------------------------+
It fully loads and respects local and external baselines, allowing you to debug exactly why your active findings occur.
In legacy projects or during initial integration, you might want to ignore existing findings to prevent CI pipeline failures and focus on new code. Igor supports baseline files with advanced checking and cleaning commands.
To create a baseline of all currently detected findings:
igor-php --generate-baseline .This generates an igor-baseline.json file (or writes to the file path specified by --baseline). Each ignored entry has a reason field pre-filled with a default comment. We highly encourage developers to document why an exclusion is a false positive or safe rather than just hiding alerts:
{
"files": {
"src/Service/MyService.php": [
{
"message": "Mutation of state 'static::$cache' in MyService::cache()",
"reason": "TODO: Explain why this state mutation is a false positive or safe"
}
]
}
}Over time, files get refactored, fixed, or deleted. To ensure your baseline doesn't become a graveyard of obsolete rules, run the check command:
igor-php --check-baseline .To automatically clean up your baseline by removing all stale/obsolete entries:
igor-php --prune-baseline .This will rewrite igor-baseline.json (or your custom baseline path) on disk, removing any rules that are no longer active, keeping your configuration neat and relevant.
Igor can also audit standard PHP projects that don't use the Symfony framework. In this case, use the --no-agent flag to disable automatic container discovery.
When using Igor without Symfony, you should manually define which directories or vendor packages to audit in your igor.json:
{
"scan_vendors": ["my-company/internal-library"],
"exclude": ["tests", "Data", "vendor/symfony"]
}💡 Note: Without Symfony, Igor performs a recursive scan of your project directory (excluding folders in exclude). Using scan_vendors allows you to force the audit of specific third-party libraries even without the Symfony service map.
Frameworks with their own DI container can give Igor the same signal the Symfony bridge provides: which classes are real shared services versus transient ones (per-request value objects, per-resolution helpers). Without it, a plain directory scan flags legitimate mutators on immutable-by-design value objects (PSR-7 Uri/Stream/Message, PSR-6 CacheItem, …) as state leaks.
Export your container's graph to a framework-agnostic JSON file and pass it with --container-dump:
{
"services": [
{ "class": "App\\Http\\Uri", "shared": false },
{ "class": "App\\Cache\\CacheItem", "shared": false },
{ "class": "App\\Service\\MailService", "shared": true }
],
"aliases": {
"App\\Contract\\MailerInterface": "App\\Service\\MailService"
}
}igor-php --no-agent --container-dump igor-container.json .By convention, keep igor-container.json at the project root, side-by-side with igor.json. Any class listed with "shared": false is treated as transient and its state mutations are skipped — exactly as the Symfony bridge already skips non-shared (prototype) services. Classes marked "shared": true, or absent from the file, continue to be audited normally. The optional "aliases" map tells Igor which interface FQCN resolves to which concrete class, so reachability ranking can follow calls made through an interface type-hint. You can also set the path in igor.json via "container_dump": "igor-container.json".
💡 The format is intentionally minimal so any framework can produce it (Laravel, Laminas, …). Symfony's igor_service_map.json is simply one richer producer of the same idea.
If you generate this file from a framework command rather than committing it, a gitignored build path (e.g. var/igor-container.json) is also fine — just regenerate it in CI before running Igor, the same way the Symfony agent map is warmed up.
Igor's core stays framework-agnostic — the Symfony bundle and the generic --container-dump contract are all the engine needs. Anyone can ship a thin bridge that produces that signal for their own framework. Community-maintained bridges:
| Framework | Bridge | Notes |
|---|---|---|
| Waffle | waffle-commons | Emits a --container-dump service map and adopts Igor's #[WorkerSafe] attribute for FrankenPHP worker-mode state audits. |
Maintain a bridge for another framework? Open a PR adding a row — the only contract is the --container-dump JSON shape shown above.
Want to understand why Igor is vital for your Worker environment? Check these real-world scenarios from our Leak Lab:
Igor identifies all leaks (Static, Stateful, Incomplete Reset) and dangerous global function calls automatically.
We've built an interactive laboratory using Symfony and FrankenPHP. You can run it locally with Docker and see the memory leaks with your own eyes.
When a Symfony project is detected, Igor combines three layers of discovery to ensure maximum reliability:
Igor reads the require-dev section of your composer.json. When it audits your Symfony container, it checks the physical path of each service. If a service is located inside a vendor/ directory belonging to a dev package (like phpunit/phpunit or symfony/maker-bundle), Igor will automatically skip it.
The IgorPhpBundle includes a CompilerPass that runs every time you clear your Symfony cache (php bin/console cache:clear).
⚠️ Important: You must run php bin/console cache:clear whenever you add or modify services in your Symfony project to ensure the Igor Agent map is up-to-date.
{
"definitions": {
"app.mail_service": {
"class": "App\\Service\\MailService",
"public": true,
"shared": true
},
"logger": {
"class": "Monolog\\Logger",
"public": true,
"shared": true
}
},
"aliases": {
"Psr\\Log\\LoggerInterface": "logger"
}
}To prevent false positives on ephemeral objects (like OpenTelemetry Spans or Transient DTOs) returned by your shared services, Igor transitions from purely lexical checks (guessing based on method names) to semantic type tracking:
Not every flagged mutator matters equally: a setter on a vendor class might never be called anywhere in your application, while another is hit on every request. Igor ranks findings accordingly:
📂 vendor/knp/snappy/src/Knp/Snappy/AbstractGenerator.php
[VENDOR] [HIGH] Mutation of state 'temporaryFiles' in AbstractGenerator::createTemporaryFile()
513 | $this->temporaryFiles[] = $filename;
[VENDOR] [INFO] Mutation of state 'binary' in AbstractGenerator::setBinary()
275 | $this->binary = $binary;
💡 Known limitation: reachability matching works on exact Class::Method pairs. Igor conservatively follows direct and multi-level extends chains — a subclass that inherits, but doesn't override, a flagged parent method is linked through to the parent's finding, and an override correctly stops that promotion at the overriding class. It also follows interface aliases from the Symfony agent map or --container-dump, so a call through an interface type-hint is linked to the concrete implementation. It does not follow traits or magic methods (__call, __get, etc.), so a call resolved only through one of those still won't be linked through. Treat [INFO] as "no call site found with this analysis", not an absolute guarantee of dead code.
You can customize Igor's behavior by creating an igor.json file at the root of your project:
{
"exclude": ["vendor", "tests", "Entity"],
"safe_namespaces": ["Symfony\\", "Doctrine\\", "IgorPhp\\IgorBundle\\"],
"scan_vendors": ["my-company/internal-bundle"],
"ignore_vendors": false,
"baseline": "igor-baseline.json",
"ignore_external_baseline": false,
"container_dump": "igor-container.json",
"console_path": "bin/console",
"env": "dev",
"verbose": false
}When you first adopt Igor, you might want to grandfather in existing technical debt by generating a baseline file:
# Generate a baseline file (default name: igor-baseline.json)
igor-php --generate-baseline .Subsequent audits will ignore findings present in this baseline file.
If your project depends on other local packages or vendor dependencies that also manage their technical debt with igor-php, Igor will automatically discover, translate, and merge their baselines into the audit!
To list all discovered vendor baselines, check their type (regular file vs. symbolic link), and inspect all ignored rules along with their documented reasons, run the debug subcommand:
igor-php debug-external-baseline [directory]This will print a clean tree structure of all active external baselines:
🔍 Debugging external baselines for project at: /Users/thomas/projects/my-project
🛡️ Found regular external baseline for package acme/package1 at: /Users/thomas/projects/my-project/vendor/acme/package1/igor-baseline.json (1 files ignored)
🛡️ Found symlinked external baseline for package acme/package3 at: /Users/thomas/projects/local-packages/package3/igor-baseline.json (1 files ignored)
🛡️ Loaded 2 external baseline paths from vendor dependencies.
📋 Summary of loaded baseline files:
- vendor/acme/package1/src/Service1.php (1 rules ignored)
• State mutation detected in Service1
◦ Reason: Legacy code needing refactor
- vendor/acme/package3/src/Service3.php (1 rules ignored)
• State mutation detected in Service3
If you want to disable this behavior and only apply your root project's baseline:
# Ignore baselines from vendor dependencies
igor-php --ignore-external-baseline .You can also set this permanently in igor.json:
{
"ignore_external_baseline": true
}💡 RECOMMENDATIONS: [PROJECT] Since this is your code, you should refactor these services to be stateless or implement ResetInterface to clear the state between requests. [VENDOR] This is third-party code. If you can't fix it, consider setting a 'max_requests' limit in your Worker configuration to mitigate memory leaks.
Igor can export findings in a structured JSON format and help you triage them using an LLM. This is particularly useful for distinguishing between harmless state (e.g., caches) and dangerous data leaks.
Generate a ready-to-use prompt for your favorite LLM (ChatGPT, Claude, etc.):
# 1. Export the audit to JSON
igor-php --output llm . > audit.json
# 2. Generate the review prompt
igor-php review audit.jsonIgor will create igor-review-prompt.md. Simply copy its content into an LLM to get a detailed security analysis and remediation plan.
Configure Igor to call an LLM directly by updating your igor.json:
If you have gemini-cli installed and configured, Igor can use it directly:
{
"llm": {
"provider": "gemini",
"model": "gemini-1.5-pro"
}
}If you run Ollama locally, Igor can use its OpenAI-compatible endpoint. This is great for privacy, but please note that triage quality depends heavily on the model size. Smaller local models (like Llama 3 8B) are significantly less capable than large online models for complex security triage.
{
"llm": {
"provider": "ollama",
"model": "llama3"
}
}Note: Igor defaults the api_url to http://localhost:11434/v1 for Ollama.
{
"llm": {
"provider": "openai",
"api_url": "https://api.openai.com/v1",
"api_key_env": "OPENAI_API_KEY",
"model": "gpt-4o"
}
}Then run:
# For Option C, ensure the API key is set
export OPENAI_API_KEY=your_secret_key
igor-php review audit.jsonIgor will automatically send the audit to the LLM and save the report to igor-review.md.
If you have a specific line that you know is safe, you can use the // @igor-ignore annotation:
// @igor-ignore
$this->cache = $data; // This line will be ignored
$this->counter++; // @igor-ignore - This line tooInstead of line-by-line comments, you can use modern PHP 8 attributes to exclude entire classes, specific methods, or individual properties.
First, import the attribute (available via the embedded Symfony bundle):
use IgorPhp\IgorBundle\Attribute\WorkerSafe;Then decorate your code elements:
Class-level: Ignore all state leak and mutation findings within the entire class.
#[WorkerSafe(scope: 'boot-time', reason: 'Configuration is frozen after warmup')]
class MyService {
// All mutations and state checks inside this class are ignored
}Method-level: Ignore state mutations occurring inside a specific method.
class MyService {
#[WorkerSafe]
public function warmUp() {
$this->cache = ['foo' => 'bar']; // This mutation is ignored
}
}Property-level: Ignore all mutations on a specific property and exclude it from the ResetInterface verification. Works flawlessly with both standard and constructor-promoted properties!
class MyService {
#[WorkerSafe]
private $cache = []; // Mutations and missing reset checks are ignored
public function __construct(
#[WorkerSafe]
private StatefulService $safeService, // Promoted property is safe!
) {}
}When using the Deep Audit mode (Symfony), Igor might analyze fewer services than your total container count. Use the --verbose flag to see exactly why a service was skipped. Common reasons include:
💡 Pro Tip: If you notice Entities, DTOs, or Data Models appearing in the Igor audit, it means they are registered as "Shared Services" in your Symfony container. This is usually a configuration error in your services.yaml. You should exclude these directories from autowiring:
# config/services.yaml services: App\: resource: '../src/' exclude: - '../src/Entity/' - '../src/Dto/' - '../src/Kernel.php'
Igor is designed to work out-of-the-box in your CI pipelines. It will exit with code 1 if any error is found, effectively stopping your build.
When running inside GitHub Actions, Igor automatically generates inline annotations. This means errors will appear directly in your Pull Request review, right next to the code causing the issue.
name: Static Analysis
on: [push, pull_request]
jobs:
igor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Install Dependencies
run: composer install --no-progress --prefer-dist
- name: Warmup Symfony Cache (for Deep Audit)
run: php bin/console cache:warmup --env=dev
- name: Run Igor Audit
run: vendor/bin/igor-php .We welcome contributions of all kinds! Please refer to our CONTRIBUTING.md guide for instructions on how to set up the project, run tests, and validate your changes using either native tools or Docker.
MIT
| Back | FazBrowse Home | New Git URL |