# Write PHP Code

> Use whenever a task writes or modifies PHP code, including PHP files, embedded PHP, tests, scripts, endpoints, and PHP code snippets. Do not activate only because a non-PHP task occurs in a project that contains PHP.

- Skill: `legendarylinux/write-php-code` (Agent Skill)
- Install (CLI): `npx skillmds@latest add legendarylinux/write-php-code`
- Raw SKILL.md: https://api.skillmd.com/api/skills/legendarylinux/write-php-code/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: LegendaryLinux (https://skillmd.com/u/legendarylinux)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/legendarylinux/write-php-code

---


# Write PHP Code

Use this skill whenever PHP code is written or changed. Apply these conventions to new PHP code and to lines touched
by the task. Preserve unrelated code and follow an explicit user instruction when it conflicts with this skill.

## General Rules

- Match the existing file shape: namespaced library class, procedural AJAX endpoint, procedural CLI script, or
  front controller. Do not introduce a class into procedural code without a task-driven reason.
- Keep lines at or under 120 characters when practical.
- Use tabs for indentation, LF line endings, UTF-8, no trailing whitespace, and a final newline.
- Begin with `<?php` and omit `?>` from pure PHP files.
- End statements with semicolons.
- Do not add `declare(strict_types=1);` unless the user or local project conventions require it.
- Use single quotes for fixed non-SQL strings, double quotes for interpolation, and double quotes for SQL.
- Preserve nearby legacy style outside touched code and avoid unrelated formatting churn.

## Files, Includes, And Imports

- Put `namespace` immediately after `<?php` in a namespaced class file.
- Put procedural `require_once(...)` statements before `use` imports.
- Use `__DIR__` or `dirname(__DIR__)` for local paths and compact concatenation around the path.
- Do not change Composer autoloading as an incidental style edit.
- Namespace reusable library classes, but do not namespace one-off endpoints or scripts unless their directory does.

```php
<?php
require_once(dirname(__DIR__).'/lib/RequestAuth.php');
require_once(dirname(__DIR__).'/lib/ResponseHandler.php');

use Vero\RequestAuth;
use Vero\ResponseHandler;
```

## Classes, Functions, And Names

- Use PascalCase class names and camelCase functions, methods, and variables.
- Use descriptive names and `Id`, not `ID`, inside camelCase names.
- Use `$db` for PDO connections, `$query` for prepared statements, and `$sql` for SQL strings.
- Use `$T` or `$E` for caught `Throwable` or `Exception` when that matches local code.
- Keep opening braces on the declaration or control-flow line with no space before `{`.
- Use compact return types such as `function fetch():array{`.
- Prefer explicit method visibility and clear parameter and return types in new code.
- Do not force types into legacy code when that would require broad cleanup.
- Use `final class FooTest extends TestCase{` for PHPUnit tests.
- A class constant may use `CONST` when required by established local style.

```php
class ResponseHandler{
	public static function sendJsonResponse(array $data, int $statusCode=200):void{
		$jsonData = json_encode($data);
		http_response_code($statusCode);
		print $jsonData;
	}
}
```

## Spacing, Blocks, And Values

- Use compact control statements and exception blocks: `if(...)`, `foreach(...)`, `try{`, and
  `}catch(Throwable $T){`.
- Keep `else`, `catch`, and `finally` on the same line as the preceding closing brace.
- Do not put spaces inside call parentheses or array brackets.
- Do not put spaces around concatenation dots.
- Put spaces around assignment and comparison operators and after commas.
- Put a space after a cast: `(int) $value`.
- Prefer early guards to deep nesting. A short, obvious one-line guard is acceptable.
- Indent `case` labels one tab inside `switch` and their contents one additional tab.
- Use short array syntax, trailing commas in multiline arrays, and one entry per line for larger arrays.
- Use `isset()` when `0`, `'0'`, or `false` is valid. Use `empty()` only when all empty values are invalid.
- Use comments sparingly for operational intent or non-obvious behavior.

## Secure Request Handling: Deny By Default

Treat every request property as untrusted. A request is denied unless its transport, route, identity, authorization,
and input all match an explicitly supported operation. Complete all applicable checks before database writes, file
writes, external calls, or other side effects.

### Request Gate

- Accept only the HTTP methods an endpoint explicitly supports. Return `405` and an `Allow` header otherwise.
- Accept only expected content types. Return `415` for unsupported media types.
- Enforce request-size limits before decoding large bodies. Prefer web-server limits plus an application check when
  the endpoint needs a lower bound.
- Read each value from one intended source. Do not use `$_REQUEST` or silently merge query, body, and cookie data.
- For JSON, require a JSON content type, decode once with exceptions enabled, require the expected top-level type,
  and reject malformed JSON. Do not reinterpret malformed JSON as an empty form post.
- Establish the session and authenticate before protected work.
- Validate CSRF protection and permitted origin for cookie-authenticated state-changing requests.
- Fail closed if an authentication, CSRF, permission, tenant-scope, or other security check cannot complete.
- Apply CORS narrowly. Do not reflect arbitrary origins or combine wildcard origins with credentials.

### Explicit Routing And Authorization

- Require `action` to be a scalar string before routing.
- Route through a fixed `switch` or an equivalent literal allowlist.
- Use POST for mutations and GET only for safe, read-only operations. Keep separate allowlists if both are supported.
- Never turn request data into a function, class, method, include path, table, column, or filesystem path.
- Never pass request data to `eval()`, deserialize it with `unserialize()`, or construct a shell command from it.
- The `default` branch must reject unknown actions. It must never fall through to a permissive operation.
- Check authorization for the specific action and resource, even when the endpoint has a broader permission check.
- Prevent cross-tenant and insecure direct-object access by constraining resource lookup to the authenticated user's
  permitted tenant, account, or ownership scope.
- Perform action-specific permission checks before revealing whether a protected record exists when that distinction
  would expose information.

### Input Allowlisting

- Define the required and optional keys for each action. Reject unknown keys instead of passing them downstream.
- Validate type before casting, then validate allowed enum values, length, range, format, and cross-field rules.
- Treat client-side validation only as a usability feature; repeat all enforcement on the server.
- Allowlist dynamic SQL identifiers and sort directions using server-owned mappings. Values always use parameters.
- For uploaded files, enforce server-side size and count limits, inspect content rather than trusting MIME headers or
  extensions, generate storage names, and store outside the web root unless public access is intentional.
- For user-supplied URLs, restrict schemes and destinations and block private or link-local targets unless the
  endpoint explicitly requires them.

### Data, Output, And Failure Safety

- Use prepared PDO statements and bound parameters for all untrusted values. Never concatenate request data into SQL.
- Use transactions when a multi-step mutation must be atomic and roll back on failure.
- Select only fields needed for the response; do not serialize database rows or objects indiscriminately.
- Encode output for its destination. Use the JSON response helper for JSON and HTML escaping for HTML text.
- Return generic client errors. Do not expose stack traces, SQL, filesystem paths, secrets, tokens, or internal
  exception messages.
- Log security-relevant denials and server failures with enough context to investigate, but never log passwords,
  session identifiers, CSRF tokens, authorization headers, or unnecessary personal data.
- Use `password_hash()` and `password_verify()` for passwords, `random_bytes()` for security tokens, and
  `hash_equals()` when comparing secret values outside an API that already performs constant-time comparison.
- Keep secrets out of source code and responses. Load them through the application's approved secret/configuration
  mechanism and rotate them when exposure is suspected.
- Use `400` for malformed input, `401` for missing or invalid authentication, `403` for denied authorization or
  CSRF/origin checks, `404` for permitted but absent resources, `405` for methods, `409` for conflicts, `415` for
  media types, `422` when the local API uses it for semantic validation, and `500` for unexpected failures.
- Keep production error display disabled and send security headers at the web server or shared middleware layer.
- Add rate or abuse limits to authentication, recovery, expensive search, upload, and other abuse-prone endpoints
  when the surrounding application provides that facility.

### Deny-By-Default Endpoint Shape

Use the project's response, session, CSRF, database, permission, and audit helpers. The following demonstrates the
control-flow invariant; adapt helper names and expected fields to the application.

```php
if($_SERVER['REQUEST_METHOD'] !== 'POST'){
	header('Allow: POST');
	ResponseHandler::sendJsonResponse(['fail_reason' => 'method not allowed'], 405);
	exit(0);
}

if(!Fidelis\Session::Start()){
	ResponseHandler::sendJsonResponse(['fail_reason' => 'authentication required'], 401);
	exit(0);
}

if(!CSRFChecker::isRequestValid()){
	ResponseHandler::sendJsonResponse(['fail_reason' => 'forbidden'], 403);
	exit(0);
}

if(!isset($_POST['action']) || !is_string($_POST['action'])){
	ResponseHandler::sendJsonResponse(['fail_reason' => 'valid action is required'], 400);
	exit(0);
}

switch($_POST['action']){
	case 'updateProvider':
		$allowedKeys = ['action', 'id', 'name'];
		$unknownKeys = array_diff(array_keys($_POST), $allowedKeys);
		if(!empty($unknownKeys) || !isset($_POST['id'], $_POST['name']) ||
			!is_string($_POST['id']) || !ctype_digit($_POST['id']) ||
			!is_string($_POST['name']) || trim($_POST['name']) === ''){
			ResponseHandler::sendJsonResponse(['fail_reason' => 'invalid request'], 400);
			break;
		}

		if(!$permissions->mayUpdateProvider((int) $_POST['id'])){
			ResponseHandler::sendJsonResponse(['fail_reason' => 'forbidden'], 403);
			break;
		}

		$query = $db->prepare("UPDATE accounting.providers SET name=:name WHERE id=:id");
		$query->bindValue(':name', trim($_POST['name']), PDO::PARAM_STR);
		$query->bindValue(':id', (int) $_POST['id'], PDO::PARAM_INT);
		$query->execute();
		ResponseHandler::sendJsonResponse([]);
		break;

	default:
		ResponseHandler::sendJsonResponse(['fail_reason' => 'unknown action requested'], 400);
		break;
}
```

## AJAX Endpoint Order

Use this order when the application uses procedural action-based endpoints:

1. Require dependencies and import classes.
2. Install centralized exception handling.
3. Enforce method, content type, and body-size requirements.
4. Start the session and enforce authentication.
5. Validate CSRF, origin, and endpoint-wide permissions.
6. Decode the one supported request format and validate `action`.
7. Connect shared resources such as the database, audit log, and permission helper.
8. Route only literal supported actions.
9. Validate action fields and action/resource authorization inside each case.
10. Perform the operation, audit it, send exactly one response, and `break`.
11. Reject everything else in `default`.
12. Put small endpoint helpers after the routing block.

Use the local JSON response helper with a `fail_reason` for errors. Uncaught handlers should log the exception
server-side, return a generic `500`, and exit nonzero.

## Database And SQL

- Use the application's established connection helper.
- Use `$db->prepare($sql)` with named placeholders for raw PDO SQL.
- Use `bindParam()` for an existing variable bound by reference and `bindValue()` for a cast or expression.
- Bind integer identifiers and flags with `PDO::PARAM_INT`.
- Use `fetch(PDO::FETCH_ASSOC)` for one row, `fetchAll(PDO::FETCH_ASSOC)` for row lists,
  `fetchAll(PDO::FETCH_COLUMN)` for scalar lists, and `fetchColumn()` for a scalar.
- Prefer `?: []` when a list response must always be an array.
- Keep short SQL on one line. Format complex SQL as readable multiline strings with uppercase keywords.
- Qualify tables and follow existing alias conventions where the local schema does.

## CLI Scripts

- Keep CLI scripts direct and procedural.
- Use `getopt()` for reusable option-driven scripts and `$argv` for simple one-off scripts.
- Validate arguments before work, print usage for help or invalid invocation, and use a nonzero exit for failures.
- Print progress with `print` and `PHP_EOL`.
- Wrap database-changing scripts in `try{...}catch(Throwable $T){...}`.
- Use a transaction when a batch must commit or roll back as one unit.

## Tests And Verification

- Put PHPUnit tests under `tests/`, use public `testSomething` methods, and add useful assertion messages in loops.
- Test every supported action and the deny paths: wrong method/content type, missing or invalid action, unknown fields,
  malformed values, unauthenticated access, CSRF failure, insufficient permissions, and cross-scope identifiers.
- Run the narrowest relevant PHP syntax check, static analysis, security checks, and tests available.
- Recheck tab indentation, compact braces, prepared statements, output handling, and the 120-character target.
- Report checks that could not be run; do not claim unperformed verification.

