"""PHP-specific review guideline categories.
Ported from the canonical Rust implementation in
``ralph-workflow/src/guidelines/php.rs`` and adapted for the Python port.
The module models core PHP guidance plus optional Laravel and Symfony
extensions.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Iterable
from typing import Protocol
class ReviewGuidelines(Protocol):
"""Protocol for language-specific guideline containers."""
quality_checks: list[str]
security_checks: list[str]
performance_checks: list[str]
testing_checks: list[str]
documentation_checks: list[str]
idioms: list[str]
anti_patterns: list[str]
def summary(self) -> str: ...
def total_checks(self) -> int: ...
[docs]
@dataclass
class PHPGuidelines:
"""Review guidelines for PHP codebases.
Args:
frameworks: Optional framework names that activate stack-specific checks.
"""
frameworks: tuple[str, ...] = ()
quality_checks: list[str] = field(init=False)
security_checks: list[str] = field(init=False)
performance_checks: list[str] = field(init=False)
testing_checks: list[str] = field(init=False)
documentation_checks: list[str] = field(init=False)
idioms: list[str] = field(init=False)
anti_patterns: list[str] = field(init=False)
def __init__(self, frameworks: Iterable[str] = ()) -> None:
normalized_frameworks = tuple(frameworks)
self.frameworks = normalized_frameworks
self.quality_checks = [
"Use PHP 8+ features such as union types, attributes, and match "
"where they improve clarity.",
"Follow PSR-1/PSR-12 coding standards and keep PSR-4 autoloading tidy.",
"Keep public APIs small and expressive with typed request/response signatures.",
"Favor value objects, DTOs, and named arguments for constructors and factory helpers.",
]
self.security_checks = [
"Use prepared statements or parameter binding for every database query.",
"Escape untrusted output with htmlspecialchars() or a trusted templating engine.",
"Validate uploads, paths, and input metadata before processing.",
"Use password_hash() and password_verify() for authentication flows.",
]
self.performance_checks = [
"Profile before optimizing and justify non-obvious performance trade-offs.",
"Avoid repeated database queries inside loops; batch work and eager load "
"relationships.",
"Cache expensive computations and rendered fragments only when "
"invalidation remains clear.",
]
self.testing_checks = [
"Cover validation, authorization, and persistence failure paths in automated tests.",
"Exercise controller, route, or HTTP behavior with integration-style tests "
"when the framework supports them.",
"Protect serialization, database side effects, and exception handling "
"instead of just happy paths.",
]
self.documentation_checks = [
"Document public classes, functions, and console commands with actual behavior, "
"arguments, and return values.",
"Keep setup docs, environment requirements, and deployment notes in sync "
"with the live PHP stack and Composer configuration.",
"Explain middleware, listener, and lifecycle behavior that affects "
"request flow or bootstrapping.",
]
self.idioms = [
"Prefer Composer ecosystem conventions over custom abstractions.",
"Keep services, controllers, and domain classes focused on single responsibilities.",
"Use value objects, enums, and typed collections when they clarify intent.",
]
self.anti_patterns = [
"Avoid using extract() with user input.",
"Do not suppress errors with the @ operator.",
"Resist register_globals-style behavior and global state.",
]
for framework in normalized_frameworks:
self._apply_framework(framework)
def _apply_framework(self, framework: str) -> None:
"""Add framework-specific guideline extensions."""
framework_name = framework.casefold()
if framework_name == "laravel":
self.quality_checks.extend(
[
"Use Eloquent relationships intentionally and avoid ad hoc query building "
"when eager loading fits.",
"Follow Laravel conventions for service providers, middleware, "
"and route structure.",
"Leverage Laravel's validation system instead of rolling custom checks "
"where possible.",
"Use middleware for cross-cutting concerns like logging, authentication, "
"and feature flags.",
]
)
self.security_checks.extend(
[
"Keep CSRF protection enabled on state-changing routes.",
"Use Gates, Policies, and Form Requests for authorization and input "
"sanitization.",
"Sanitize input with request validation and escape output in views or APIs.",
]
)
self.performance_checks.extend(
[
"Review Eloquent queries for N+1 risks and eager load where needed.",
"Push slow work to queues or async jobs when it shouldn't run in "
"the request cycle.",
]
)
return
if framework_name == "symfony":
self.quality_checks.extend(
[
"Follow Symfony best practices for bundles, services, and controllers.",
"Use the DependencyInjection component consistently and keep wiring explicit.",
"Model validation with Symfony forms, constraints, or the validator component.",
]
)
self.security_checks.extend(
[
"Configure Symfony Security with firewalls, encoders, and voters as intended.",
"Use voters or access control expressions for authorization.",
]
)
self.documentation_checks.append(
"Document service wiring, bundles, and configuration conventions "
"when defaults are overridden."
)
[docs]
def summary(self) -> str:
"""Return a short human-readable summary."""
return (
f"{len(self.quality_checks)} quality checks, "
f"{len(self.security_checks)} security checks, "
f"{len(self.anti_patterns)} anti-patterns"
)
[docs]
def total_checks(self) -> int:
"""Return the total number of configured checks."""
return sum(
[
len(self.quality_checks),
len(self.security_checks),
len(self.performance_checks),
len(self.testing_checks),
len(self.documentation_checks),
len(self.idioms),
len(self.anti_patterns),
]
)
__all__ = ["PHPGuidelines"]