ahmed-bhs/doctrine-doctor
Doctrine Doctor is a runtime analysis tool for Doctrine ORM integrated into the Symfony Web Profiler. It detects real-world issues like N+1 queries, slow queries, missing indexes, hydration overhead, and injection risks, with actionable backtraces and suggestions.
## Getting Started
Install via Composer:
```bash
composer require vendor/package-name --dev
Register the package in config/app.php under providers (if not auto-discovered). For first-time use, run:
php artisan vendor:package-name:analyze
Focus on database security and performance checks first—new analyzers (OverprivilegedDatabaseUserAnalyzer, HardcodedDatabaseCredentialsAnalyzer) target critical production risks. Use the --focus flag to target specific analyzers:
php artisan vendor:package-name:analyze --focus=database
Credential Hardcoding Detection:
DBAL configurations with embedded credentials (e.g., username: 'root', password: '...' in config/database.php or migrations)..env and use env() helpers. Example:
// Before (flagged)
$conn = DriverManager::getConnection(['url' => 'mysql://root:password@localhost/db']);
// After
$conn = DriverManager::getConnection(['url' => env('DB_DSN')]);
Privilege Escalation Detection:
root).app_user@localhost) and grant only required permissions:
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'secure_password';
GRANT SELECT, INSERT ON `app_db`.`users` TO 'app_user'@'localhost';
HardcodedDatabaseCredentialsAnalyzer to ensure credentials for least-privilege users are also secured.N+1 Query Optimization:
NPlusOneAnalyzer to detect repeated findBy() calls (e.g., fetching User entities by email in loops).IN or use DQL:
// Before (flagged)
foreach ($emails as $email) {
$user = $entityManager->find(User::class, $email); // Non-key lookup!
}
// After
$users = $entityManager->createQuery(
'SELECT u FROM App\Entity\User u WHERE u.email IN (:emails)'
)->setParameter('emails', $emails)->getResult();
Cache component or Doctrine’s SecondLevelCache.SensitiveDataExposureAnalyzer now flags getters for sensitive fields (e.g., getPassword()) without @ORM\Column(options={"fieldName": "hashed_password"}) or explicit protection.#[ORM\Column(type: 'string', options: ["fieldName" => "hashed_password"])]
private string $password;
public function getPassword(): string { return $this->password; } // Now flagged if no protection
PropertyTypeMismatchAnalyzer to ensure type safety (e.g., string vs. int for IDs).PropertyTypeMismatchAnalyzer now suggests fixes for:
#[Assert\NotNull] vs. ?string).string in PHP vs. integer in DB).// Before (flagged)
#[ORM\Column(type: 'string')]
private ?int $userId; // Mismatch
// After
#[ORM\Column(type: 'integer', nullable: true)]
private ?int $userId;
False Positives:
OverprivilegeDatabaseUserAnalyzer may flag test databases or local dev setups. Exclude them via:
// config/vendor-package-name.php
'analyzers' => [
'OverprivilegedDatabaseUserAnalyzer' => [
'exclude_databases' => ['test_db', 'homestead'],
],
],
HardcodedDatabaseCredentialsAnalyzer ignores env() calls but may miss dynamic DSNs (e.g., sprintf('mysql://%s:%s@...', $user, $pass)). Use static analysis tools like PHPStan in parallel.Performance Tradeoffs:
NPlusOneAnalyzer has a higher runtime cost. Run it separately:
php artisan vendor:package-name:analyze --only=NPlusOneAnalyzer
--path=src/Entity or --exclude=tests/.#[PHPDoc\Property(written: PHPDoc\When::NEVER)]
private string $apiKey;
public function getToken(): string {
if ($this->isDebug()) {
throw new \RuntimeException('Token exposure in debug mode!');
}
return $this->token;
}
Doctrine-Specific Quirks:
id as string vs. child id as int). Manually verify SingleTableInheritance or ClassTableInheritance setups.Type::getClassName() matches the PHP type hint.Nullability:
#[ORM\Column(nullable: true)]
#[Assert\NotNull]
private ?string $name;
#[Assert\NotBlank] on nullable fields. Resolve by adding @Assert\NotBlank(groups: ["Default", "!Create"]) for conditional validation.BaseAnalyzer and register them in config/vendor-package-name.php:
'analyzers' => [
App\Custom\MyAnalyzer::class,
],
resources/views/vendor/package-name/analyzers/. Example for NPlusOneAnalyzer:
{# Custom template for IN queries #}
{% if entityManager %}
Use a DQL IN query:
```php
$qb = $entityManager->createQueryBuilder();
$qb->select('u')
->from('{{ entityClass }}', 'u')
->where('u.{{ property }} IN (:values)')
->setParameter('values', {{ values|json_encode }});
```
{% endif %}
# .github/workflows/analysis.yml
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- run: php artisan vendor:package-name:analyze --fail-on=database,security
How can I help you explore Laravel packages today?