Adding a Prettier README.md
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user