Skip to content

Commit 9ab4aac

Browse files
authored
Merge pull request #36 from flightphp/security/identifier-escaping-sql-hardening
Harden SQL identifier escaping and raw SQL APIs
2 parents 9b141b9 + bad6e19 commit 9ab4aac

7 files changed

Lines changed: 729 additions & 14 deletions

File tree

AGENTS.md

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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

Comments
 (0)