|
| 1 | +# AGENTS.md — FlightPHP Active Record |
| 2 | + |
| 3 | +Guidance for AI agents and contributors working in this repository. |
| 4 | + |
| 5 | +This package follows the same philosophy as [flightphp/core](https://github.com/flightphp/core). Ideologies below are adapted from core’s project guidelines and applied where they fit an ORM plugin (not the framework kernel). |
| 6 | + |
| 7 | +## Overview |
| 8 | + |
| 9 | +**flightphp/active-record** is a micro Active Record library: map a database row to a PHP object with chainable queries, relations, events, and PDO/mysqli adapters. |
| 10 | + |
| 11 | +- **Package:** `flightphp/active-record` (Packagist) |
| 12 | +- **License:** MIT |
| 13 | +- **PHP:** `>=7.4` (PHP 8+ supported; do not require 8-only syntax) |
| 14 | +- **Runtime dependencies:** none (only PHP itself) |
| 15 | +- **Namespace:** `flight\` (PSR-4 → `src/`) |
| 16 | +- **Docs:** https://docs.flightphp.com/en/v3/awesome-plugins/active-record |
| 17 | +- **README:** [README.md](./README.md) (short intro; full API is in the docs) |
| 18 | + |
| 19 | +Works standalone or with [Flight PHP](https://docs.flightphp.com). It does **not** require Flight core at runtime. |
| 20 | + |
| 21 | +## Project guidelines |
| 22 | + |
| 23 | +These are the Flight ecosystem rules that apply to this repo. Prefer them over inventing framework-heavy patterns. |
| 24 | + |
| 25 | +1. **PHP 7.4 must stay supported.** PHP 8+ is fine, but avoid PHP 8-only features (union types, constructor property promotion, `match`, named arguments in library code, mixed union returns, etc.) unless the project explicitly raises the minimum version. |
| 26 | + |
| 27 | +2. **Stay dependency-free at runtime.** Do not add Composer runtime dependencies, polyfills, or interface-only packages “for cleanliness.” Dev tools (PHPUnit, PHPCS, runway, coverage-check) are fine under `require-dev`. |
| 28 | + |
| 29 | +3. **Simple and fast.** Flight projects prioritize performance and a small surface area. Prefer fewer allocations, fewer queries (e.g. eager load over N+1), and straightforward SQL building over clever abstractions. |
| 30 | + |
| 31 | +4. **Do not bloat the library.** New capability should earn its place. Prefer a small, composable API over a kitchen-sink ORM. If something is large or optional (CLI scaffolding, etc.), keep it in a clear corner (`commands/`, runway) rather than growing `ActiveRecord` forever. |
| 32 | + |
| 33 | +5. **This is not Eloquent / Doctrine / Laravel / Yii.** It is a micro Active Record: chainable conditions, light relations, events, and adapters. Do not smuggle in migrations, unit-of-work, attribute mapping layers, repositories-as-required-pattern, nested graph loaders, or other large-framework defaults unless there is a clear, minimal design that fits existing style. |
| 34 | + |
| 35 | +6. **New features must be documented and tested.** Public behavior needs tests (this repo enforces **100%** `src/` line coverage) and accurate docs/docblocks. User-facing API changes should remain valid against the [public docs](https://docs.flightphp.com/en/v3/awesome-plugins/active-record). |
| 36 | + |
| 37 | +7. **Simplicity over cleverness.** Magic (`__call`, `__get`, `__set`) already exists for the query/relation ergonomics—do not add more layers of indirection without a strong reason. Prefer readable condition building and explicit relation config. |
| 38 | + |
| 39 | +8. **Extensibility without core bloat.** Prefer extension points users already have: subclass methods (events), `$relations`, `DatabaseInterface` adapters, `setCustomData`, chainable query methods. Avoid hard-wiring Flight framework services into the library. |
| 40 | + |
| 41 | +## Repository layout |
| 42 | + |
| 43 | +``` |
| 44 | +src/ |
| 45 | + ActiveRecord.php # Main abstract model — CRUD, queries, relations, eager load, events |
| 46 | + ActiveRecordData.php # OPERATORS, SQL_PARTS, DEFAULT_SQL_EXPRESSIONS, EVENTS |
| 47 | + Base.php # Attribute bag ($data) with magic __get/__set |
| 48 | + Expressions.php # SQL fragment value object |
| 49 | + WrapExpressions.php # Parenthesized expression groups (OR/AND wraps) |
| 50 | + database/ |
| 51 | + DatabaseInterface.php |
| 52 | + DatabaseStatementInterface.php |
| 53 | + pdo/ # PdoAdapter, PdoStatementAdapter |
| 54 | + mysqli/ # MysqliAdapter, MysqliStatementAdapter |
| 55 | + commands/ |
| 56 | + RecordCommand.php # runway CLI: make:record (scaffold model from table schema) |
| 57 | +tests/ |
| 58 | + ActiveRecordTest.php |
| 59 | + ActiveRecordPdoIntegrationTest.php |
| 60 | + ActiveRecordMysqliTest.php |
| 61 | + EagerLoadingTest.php |
| 62 | + ExpressionsTest.php |
| 63 | + WrapExpressionsTest.php |
| 64 | + commands/RecordCommandTest.php |
| 65 | + classes/ # Fixtures: User, Contact, QueryCountingAdapter |
| 66 | +records/ # Empty placeholder (app models go under runway app_root) |
| 67 | +``` |
| 68 | + |
| 69 | +`coverage/` and `clover.xml` are generated (gitignored). Do not hand-edit them. |
| 70 | + |
| 71 | +## Core architecture (read before changing behavior) |
| 72 | + |
| 73 | +### Model lifecycle |
| 74 | + |
| 75 | +1. User subclasses `flight\ActiveRecord` and passes a DB connection + table name (or config) in the constructor. |
| 76 | +2. Raw `PDO` / `mysqli` is wrapped by `transformAndPersistConnection()` into a `DatabaseInterface` adapter. |
| 77 | +3. Property assignment goes through `__set` → `$data` + `$dirty` (unless the key is an SQL expression or relation). |
| 78 | +4. Query methods (`eq`, `like`, `orderBy`, …) are magic via `__call` → `ActiveRecordData::OPERATORS` / `SQL_PARTS`. |
| 79 | +5. `find` / `findAll` / `insert` / `update` / `delete` build SQL with named placeholders (`:phN` from `ActiveRecordData::PREFIX`), then execute through the adapter. |
| 80 | +6. Events (`beforeFind`, `afterInsert`, etc.) are optional protected methods on the subclass, listed in `ActiveRecordData::EVENTS`. |
| 81 | + |
| 82 | +### Relationships |
| 83 | + |
| 84 | +```php |
| 85 | +protected array $relations = [ |
| 86 | + 'contacts' => [ self::HAS_MANY, Contact::class, 'user_id' ], |
| 87 | + 'contact' => [ self::HAS_ONE, Contact::class, 'user_id', [/* optional callbacks */] ], |
| 88 | + 'user' => [ self::BELONGS_TO, User::class, 'user_id', [], 'back_ref_name' ], |
| 89 | +]; |
| 90 | +``` |
| 91 | + |
| 92 | +- `HAS_MANY` / `HAS_ONE`: third element is the **foreign key on the related table**. |
| 93 | +- `BELONGS_TO`: third element is the **local key** on the current model. |
| 94 | +- Optional 4th: callback map when resolving the relation (`eq`, `where`, `order`, …). |
| 95 | +- Optional 5th: back-reference property name. |
| 96 | + |
| 97 | +**Lazy load:** `$user->contacts` → `getRelation()`. |
| 98 | +**Eager load:** `$user->with('contacts')->findAll()` → `loadEagerRelations()` batches with `IN (...)`. |
| 99 | + |
| 100 | +Eager-load limitations (do not “fix” without an intentional, minimal design): |
| 101 | + |
| 102 | +- No nested paths like `with(['contacts.addresses'])` |
| 103 | +- No closure constraints on `with()` |
| 104 | +- Unknown relation names throw `Exception` |
| 105 | + |
| 106 | +### Database adapters |
| 107 | + |
| 108 | +| Input type | Adapter | |
| 109 | +|------------|---------| |
| 110 | +| `PDO` | `flight\database\pdo\PdoAdapter` | |
| 111 | +| `mysqli` | `flight\database\mysqli\MysqliAdapter` | |
| 112 | +| `DatabaseInterface` | used as-is | |
| 113 | + |
| 114 | +mysqli converts named placeholders to `?`. Keep SQL/placeholder logic driver-agnostic when possible; put driver quirks in the adapter layer. |
| 115 | + |
| 116 | +### runway command |
| 117 | + |
| 118 | +`src/commands/RecordCommand.php` implements `make:record` for [flightphp/runway](https://docs.flightphp.com/awesome-plugins/runway). It introspects a table and writes a record class under `app_root` from `.runway-config.json`. Tests live under `tests/commands/`. |
| 119 | + |
| 120 | +## Development & testing |
| 121 | + |
| 122 | +```bash |
| 123 | +composer install |
| 124 | + |
| 125 | +composer test # phpunit (random order, stop on failure) |
| 126 | +composer test-coverage # HTML + clover; enforces 100% via coverage-check |
| 127 | +composer beautify # phpcbf --standard=phpcs.xml |
| 128 | +composer phpcs # phpcs -n --standard=phpcs.xml |
| 129 | +``` |
| 130 | + |
| 131 | +**Coverage:** `src/` is expected to stay at **100%** line coverage. New branches need tests. Run `composer test-coverage` before considering work complete. |
| 132 | + |
| 133 | +- PHPUnit: `phpunit.xml` — suite = `tests/`, coverage = `src/` |
| 134 | +- Style: **PSR-12** via `phpcs.xml` on `src/` and `tests/` (this package uses PSR-12, not core’s PSR-1) |
| 135 | +- Prefer `declare(strict_types=1);` |
| 136 | +- Use **strict comparisons** (`===`, `!==`) |
| 137 | + |
| 138 | +### Testing conventions |
| 139 | + |
| 140 | +- Prefer **SQLite in-memory** (`sqlite::memory:`) for integration tests (`ext-pdo_sqlite` is required-dev). |
| 141 | +- Model fixtures: `tests/classes/` (`User`, `Contact`, …). Match real relation configs when testing relations/eager load. |
| 142 | +- Use `QueryCountingAdapter` to assert query counts (N+1 vs eager load). |
| 143 | +- Keep PDO vs mysqli differences in their own tests/adapters. |
| 144 | +- Fixtures are often `require_once`’d in `setUpBeforeClass`—follow that pattern unless you also add proper `autoload-dev`. |
| 145 | + |
| 146 | +## Coding standards (library-specific) |
| 147 | + |
| 148 | +Apply the project guidelines first; then these details when editing: |
| 149 | + |
| 150 | +1. **Chainable API.** Query builders and fluent mutators return `$this` / `self`. |
| 151 | +2. **Security.** |
| 152 | + - Condition **values** (`eq`, `in`, `like`, …) are bound as parameters — safe for untrusted data. |
| 153 | + - **Identifiers** go through `escapeIdentifier()` (delimiter-escaped per engine). Do not weaken this. |
| 154 | + - `where()`, `having()`, `select()`, `order()`/`orderBy()`, `group()`, and `join()` **ON** accept raw SQL — never pass untrusted input. Prefer `eq()`/`in()`/… for filters and `orderByColumn()` for untrusted sort columns. |
| 155 | + - Simple table names / `table alias` / `table AS alias` in `join()` are auto-quoted; complex join sources stay raw for BC. |
| 156 | + - `copyFrom()`/`dirty()` keys become column names — only pass trusted keys. |
| 157 | +3. **Dirty tracking.** Inserts/updates persist `$dirty` only. Preserve that contract when changing assignment or `save()`. |
| 158 | +4. **Events.** Use `processEvent` and the names in `ActiveRecordData::EVENTS`. Keep hook signatures compatible with docs. |
| 159 | +5. **Public API is the contract.** Prefer additive changes. Keep README/docs examples working. New safe helpers (e.g. `orderByColumn`) are fine; do not change semantics of existing raw SQL methods for complex expressions. |
| 160 | +6. **Performance-conscious defaults.** Eager load exists to cut N+1; avoid designs that force per-row queries without an opt-in path. |
| 161 | +7. **No framework lock-in.** Do not require `Flight::` inside library code; examples in docs may show Flight registration for apps. |
| 162 | + |
| 163 | +## Documentation sources |
| 164 | + |
| 165 | +| Resource | Use for | |
| 166 | +|----------|---------| |
| 167 | +| [Active Record docs (v3)](https://docs.flightphp.com/en/v3/awesome-plugins/active-record) | Full public API, events, relations, eager loading, connections | |
| 168 | +| [README.md](./README.md) | Install + basic example | |
| 169 | +| [flightphp/core copilot instructions](https://github.com/flightphp/core/blob/master/.github/copilot-instructions.md) | Shared Flight ecosystem philosophy | |
| 170 | +| Packagist `flightphp/active-record` | Version / install metadata | |
| 171 | + |
| 172 | +User-visible behavior changes should stay aligned with the docs site when possible; at minimum keep in-repo comments and tests accurate. |
| 173 | + |
| 174 | +## Common tasks (agent playbook) |
| 175 | + |
| 176 | +| Goal | Where to look | |
| 177 | +|------|----------------| |
| 178 | +| Query operator / SQL part mapping | `ActiveRecordData.php`, `__call` in `ActiveRecord.php` | |
| 179 | +| find / findAll / insert / update / save / delete | `ActiveRecord.php` public methods | |
| 180 | +| Relation lazy load | `getRelation()` | |
| 181 | +| Eager load | `with()`, `loadEagerRelations()`, `assignEagerLoadedRelations()` | |
| 182 | +| Events | `processEvent()`, `ActiveRecordData::EVENTS` | |
| 183 | +| PDO/mysqli bugs | `src/database/**` + matching tests | |
| 184 | +| Scaffold CLI | `src/commands/RecordCommand.php` | |
| 185 | +| N+1 regression | `tests/EagerLoadingTest.php` + `QueryCountingAdapter` | |
| 186 | + |
| 187 | +## PR / contribution checklist |
| 188 | + |
| 189 | +- [ ] Change fits **project guidelines** (simple, fast, no bloat, PHP 7.4-safe, no new runtime deps) |
| 190 | +- [ ] `composer test` passes |
| 191 | +- [ ] `composer test-coverage` still meets 100% |
| 192 | +- [ ] `composer beautify` && `composer phpcs` clean |
| 193 | +- [ ] Public API covered by tests; docs/docblocks updated if behavior changed |
| 194 | +- [ ] No secrets or local `.runway-config.json` credentials committed (gitignored) |
| 195 | + |
| 196 | +## Out of scope / non-goals |
| 197 | + |
| 198 | +- Becoming a full ORM (migrations, schema builder, unit of work, nested eager-load graphs, polymorphic relations by default) |
| 199 | +- Runtime dependency on Flight core or other frameworks |
| 200 | +- Dropping PHP 7.4 support without an explicit project decision |
| 201 | +- “Just like Laravel Eloquent” feature parity |
0 commit comments