Adding a Prettier README.md

This commit is contained in:
2026-08-19 11:00:28 -03:00
parent 46dd177be9
commit 29d05b84e6
+215 -3
View File
@@ -1,3 +1,215 @@
# ORM
aasasf
ddd
# urfat-orm
A lightweight, zero-dependency PHP ORM with Laravel/Eloquent-inspired syntax. Built on top of PDO, it supports multiple named connections, soft deletes, timestamps, eager loading, and pivot table relationships.
---
## Installation
```bash
composer install
```
No external dependencies. PHP 8.0+ required.
---
## Setup
```php
require 'vendor/autoload.php';
use ORM\Connection;
use ORM\Connections;
// Using the factory helper
$pdo = Connection::open('pgsql', 'dbname=mydb host=localhost', 'user', 'pass', 'myprefix_');
// Or register a raw PDO
$pdo = new PDO("pgsql:dbname=mydb;host=localhost", 'user', 'pass');
$pdo->prefix = 'myprefix_';
Connections::addConnection($pdo); // 'default'
Connections::addConnection($pdo, 'secondary'); // named connection
```
Supported drivers: `pgsql`, `mysql`, `sqlite`, `firebird`, `oracle`, `mssql`.
---
## Defining a Model
```php
use ORM\Entity;
class User extends Entity {
const _tableName = 'users';
const _connectionName = 'default'; // optional, defaults to 'default'
const _softdelete = true; // filters deleted_at IS NULL on all queries
const _timestamps = true; // auto-sets created_at / updated_at on save
const _ignore = ['password_confirm']; // excluded from INSERT / UPDATE
const _idPolice = [ // optional custom ID strategy
'type' => 'nextval',
'min' => 500,
'max' => 9999,
'step' => 10,
];
protected function posts() {
return $this->hasMany(Post::class, 'user_id');
}
protected function country() {
return $this->belongsTo(Country::class, 'country_id', 'id');
}
protected function roles() {
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
}
}
```
---
## Features
### CRUD
```php
// Create
$user = new User(['name' => 'Alice', 'email' => 'alice@example.com']);
$user->save(); // INSERT; $user->id is populated after
// Read
$user = User::findOne(42);
$user = User::findOne(['email' => ['=', 'alice@example.com']]);
$users = User::findAll();
$users = User::findMany(['active' => ['=', 1]], ['*'], ['LIMIT' => 10, 'OFFSET' => 0]);
$n = User::count(['active' => ['=', 1]]);
// Update
$user->name = 'Bob';
$user->save(); // UPDATE because $user->id exists
// Delete
$user->delete(); // soft-delete if _softdelete=true, otherwise hard-delete
$user->purge(); // always hard-delete
$user->restore(); // clears deleted_at (soft-delete models only)
```
Criteria format: `['column' => ['operator', value]]`
Supported operators: `=`, `<`, `>`, `<=`, `>=`, `IN`, `LIKE`
### Soft Delete
When `_softdelete = true`, all `findAll`, `findOne`, `findMany`, and `count` calls automatically append `WHERE deleted_at IS NULL`. Pass `true` as the last argument to include trashed records:
```php
$all = User::findAll(['*'], [], true); // includes soft-deleted
$trashed = User::findOne(42, ['*'], true);
```
### Timestamps
When `_timestamps = true`, `created_at` and `updated_at` are set on construction and `updated_at` is refreshed on every `save()`.
### Relationships
| Method | Description |
|---|---|
| `hasOne(Class, $remoteField)` | One child record keyed by local id |
| `hasMany(Class, $remoteField)` | Many child records keyed by local id |
| `belongsTo(Class, $localField, $remoteField)` | Parent record |
| `belongsToMany(Class, $pivot, $localKey, $remoteKey)` | Many-to-many via pivot |
| `belongsToManyExtended(...)` | Many-to-many via pivot; returns pivot rows with foreign object in `->childElement` |
```php
// Lazy load (fires a query on access)
$posts = $user->posts()->get();
$country = $user->country; // magic __get shorthand
// Eager load on a Collection
$users = User::findAll()->with(['posts', 'country']);
// Eager load on a single Entity
$user->with(['posts', 'country']);
```
### Multiple Named Connections
```php
Connections::addConnection($pdo1); // 'default'
Connections::addConnection($pdo2, 'analytics'); // 'analytics'
class Event extends Entity {
const _connectionName = 'analytics';
}
```
### Custom ID Strategy (`_idPolice`)
When `type = 'nextval'` the ORM computes the next ID from `MAX(id)` before inserting, respecting `min`, `max`, and `step`. Useful for databases without auto-increment sequences.
### Raw Query Helpers (`DBInstance`)
For queries that don't fit the Entity API:
```php
use ORM\DBInstance;
// Single record with prepared statement
$row = DBInstance::getRecord('users', ['email' => ['=', $email]]);
// Multiple records
$rows = DBInstance::getRecords('users', ['active' => ['=', 1]], ['id', 'name'], ['LIMIT' => 50]);
// Free SQL
$rows = DBInstance::getRecordsSql("SELECT * FROM {users} WHERE created_at > ?", [$date]);
// Insert and get last id
$id = DBInstance::insertRecord('logs', ['action' => 'login', 'user_id' => 1]);
// Update a single field
DBInstance::setField('users', 'active', 0, $id);
// Direct query
DBInstance::execute("UPDATE {users} SET score = score + 1 WHERE id = ?", [$id]);
```
Table names always use `{tableName}` placeholder syntax so the connection prefix is applied automatically.
### SQL Tracing
```php
$pdo->traceSQL = true; // prints every query to stdout
```
---
## Known Bugs
| # | Location | Description |
|---|---|---|
| 1 | `Collection::with()` | After the Relations namespace refactor, the `switch` class-name checks still use old names (`ORM\HasMany`, etc.) — all branches fall to `default`, which works but is inaccurate |
| 2 | `Entity::findMany()`, `Entity::count()` | When no criteria are given and `_softdelete = true`, `AND deleted_at IS NULL` is appended without a preceding `WHERE`, producing invalid SQL |
| 3 | `Entity` constructor / `update()` | Timestamp format uses `date('Y-m-d h:m:s')``h` is 12-hour clock and the second `m` is month; should be `date('Y-m-d H:i:s')` |
| 4 | `Entity::__get()` | Returns `0` for any undefined property, silently masking typos |
| 5 | `DBInstance::queryOne()` | When called with `$data`, it delegates to `queryPrepare()` which calls `fetchAll()` and returns an array, not a single object |
| 6 | `DBInstance::setField()` | Uses PHP string interpolation `{$table}` instead of the ORM's `{tableName}` syntax, so the connection prefix is never applied |
| 7 | `Entity::findMany()` | Values are interpolated directly into SQL (no prepared statements), making this method unsafe for user-supplied input |
---
## To Do
- [ ] Fix `findMany()` and `count()` soft-delete `WHERE` clause when no other criteria exist
- [ ] Fix timestamp format (`H:i:s`)
- [ ] Fix `Collection::with()` class-name checks after Relations namespace refactor
- [ ] Fix `DBInstance::setField()` table prefix placeholder
- [ ] Fix `DBInstance::queryOne()` to actually return one record
- [ ] Make `findMany()` use prepared statements
- [ ] `Collection::with()` — use a bulk `IN (...)` query instead of one query per item (N+1 problem)
- [ ] Relations cannot be iterated in Mustache / template engines (magic `__get` on `Entity` calls `->get()` but templates can't iterate the result transparently)
- [ ] `__get()` should return `null` instead of `0` for undefined properties
- [ ] Add formal test suite (PHPUnit)
- [ ] Support `ORDER BY` in `findAll()` and `findMany()`
- [ ] Support `whereNull` / `whereNotNull` criteria shorthand