# PHP rules

Follow these CodeWiki-derived rules when you work in this project.

- `array_filter()` preserves integer keys.
  Why: Filtering `[10, 20, 30]` can produce `[1 => 20, 2 => 30]`, which encodes to a JSON object.
  Source: [Array functions](https://codewiki.com/php/array-functions/)
- Do not assume this is safe: omitting the callback removes every empty value, not just `null` and empty strings.
  Why: Integer `0` and string `"0"` disappear too.
  Source: [Array functions](https://codewiki.com/php/array-functions/)
- Do not assume this is safe: without an initial value, PHP passes `null` to the first callback.
  Why: It doesn't adopt the first array element as JavaScript does.
  Source: [Array functions](https://codewiki.com/php/array-functions/)
- `array_merge()` resolves conflicts only at the top level.
  Why: A right-side nested array replaces the left-side value, while `array_merge_recursive()` can collect conflicting scalar values into arrays.
  Source: [Array functions](https://codewiki.com/php/array-functions/)
- `ARRAY_FILTER_USE_BOTH` passes value, then key.
  Why: A `usort()` comparator receives two values and must return an integer. Generated code often reverses the filter parameters or returns a Boolean from the comparator.
  Source: [Array functions](https://codewiki.com/php/array-functions/)
- `usort()` takes the array by reference, changes the variable, and replaces both string and integer keys with sequential indexes.
  Source: [Array functions](https://codewiki.com/php/array-functions/)
- `isset($row['email'])` returns `false` both when the key is missing and when its value is `null`.
  Why: If `null` is a valid state, the test loses business information.
  Source: [Arrays](https://codewiki.com/php/arrays/)
- Do not assume this is safe: removing the middle of a list does not move later keys.
  Why: Indexing from `0` through `count($items) - 1` may read a missing key and skip a higher key that still exists.
  Source: [Arrays](https://codewiki.com/php/arrays/)
- An external key `1` and string key `'1'` normalize to the same integer key.
  Why: The later value overwrites the earlier one; the array does not preserve two superficially different inputs.
  Source: [Arrays](https://codewiki.com/php/arrays/)
- After `foreach ($items as &$item)`, `$item` still refers to the final element.
  Why: A later ordinary assignment or reuse of the same loop variable can silently corrupt that element.
  Source: [Arrays](https://codewiki.com/php/arrays/)
- Do not assume this is safe: `$copy = $original` makes the two array structures independent, but it does not automatically clone objects stored inside.
  Why: Mutating a shared object through either array remains visible through the other.
  Source: [Arrays](https://codewiki.com/php/arrays/)
- Do not assume this is safe: `(int)` guarantees an integer result, but not that the original value was a complete, valid integer in the business range.
  Why: Missing values and some invalid inputs can therefore become a plausible `0`.
  Source: [Data types](https://codewiki.com/php/data-types/)
- Strict mode controls checks at declared positions.
  Why: It does not parse query parameters into integers or validate array keys, string formats, or numeric ranges.
  Source: [Data types](https://codewiki.com/php/data-types/)
- `==` and `empty()` place several representations on the same branch.
  Why: String `'0'`, integer `0`, `null`, and the empty string may mean entirely different things in the domain.
  Source: [Data types](https://codewiki.com/php/data-types/)
- `array $orders` checks only that the outer value is an array.
  Why: Missing keys, wrong element types, and list-versus-map shape can all pass the runtime declaration.
  Source: [Data types](https://codewiki.com/php/data-types/)
- Do not assume this is safe: `$copy = $original` copies an object handle, not the instance.
  Why: A property change through either variable is visible through the other variable because both refer to the same object.
  Source: [Data types](https://codewiki.com/php/data-types/)
- Do not assume this is safe: a database or HTTP request containing `'paid'` has not produced `OrderStatus::Paid`.
  Why: Passing that string directly to an `OrderStatus` parameter throws `TypeError`.
  Source: [Enums](https://codewiki.com/php/enums/)
- `OrderStatus::tryFrom($raw) ??
  Why: OrderStatus::Created` interprets missing input, a typo, and an unknown new value as "created."
  Source: [Enums](https://codewiki.com/php/enums/)
- For `case Paid = 'paid'`, `name` is `'Paid'`, while `value` is `'paid'`.
  Why: `from()` and `tryFrom()` look up only the latter.
  Source: [Enums](https://codewiki.com/php/enums/)
- Adding `default` to every `match` hides missing branches.
  Why: After a case is added, old logic keeps running but may choose a completely wrong business branch.
  Source: [Enums](https://codewiki.com/php/enums/)
- Enums cannot be extended or receive cases at runtime.
  Why: Modeling third-party payment providers as one enum forces the core package to release a version for every plugin.
  Source: [Enums](https://codewiki.com/php/enums/)
- Changing a backing value, case name, or enum class name can break existing database rows, messages, caches, and PHP-serialized data.
  Source: [Enums](https://codewiki.com/php/enums/)
- `E_ALL` is a diagnostic bit mask.
  Why: It does not make `set_error_handler()` receive `Error` or `Exception` objects, and it does not let a user handler take over every startup, parse, and compile-time termination.
  Source: [Error handling](https://codewiki.com/php/error-handling/)
- Do not assume this is safe: after a handler returns `true`, PHP's default handler does not continue.
  Why: If the custom handler writes to one fallible logging destination, the diagnostic may disappear completely.
  Source: [Error handling](https://codewiki.com/php/error-handling/)
- Do not assume this is safe: the `@` error-suppression operator does not guarantee that a custom callback will never run.
  Why: Throwing `ErrorException` unconditionally can make deliberately suppressed probe code fail unexpectedly.
  Source: [Error handling](https://codewiki.com/php/error-handling/)
- An error handler is mutable process-wide state.
  Why: When a library function or test installs one without restoring it, later requests, tests, and framework code get different control flow.
  Source: [Error handling](https://codewiki.com/php/error-handling/)
- Error messages and stacks can contain absolute paths, SQL, internal class names, request data, or credentials.
  Why: Returning `$error->getMessage()` directly turns a diagnostic boundary into an information leak.
  Source: [Error handling](https://codewiki.com/php/error-handling/)
- `catch (Exception $error)` cannot receive `TypeError`, `ValueError`, or other `Error` subclasses.
  Why: Generated code often leaves boundary cleanup or a unified response uncovered for this reason.
  Source: [Exceptions](https://codewiki.com/php/exceptions/)
- Do not assume this is safe: an empty `catch`, or one that only logs and continues, lets later code run after a precondition has failed.
  Why: The eventual data damage may occur far from the real throw site.
  Source: [Exceptions](https://codewiki.com/php/exceptions/)
- Do not assume this is safe: `throw new RepositoryException('Query failed')` loses the programmatic cause relationship.
  Why: Copying the old message into the new one still doesn't preserve types or stacks for reliable traversal.
  Source: [Exceptions](https://codewiki.com/php/exceptions/)
- A `return` in `finally` replaces the pending return value from `try` or `catch` and can swallow an exception already in flight.
  Why: Code that looks like cleanup then changes the business result.
  Source: [Exceptions](https://codewiki.com/php/exceptions/)
- Do not assume this is safe: throwing to end a search loop, or treating every "not found" result as an exception, disguises an expected branch as a failure.
  Why: It also makes the normal result set harder to see from the signature.
  Source: [Exceptions](https://codewiki.com/php/exceptions/)
- Writing `$error->getMessage()`, file paths, or a stack trace into an HTTP response exposes implementation detail and may also expose queries or personal data.
  Source: [Exceptions](https://codewiki.com/php/exceptions/)
- `__get()` doesn't observe an ordinary read of an accessible public property, and `__call()` doesn't wrap a declared public method.
  Why: Putting all authorization, auditing, or caching in these fallback hooks leaves bypass paths.
  Source: [Magic methods](https://codewiki.com/php/magic-methods/)
- Returning `null` for every unknown property makes `$order->statsu` look like a valid missing value.
  Why: If `__isset()`, `__get()`, and `__unset()` use different name rules, one property also presents contradictory states across operations.
  Source: [Magic methods](https://codewiki.com/php/magic-methods/)
- `$service->$name(...$arguments)` turns a caller-supplied name into a capability choice.
  Why: When the service gains a public maintenance method, the proxy may expose it without any proxy code change.
  Source: [Magic methods](https://codewiki.com/php/magic-methods/)
- Cycles, garbage collection, and script shutdown affect when `__destruct()` runs.
  Why: Putting a transaction commit, queue acknowledgment, or only remote write there makes correctness depend on an uncontrolled lifetime.
  Source: [Magic methods](https://codewiki.com/php/magic-methods/)
- `get_object_vars($this)` may include tokens, connections, closures, caches, and framework services.
  Why: It turns internal field names into a durable format and may restore an object that hasn't passed constructor validation.
  Source: [Magic methods](https://codewiki.com/php/magic-methods/)
- Attacker-controlled serialized data can construct an object graph and engage autoloading and magic hooks.
  Why: Restricting allowed classes doesn't turn this format into a generally safe input protocol.
  Source: [Magic methods](https://codewiki.com/php/magic-methods/)
- Do not assume this is safe: `use Vendor\Package\Client;` does not execute `require` or guarantee that `Client` is defined.
  Why: It only tells the compiler what a short name means in this file.
  Source: [Namespaces](https://codewiki.com/php/namespaces/)
- The same import table cannot give both `Vendor\One\Logger` and `Vendor\Two\Logger` the alias `Logger`.
  Why: The second import causes a compile-time name conflict.
  Source: [Namespaces](https://codewiki.com/php/namespaces/)
- Generated code often puts `$class = 'Invoice'; new $class();` in a file that imports `App\Models\Invoice`.
  Why: The string does not become a complete name.
  Source: [Namespaces](https://codewiki.com/php/namespaces/)
- Inside `App\Jobs`, `new DateTimeImmutable()` resolves to `App\Jobs\DateTimeImmutable`.
  Why: Function fallback to global scope does not imply class fallback.
  Source: [Namespaces](https://codewiki.com/php/namespaces/)
- With `Acme\` mapped to `src/`, `Acme\Billing\InvoiceWriter` belongs at `src/Billing/InvoiceWriter.php`, with directory, file, and class-name case kept consistent.
  Source: [Namespaces](https://codewiki.com/php/namespaces/)
- File-level `use App\Models\Invoice;` imports a class name, while `function () use ($invoice) { ...
  Why: }` copies a variable into a closure or captures it by reference. The keyword is the same, but the operations are unrelated.
  Source: [Namespaces](https://codewiki.com/php/namespaces/)
- Public writable properties let callers bypass object rules.
  Why: Generated code often constructs an empty object and assigns fields one by one; omit one typed property and the failure is delayed until its first read.
  Source: [Object-oriented programming](https://codewiki.com/php/oop/)
- A child constructor doesn't automatically execute the parent constructor it overrides.
  Why: If it omits `parent::__construct()`, required parent properties may remain uninitialized.
  Source: [Object-oriented programming](https://codewiki.com/php/oop/)
- Inheriting to reuse code couples protected parent state and lifecycle into the child.
  Why: Once a child rejects input the parent accepts or changes promised side effects, it no longer safely substitutes for the parent.
  Source: [Object-oriented programming](https://codewiki.com/php/oop/)
- `readonly` prevents property reassignment but doesn't make the referenced object immutable.
  Why: Calling `$order->customer->rename()` may still mutate the nested object.
  Source: [Object-oriented programming](https://codewiki.com/php/oop/)
- Object `==` compares class and property values, while `===` checks whether both expressions point to the same instance.
  Why: Swapping them confuses equal contents with identical identity.
  Source: [Object-oriented programming](https://codewiki.com/php/oop/)
- `int|string` constrains runtime types only.
  Why: It doesn't determine whether a string is an allowed status, whether an integer is in range, or what an array contains. A broad union can also spread a data model that should have been normalized across the codebase.
  Source: [PHP 8.0 features](https://codewiki.com/php/php8-features/)
- Generated code often converts every call to named form, then still treats a parameter rename as a risk-free refactor.
  Why: The old name becomes an `Unknown named parameter` error at runtime, while tests that call only positionally won't expose it.
  Source: [PHP 8.0 features](https://codewiki.com/php/php8-features/)
- `#[RequiresRole('admin')]` is only metadata.
  Why: Without a consumer, it checks no identity. A consumer that calls `getAttributes()` but never instantiates the descriptors may also leave bad constructor arguments and invalid targets undiscovered until much later.
  Source: [PHP 8.0 features](https://codewiki.com/php/php8-features/)
- A broad `default` makes future input or an enum case inherit old policy automatically.
  Why: Omitting `default` has the opposite limitation: it throws `UnhandledMatchError` only when the missing path executes and still provides no compile-time proof.
  Source: [PHP 8.0 features](https://codewiki.com/php/php8-features/)
- `?->` turns a technically nullable access into `null`, but it doesn't tell whether missing data is expected, retryable, or corrupt.
  Why: Appending `?? ''` casually further erases the difference between “no object” and “empty field.”
  Source: [PHP 8.0 features](https://codewiki.com/php/php8-features/)
- One commit that promotes constructor properties, replaces `switch` with `match`, and adds unions changes assignment, comparison, and failure modes together.
  Why: An old `switch` may rely on loose comparison or fall-through, so a direct replacement can be syntactically valid and behaviorally different.
  Source: [PHP 8.0 features](https://codewiki.com/php/php8-features/)
- An assignment inside a condition turns a typo into valid code.
  Why: `if ($isAdmin = true)` changes the variable to `true` and then always enters the branch.
  Source: [PHP fundamentals](https://codewiki.com/php/fundamentals/)
- `and` and `or` have lower precedence than assignment.
  Why: `$allowed = true and false` assigns `true` to `$allowed` before the complete expression evaluates to `false`.
  Source: [PHP fundamentals](https://codewiki.com/php/fundamentals/)
- A truthiness test on external text swallows the valid string `'0'`.
  Why: For example, `if (!$quantity)` cannot distinguish absence, an empty string, zero, and Boolean `false`.
  Source: [PHP fundamentals](https://codewiki.com/php/fundamentals/)
- Do not assume this is safe: a `switch` case without `break` continues into later cases, and `switch` matches cases loosely.
  Why: Generated code often copies `switch` habits from another language directly into PHP.
  Source: [PHP fundamentals](https://codewiki.com/php/fundamentals/)
- After `foreach ($items as &$item)`, `$item` still references the last array element.
  Why: A later loop that reuses the name can corrupt that last item even when the later loop contains no `&`.
  Source: [PHP fundamentals](https://codewiki.com/php/fundamentals/)
- Do not assume this is safe: a class using a trait does not mean its objects implement a type contract named after that trait.
  Why: A parameter type named for the trait cannot mean "any object that uses this trait" and makes substitution depend on an implementation detail.
  Source: [Traits](https://codewiki.com/php/traits/)
- Generated or copied traits often read `$this->name`, call `$this->save()`, or declare a constructor.
  Why: Those dependencies and the required initialization order are not visible from the trait's public methods.
  Source: [Traits](https://codewiki.com/php/traits/)
- Do not assume this is safe: `Logger::write as private writeLog` adds a private alias but does not remove the original `write()` or resolve a conflict with another trait's `write()`.
  Why: Misreading it can leave an unintended public API or preserve a fatal collision.
  Source: [Traits](https://codewiki.com/php/traits/)
- A trait property and same-named class property can be composed only when their visibility, type, `readonly` modifier, and initial value are compatible.
  Why: Trait and class constants have their own compatibility rules, and a mismatch makes class loading fail.
  Source: [Traits](https://codewiki.com/php/traits/)
- Do not assume this is safe: a trait static property is not object state, and inheritance sharing depends on whether the child uses the trait again.
  Why: Accessing static members directly through the trait name has also been deprecated since PHP 8.1.
  Source: [Traits](https://codewiki.com/php/traits/)
- A trait that pulls a database, cache, or network client from a global container makes its class constructor appear dependency-free while requiring external resources at runtime.
  Why: Tests must replace globals, and several traits can create an implicit call order.
  Source: [Traits](https://codewiki.com/php/traits/)
