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
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.
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.
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:
- Require dependencies and import classes.
- Install centralized exception handling.
- Enforce method, content type, and body-size requirements.
- Start the session and enforce authentication.
- Validate CSRF, origin, and endpoint-wide permissions.
- Decode the one supported request format and validate
action.
- Connect shared resources such as the database, audit log, and permission helper.
- Route only literal supported actions.
- Validate action fields and action/resource authorization inside each case.
- Perform the operation, audit it, send exactly one response, and
break.
- Reject everything else in
default.
- 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.
1---2name: write-php-code3description: 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.4---56# Write PHP Code78Use this skill whenever PHP code is written or changed. Apply these conventions to new PHP code and to lines touched9by the task. Preserve unrelated code and follow an explicit user instruction when it conflicts with this skill.1011## General Rules1213- Match the existing file shape: namespaced library class, procedural AJAX endpoint, procedural CLI script, or14 front controller. Do not introduce a class into procedural code without a task-driven reason.15- Keep lines at or under 120 characters when practical.16- Use tabs for indentation, LF line endings, UTF-8, no trailing whitespace, and a final newline.17- Begin with `<?php` and omit `?>` from pure PHP files.18- End statements with semicolons.19- Do not add `declare(strict_types=1);` unless the user or local project conventions require it.20- Use single quotes for fixed non-SQL strings, double quotes for interpolation, and double quotes for SQL.21- Preserve nearby legacy style outside touched code and avoid unrelated formatting churn.2223## Files, Includes, And Imports2425- Put `namespace` immediately after `<?php` in a namespaced class file.26- Put procedural `require_once(...)` statements before `use` imports.27- Use `__DIR__` or `dirname(__DIR__)` for local paths and compact concatenation around the path.28- Do not change Composer autoloading as an incidental style edit.29- Namespace reusable library classes, but do not namespace one-off endpoints or scripts unless their directory does.3031```php32<?php33require_once(dirname(__DIR__).'/lib/RequestAuth.php');34require_once(dirname(__DIR__).'/lib/ResponseHandler.php');3536use Vero\RequestAuth;37use Vero\ResponseHandler;38```3940## Classes, Functions, And Names4142- Use PascalCase class names and camelCase functions, methods, and variables.43- Use descriptive names and `Id`, not `ID`, inside camelCase names.44- Use `$db` for PDO connections, `$query` for prepared statements, and `$sql` for SQL strings.45- Use `$T` or `$E` for caught `Throwable` or `Exception` when that matches local code.46- Keep opening braces on the declaration or control-flow line with no space before `{`.47- Use compact return types such as `function fetch():array{`.48- Prefer explicit method visibility and clear parameter and return types in new code.49- Do not force types into legacy code when that would require broad cleanup.50- Use `final class FooTest extends TestCase{` for PHPUnit tests.51- A class constant may use `CONST` when required by established local style.5253```php54class ResponseHandler{55 public static function sendJsonResponse(array $data, int $statusCode=200):void{56 $jsonData = json_encode($data);57 http_response_code($statusCode);58 print $jsonData;59 }60}61```6263## Spacing, Blocks, And Values6465- Use compact control statements and exception blocks: `if(...)`, `foreach(...)`, `try{`, and66 `}catch(Throwable $T){`.67- Keep `else`, `catch`, and `finally` on the same line as the preceding closing brace.68- Do not put spaces inside call parentheses or array brackets.69- Do not put spaces around concatenation dots.70- Put spaces around assignment and comparison operators and after commas.71- Put a space after a cast: `(int) $value`.72- Prefer early guards to deep nesting. A short, obvious one-line guard is acceptable.73- Indent `case` labels one tab inside `switch` and their contents one additional tab.74- Use short array syntax, trailing commas in multiline arrays, and one entry per line for larger arrays.75- Use `isset()` when `0`, `'0'`, or `false` is valid. Use `empty()` only when all empty values are invalid.76- Use comments sparingly for operational intent or non-obvious behavior.7778## Secure Request Handling: Deny By Default7980Treat every request property as untrusted. A request is denied unless its transport, route, identity, authorization,81and input all match an explicitly supported operation. Complete all applicable checks before database writes, file82writes, external calls, or other side effects.8384### Request Gate8586- Accept only the HTTP methods an endpoint explicitly supports. Return `405` and an `Allow` header otherwise.87- Accept only expected content types. Return `415` for unsupported media types.88- Enforce request-size limits before decoding large bodies. Prefer web-server limits plus an application check when89 the endpoint needs a lower bound.90- Read each value from one intended source. Do not use `$_REQUEST` or silently merge query, body, and cookie data.91- For JSON, require a JSON content type, decode once with exceptions enabled, require the expected top-level type,92 and reject malformed JSON. Do not reinterpret malformed JSON as an empty form post.93- Establish the session and authenticate before protected work.94- Validate CSRF protection and permitted origin for cookie-authenticated state-changing requests.95- Fail closed if an authentication, CSRF, permission, tenant-scope, or other security check cannot complete.96- Apply CORS narrowly. Do not reflect arbitrary origins or combine wildcard origins with credentials.9798### Explicit Routing And Authorization99100- Require `action` to be a scalar string before routing.101- Route through a fixed `switch` or an equivalent literal allowlist.102- Use POST for mutations and GET only for safe, read-only operations. Keep separate allowlists if both are supported.103- Never turn request data into a function, class, method, include path, table, column, or filesystem path.104- Never pass request data to `eval()`, deserialize it with `unserialize()`, or construct a shell command from it.105- The `default` branch must reject unknown actions. It must never fall through to a permissive operation.106- Check authorization for the specific action and resource, even when the endpoint has a broader permission check.107- Prevent cross-tenant and insecure direct-object access by constraining resource lookup to the authenticated user's108 permitted tenant, account, or ownership scope.109- Perform action-specific permission checks before revealing whether a protected record exists when that distinction110 would expose information.111112### Input Allowlisting113114- Define the required and optional keys for each action. Reject unknown keys instead of passing them downstream.115- Validate type before casting, then validate allowed enum values, length, range, format, and cross-field rules.116- Treat client-side validation only as a usability feature; repeat all enforcement on the server.117- Allowlist dynamic SQL identifiers and sort directions using server-owned mappings. Values always use parameters.118- For uploaded files, enforce server-side size and count limits, inspect content rather than trusting MIME headers or119 extensions, generate storage names, and store outside the web root unless public access is intentional.120- For user-supplied URLs, restrict schemes and destinations and block private or link-local targets unless the121 endpoint explicitly requires them.122123### Data, Output, And Failure Safety124125- Use prepared PDO statements and bound parameters for all untrusted values. Never concatenate request data into SQL.126- Use transactions when a multi-step mutation must be atomic and roll back on failure.127- Select only fields needed for the response; do not serialize database rows or objects indiscriminately.128- Encode output for its destination. Use the JSON response helper for JSON and HTML escaping for HTML text.129- Return generic client errors. Do not expose stack traces, SQL, filesystem paths, secrets, tokens, or internal130 exception messages.131- Log security-relevant denials and server failures with enough context to investigate, but never log passwords,132 session identifiers, CSRF tokens, authorization headers, or unnecessary personal data.133- Use `password_hash()` and `password_verify()` for passwords, `random_bytes()` for security tokens, and134 `hash_equals()` when comparing secret values outside an API that already performs constant-time comparison.135- Keep secrets out of source code and responses. Load them through the application's approved secret/configuration136 mechanism and rotate them when exposure is suspected.137- Use `400` for malformed input, `401` for missing or invalid authentication, `403` for denied authorization or138 CSRF/origin checks, `404` for permitted but absent resources, `405` for methods, `409` for conflicts, `415` for139 media types, `422` when the local API uses it for semantic validation, and `500` for unexpected failures.140- Keep production error display disabled and send security headers at the web server or shared middleware layer.141- Add rate or abuse limits to authentication, recovery, expensive search, upload, and other abuse-prone endpoints142 when the surrounding application provides that facility.143144### Deny-By-Default Endpoint Shape145146Use the project's response, session, CSRF, database, permission, and audit helpers. The following demonstrates the147control-flow invariant; adapt helper names and expected fields to the application.148149```php150if($_SERVER['REQUEST_METHOD'] !== 'POST'){151 header('Allow: POST');152 ResponseHandler::sendJsonResponse(['fail_reason' => 'method not allowed'], 405);153 exit(0);154}155156if(!Fidelis\Session::Start()){157 ResponseHandler::sendJsonResponse(['fail_reason' => 'authentication required'], 401);158 exit(0);159}160161if(!CSRFChecker::isRequestValid()){162 ResponseHandler::sendJsonResponse(['fail_reason' => 'forbidden'], 403);163 exit(0);164}165166if(!isset($_POST['action']) || !is_string($_POST['action'])){167 ResponseHandler::sendJsonResponse(['fail_reason' => 'valid action is required'], 400);168 exit(0);169}170171switch($_POST['action']){172 case 'updateProvider':173 $allowedKeys = ['action', 'id', 'name'];174 $unknownKeys = array_diff(array_keys($_POST), $allowedKeys);175 if(!empty($unknownKeys) || !isset($_POST['id'], $_POST['name']) ||176 !is_string($_POST['id']) || !ctype_digit($_POST['id']) ||177 !is_string($_POST['name']) || trim($_POST['name']) === ''){178 ResponseHandler::sendJsonResponse(['fail_reason' => 'invalid request'], 400);179 break;180 }181182 if(!$permissions->mayUpdateProvider((int) $_POST['id'])){183 ResponseHandler::sendJsonResponse(['fail_reason' => 'forbidden'], 403);184 break;185 }186187 $query = $db->prepare("UPDATE accounting.providers SET name=:name WHERE id=:id");188 $query->bindValue(':name', trim($_POST['name']), PDO::PARAM_STR);189 $query->bindValue(':id', (int) $_POST['id'], PDO::PARAM_INT);190 $query->execute();191 ResponseHandler::sendJsonResponse([]);192 break;193194 default:195 ResponseHandler::sendJsonResponse(['fail_reason' => 'unknown action requested'], 400);196 break;197}198```199200## AJAX Endpoint Order201202Use this order when the application uses procedural action-based endpoints:2032041. Require dependencies and import classes.2052. Install centralized exception handling.2063. Enforce method, content type, and body-size requirements.2074. Start the session and enforce authentication.2085. Validate CSRF, origin, and endpoint-wide permissions.2096. Decode the one supported request format and validate `action`.2107. Connect shared resources such as the database, audit log, and permission helper.2118. Route only literal supported actions.2129. Validate action fields and action/resource authorization inside each case.21310. Perform the operation, audit it, send exactly one response, and `break`.21411. Reject everything else in `default`.21512. Put small endpoint helpers after the routing block.216217Use the local JSON response helper with a `fail_reason` for errors. Uncaught handlers should log the exception218server-side, return a generic `500`, and exit nonzero.219220## Database And SQL221222- Use the application's established connection helper.223- Use `$db->prepare($sql)` with named placeholders for raw PDO SQL.224- Use `bindParam()` for an existing variable bound by reference and `bindValue()` for a cast or expression.225- Bind integer identifiers and flags with `PDO::PARAM_INT`.226- Use `fetch(PDO::FETCH_ASSOC)` for one row, `fetchAll(PDO::FETCH_ASSOC)` for row lists,227 `fetchAll(PDO::FETCH_COLUMN)` for scalar lists, and `fetchColumn()` for a scalar.228- Prefer `?: []` when a list response must always be an array.229- Keep short SQL on one line. Format complex SQL as readable multiline strings with uppercase keywords.230- Qualify tables and follow existing alias conventions where the local schema does.231232## CLI Scripts233234- Keep CLI scripts direct and procedural.235- Use `getopt()` for reusable option-driven scripts and `$argv` for simple one-off scripts.236- Validate arguments before work, print usage for help or invalid invocation, and use a nonzero exit for failures.237- Print progress with `print` and `PHP_EOL`.238- Wrap database-changing scripts in `try{...}catch(Throwable $T){...}`.239- Use a transaction when a batch must commit or roll back as one unit.240241## Tests And Verification242243- Put PHPUnit tests under `tests/`, use public `testSomething` methods, and add useful assertion messages in loops.244- Test every supported action and the deny paths: wrong method/content type, missing or invalid action, unknown fields,245 malformed values, unauthenticated access, CSRF failure, insufficient permissions, and cross-scope identifiers.246- Run the narrowest relevant PHP syntax check, static analysis, security checks, and tests available.247- Recheck tab indentation, compact braces, prepared statements, output handling, and the 120-character target.248- Report checks that could not be run; do not claim unperformed verification.