station · ivanmisic.net log in
§02 php-rules ↵ back to rules
Mine Rules

php-rules

A PHP 8.2+ .claude/rules pack: strict-types standards, migration-and-query database discipline, and the security calls that back the generic checklist.

The PHP layer that sits on top of the generic pack. Broad paths: globs (**/*.php), so it works whatever your directory layout is.

What's inside

  • php-standards.md: declare(strict_types), typed signatures, enums over string literals, match over switch, the disciplined @-suppression patterns, N+1 and TOCTOU, no var_dump in commits.
  • database.md: migrations with UP and DOWN, model or repository access only, prepared statements, the LIMIT and OFFSET cast, column allow-lists, indexing.
  • php-security.md: filter_var, htmlspecialchars, password_verify and password_hash, hash_equals, JSON hex flags, realpath path containment, CRLF-safe headers, the SVG-upload ban, the dangerous-function list.

How to use

Download and drop the .md files into .claude/rules/ alongside the generic pack. They load whenever you touch a .php file; database.md also loads on .sql. The generic security-discipline file states the principle; php-security.md names the call that satisfies it.

Want these tuned to your actual codebase? Run /a-rules-optimizer from the dev-workflow-forge plugin.

What's in the bundle 3 files
database.md 1.5 KB
---
paths:
  - "**/*.php"
  - "**/*.sql"
---

# Database Standards

## Migrations (mandatory)
- Every schema change gets a migration file: creating tables, adding or removing columns, changing types, adding or removing indexes or constraints.
- Number migrations in sequence and include both an UP and a DOWN section so rollback is possible.
- Use `IF EXISTS` and `IF NOT EXISTS` so a migration is idempotent.
- Test the DOWN section locally before deploying. A migration you can't roll back is a one-way door.

## Access Pattern
- All database access goes through a model or repository layer. No raw SQL in controllers or views.
- Guard large TEXT and content fields behind the tooling that handles diffing and cache invalidation; don't hand-edit them with ad-hoc UPDATE statements.

## Query Safety
- Prepared statements for every query that touches a variable: `$stmt->execute(['id' => $id])`.
- LIMIT and OFFSET can't be bound as parameters in PDO. Cast to int and validate the range, then concatenate. Never interpolate a raw request value.
- Define an allow-list of columns a query may filter or order by, and reject anything outside it. Never build a WHERE or ORDER BY clause from an unvalidated field name.

## Conventions
- Tables plural, `snake_case`. Primary key `id`. Foreign keys `{table}_id`. Timestamps `created_at` and `updated_at`. Soft delete `deleted_at` (nullable).

## Indexing
- Index the columns you look up, filter, join, and order by: foreign keys, slugs, status, and any dated sort column. Add composite indexes for the multi-column filters you actually run.
php-security.md 2.2 KB
---
paths:
  - "**/*.php"
---

# PHP Security

The PHP enforcement of the generic security discipline. Read the generic `security-discipline` rules first; this file names the PHP calls that satisfy them.

## Input Validation
- Use `filter_var()` for email and URL validation.
- Only allow `http://` and `https://` URL schemes; reject the rest.
- Path containment: any filesystem path built from a request or DB value must resolve with `realpath()` and be rejected (404) unless the result is prefixed by the realpath'd base directory plus a separator, checked before you open the file.

## Output Escaping
- Escape every dynamic value in a view with `htmlspecialchars($v, ENT_QUOTES, 'UTF-8')` or your framework's escape helper. Never echo raw input.
- For JSON responses, use `JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_SLASHES` so a value can't break out of an inline `<script>`.
- When interpolating a DB-sourced filename or slug into an HTTP header (`Content-Disposition`), strip CR and LF and escape quotes.

## Credentials and Comparison
- Verify passwords with `password_verify()` against a `password_hash()` bcrypt or argon2 digest. Never md5, sha1, or plaintext.
- Compare API keys, tokens, signatures, CSRF validators, and OAuth `state` with `hash_equals()` (trusted value first, non-empty guarded), never `==` or `===`.

## CSRF and Sessions
- CSRF token on every state-changing form and on JSON or AJAX POSTs; validate before acting on the body.
- Session cookies: httponly, secure, SameSite=Lax; regenerate the session id on login; set a timeout.

## File Uploads
- Validate MIME, size, and extension; sanitize the filename; store under a generated unique name.
- Do not accept SVG uploads. A MIME and extension check does not stop a weaponized SVG (`<script>`, `on*=`, `javascript:` hrefs).

## Concurrency
- `file_put_contents()` to shared state must pass `LOCK_EX`. Operate directly and handle the error rather than check-then-act.

## Dangerous Functions
- Avoid `extract`, `unserialize`, `eval`, `shell_exec`, `system`, `exec`, `passthru`, `proc_open`. If one is genuinely required, whitelist the binary with an absolute path, pass arguments through `escapeshellarg()`, and document why.
php-standards.md 1.8 KB
---
paths:
  - "**/*.php"
---

# PHP Standards

Layers on the generic code-quality and architecture rules; this file is the PHP-specific enforcement.

## PHP 8.2+
- `declare(strict_types=1);` at the top of every file.
- Type declarations on every function: parameters and return types.
- Enums for fixed sets of constants, not loose string literals.
- When a domain enum exists, compare against the enum case (or its `->value`), not a raw string literal. A raw `=== 'archived'` in a file whose siblings use the enum is the tell.
- `match` over `switch` for value mapping.
- Constructor property promotion.

## Naming
- Classes `PascalCase`, methods and variables `camelCase`, constants `UPPER_SNAKE_CASE`, DB columns `snake_case`.

## Error Suppression
The `@` operator is allowed only with explicit intent, shown one of these ways:
- A sibling `// reason: ...` comment explaining why the failure is safe to swallow.
- A check that distinguishes success from failure: `if (!@unlink($f) && file_exists($f)) { /* handle */ }`.
- An idempotent mkdir with a recheck: `@mkdir($d, 0700, true); if (!is_dir($d)) { throw ...; }`.

A bare `@`-suppressed call with none of the above is rejected in review.

## Efficiency
- Batch-load before a loop; never query inside a loop (N+1).
- Operate directly and handle the error instead of check-then-act (TOCTOU).
- Guard updates in loops with change detection; avoid unconditional writes.
- Read only the columns and rows you need.

## Complexity
- Files over ~500 lines and methods over ~30 lines are hot spots. Shrink, don't grow, an already-large file.
- Strip unused `use` imports as you edit. If a class is referenced only in a docblock, use a fully-qualified name there and drop the `use`.

## Debug Output
- No `var_dump()`, `print_r()`, or `dd()` in committed code. Use logging. Keep throwaway experiments out of the committed tree.

Everything mine on this shelf comes from my own working setup, shared for learning. Test it and adapt it to your project before relying on it; you run it at your own risk.