From acc
Analyzes PHP code for side effects including state mutation, global access, static method calls, I/O operations, and output mixed with business logic to improve testability.
How this skill is triggered — by the user, by Claude, or both
Slash command
/acc:check-side-effectsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Analyze PHP code for side effects that reduce testability.
Analyze PHP code for side effects that reduce testability.
// SIDE EFFECT: Mutates input
public function normalize(array &$data): void
{
$data['name'] = trim($data['name']);
$data['email'] = strtolower($data['email']);
}
// PURE: Returns new array
public function normalize(array $data): array
{
return [
'name' => trim($data['name']),
'email' => strtolower($data['email']),
];
}
// SIDE EFFECT: Mutates object
public function complete(Order $order): void
{
$order->setStatus('completed');
$order->setCompletedAt(new DateTime());
}
// BETTER: Command pattern or event
public function complete(Order $order): OrderCompletedEvent
{
return new OrderCompletedEvent($order->getId(), new DateTimeImmutable());
}
// SIDE EFFECT: Reads global
function getUser(): ?User
{
global $currentUser;
return $currentUser;
}
// SIDE EFFECT: Superglobal access
public function handleRequest(): Response
{
$userId = $_SESSION['user_id']; // Hidden dependency
$data = $_POST; // Hidden input
}
// BETTER: Explicit parameters
public function handleRequest(Session $session, Request $request): Response
{
$userId = $session->get('user_id');
$data = $request->all();
}
// SIDE EFFECT: Static state
class Counter
{
private static int $count = 0;
public static function increment(): void
{
self::$count++; // Shared mutable state
}
public static function getCount(): int
{
return self::$count;
}
}
// BETTER: Instance state
class Counter
{
private int $count = 0;
public function increment(): void
{
$this->count++;
}
}
// SIDE EFFECT: Mixed concerns
public function calculateDiscount(int $userId): float
{
$user = $this->db->query("SELECT * FROM users WHERE id = ?", [$userId]); // I/O
$orderCount = $this->db->query("SELECT COUNT(*) FROM orders WHERE user_id = ?", [$userId]); // I/O
// Pure calculation
if ($orderCount > 100) return 0.20;
if ($orderCount > 50) return 0.15;
return 0.10;
}
// BETTER: Separated concerns
// Pure function
public function calculateDiscountRate(int $orderCount): float
{
return match(true) {
$orderCount > 100 => 0.20,
$orderCount > 50 => 0.15,
default => 0.10,
};
}
// I/O in service
public function getUserDiscountRate(int $userId): float
{
$orderCount = $this->orderRepository->countByUser($userId);
return $this->calculateDiscountRate($orderCount);
}
// SIDE EFFECT: Direct output
public function render(array $data): void
{
echo json_encode($data); // Side effect
header('Content-Type: application/json'); // Side effect
}
// BETTER: Return value
public function render(array $data): JsonResponse
{
return new JsonResponse($data);
}
// SIDE EFFECT: Non-deterministic
public function createToken(): Token
{
return new Token(
bin2hex(random_bytes(32)), // Random
new DateTime('+1 hour'), // Current time
);
}
// BETTER: Inject dependencies
public function createToken(
RandomGeneratorInterface $random,
ClockInterface $clock,
): Token {
return new Token(
bin2hex($random->bytes(32)),
$clock->now()->modify('+1 hour'),
);
}
// SIDE EFFECT: Logging in calculation
public function calculate(Order $order): Money
{
$this->logger->debug('Starting calculation'); // Side effect
$total = $this->computeTotal($order);
$this->metrics->increment('orders.calculated'); // Side effect
$this->logger->info('Calculated', ['total' => $total]); // Side effect
return $total;
}
// BETTER: Separate concerns
public function calculate(Order $order): Money
{
return $this->computeTotal($order);
}
// Decorate for logging
class LoggingOrderCalculator implements OrderCalculatorInterface
{
public function __construct(
private OrderCalculatorInterface $inner,
private LoggerInterface $logger,
) {}
public function calculate(Order $order): Money
{
$this->logger->debug('Starting calculation');
$total = $this->inner->calculate($order);
$this->logger->info('Calculated', ['total' => $total]);
return $total;
}
}
# Echo/print
Grep: "(echo|print)\s+" --glob "**/*.php"
# Header calls
Grep: "header\s*\(" --glob "**/*.php"
# Superglobals
Grep: "\$_(GET|POST|SESSION|COOKIE|SERVER|FILES)" --glob "**/*.php"
# Global keyword
Grep: "global\s+\\\$" --glob "**/*.php"
# Static property write
Grep: "(self|static)::\\\$\w+\s*[+\-*\/]?=" --glob "**/*.php"
# File operations
Grep: "(file_put_contents|fwrite|unlink|mkdir|chmod)\(" --glob "**/*.php"
| Side Effect | Severity |
|---|---|
| Superglobal access | 🟠 Major |
| Static state mutation | 🟠 Major |
| I/O mixed with logic | 🟠 Major |
| Input mutation | 🟡 Minor |
| Logging in pure logic | 🟢 Suggestion |
// Query: No side effects
public function getTotalPrice(Order $order): Money;
// Command: Side effects
public function completeOrder(Order $order): void;
// Before: Mutate
$order->applyDiscount($discount);
// After: Return new
$discountedOrder = $order->withDiscount($discount);
### Side Effect: [Description]
**Severity:** 🟠/🟡/🟢
**Location:** `file.php:line`
**Type:** [State Mutation|Global Access|I/O|...]
**Issue:**
Method mixes business logic with database I/O.
**Current:**
```php
public function calculateDiscount(int $userId): float
{
$user = $this->db->query(...); // I/O
return $this->computeDiscount($user); // Logic
}
Suggested:
public function calculateDiscount(User $user): float
{
return $this->computeDiscount($user); // Pure
}
Testing Impact:
npx claudepluginhub dykyi-roman/awesome-claude-code --plugin accAnalyzes PHP code for pure function patterns: detects side-effect-free methods, deterministic output, immutable inputs. Identifies refactors to improve testability.
Review PHP code using PhpStorm inspections. Use when editing PHP files, reviewing code quality, fixing PHP issues, or when asked about PHP best practices.
Detects hidden side effects, implicit state mutation, and inconsistent error handling in functions and modules. Activates when code surprises callers or debugging traces opaque state changes.