- About
- Features
- Requirements
- Database Compatibility
- Installation
- Quick Start
- Documentation
- Examples
- API Reference
- Contributing
- License
ExtendOrm is a lightweight, easy-to-use Object-Relational Mapping (ORM) library for PHP 8.4+. It provides a simple yet powerful way to interact with your database using PHP objects, with support for relationships, query building, and transactions.
Perfect for developers who want ORM functionality without the complexity and overhead of heavier solutions like Doctrine or Eloquent.
- π Lightweight & Fast - Minimal overhead, no bloat
- π§ PHP 8.4+ Attributes - Clean, modern syntax for model definitions
- π Relationships - HasOne, HasMany, BelongsTo support
- π CRUD Operations - Simple save, find, update, delete methods
- π Query Builder - Fluent interface for complex queries
- π Transactions - Automatic rollback on errors
- π¦ Pagination - Built-in support for paginated results
- π― Type-Safe - Leverages PHP's type system
- πΎ PDO-Based - Works with MySQL, PostgreSQL, SQLite, and more
- PHP 8.4 or higher
- PDO extension enabled
- Database: MySQL or any PDO-compatible database
β οΈ Important Note: For databases other than MySQL (PostgreSQL, SQLite, etc.), you may need to modify theQueryBuilderto handle database-specific SQL syntax (e.g.,LIMIT/OFFSETsyntax, identifier quoting, date functions, etc.)
| Database | Status | Notes |
|---|---|---|
| MySQL | β Fully Supported | Tested and recommended |
| MariaDB | β Fully Supported | Compatible with MySQL |
| PostgreSQL | May require LIMIT/OFFSET syntax changes |
|
| SQLite | May require quote identifier changes | |
| SQL Server | β Not Tested | Unconfirmed compatibility |
| Oracle | β Not Tested | Unconfirmed compatibility |
The QueryBuilder currently uses MySQL syntax for the following:
-- LIMIT clause (MySQL style)
LIMIT offset, count
-- Identifier quoting
`column_name`
-- Auto-increment
AUTO_INCREMENT
-- Date/Time functions
NOW(), CURDATE()If you're using PostgreSQL, SQLite, or another database, you may need to adjust:
-
LIMIT/OFFSET Syntax
-- MySQL: LIMIT offset, count LIMIT 0, 10 -- PostgreSQL/SQLite: LIMIT count OFFSET offset LIMIT 10 OFFSET 0
-
Identifier Quoting
-- MySQL: backticks `column_name` -- PostgreSQL/SQLite: double quotes "column_name"
-
Auto-increment Primary Keys
-- MySQL: AUTO_INCREMENT id INT AUTO_INCREMENT PRIMARY KEY -- PostgreSQL: SERIAL id SERIAL PRIMARY KEY -- SQLite: AUTOINCREMENT id INTEGER PRIMARY KEY AUTOINCREMENT
composer require fishyboat21/extendorm- Clone the repository:
git clone https://github.com/FishyBoat21/ExtendOrm.git- Include the autoloader in your project:
require_once 'vendor/autoload.php';- Or manually load the classes:
spl_autoload_register(function ($class) {
$prefix = 'FishyBoat21\\ExtendOrm\\';
$base_dir = __DIR__ . '/ExtendOrm/src/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relative_class = substr($class, $len);
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
if (file_exists($file)) {
require $file;
}
});<?php
require_once 'vendor/autoload.php';
use FishyBoat21\ExtendOrm\Database;
use FishyBoat21\ExtendOrm\QueryBuilder2\QueryBuilder2;
use FishyBoat21\ExtendOrm\Attribute\Table;
use FishyBoat21\ExtendOrm\Attribute\Column;
use FishyBoat21\ExtendOrm\Attribute\PrimaryKey;
use FishyBoat21\ExtendOrm\Model;
use PDO;
// 1. Setup database connection (MySQL example)
$pdo = new PDO(
'mysql:host=localhost;dbname=mydb;charset=utf8mb4',
'username',
'password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// For PostgreSQL:
// $pdo = new PDO('pgsql:host=localhost;dbname=mydb', 'username', 'password');
// For SQLite:
// $pdo = new PDO('sqlite:/path/to/database.sqlite');
// 2. Boot the ORM
Database::Boot($pdo);
// 3. Create query builder
$qb = new QueryBuilder2();
// 4. Define your model
#[Table('users')]
class User extends Model {
#[PrimaryKey]
#[Column('id')]
public int $id;
#[Column('username')]
public string $username;
#[Column('email')]
public string $email;
}
// 5. Create and save a record
$user = new User($qb);
$user->username = 'johndoe';
$user->email = 'john@example.com';
$user->save();
// 6. Find records
use FishyBoat21\ExtendOrm\Criteria;
use FishyBoat21\ExtendOrm\Criterion;
use FishyBoat21\ExtendOrm\QueryBuilder2\QueryBuilderOperator;
$criteria = new Criteria();
$criteria->Add(new Criterion('id', QueryBuilderOperator::Equals, 1));
$foundUser = User::FindOne($criteria, $qb);
echo $foundUser->username; // Output: johndoeInitialize the database connection using the singleton Database class:
use FishyBoat21\ExtendOrm\Database;
use PDO;
// MySQL
$pdo = new PDO(
'mysql:host=localhost;dbname=mydb;charset=utf8mb4',
'username',
'password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// PostgreSQL
// $pdo = new PDO(
// 'pgsql:host=localhost;dbname=mydb',
// 'username',
// 'password',
// [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
// );
// SQLite
// $pdo = new PDO('sqlite:/path/to/database.sqlite');
// Boot the ORM with your PDO connection
Database::Boot($pdo);
// Get instance later in your code
$db = Database::GetInstance();
$connection = $db->GetConnection();Extend the abstract Model class and use PHP attributes to map your database tables:
use FishyBoat21\ExtendOrm\Model;
use FishyBoat21\ExtendOrm\Attribute\Table;
use FishyBoat21\ExtendOrm\Attribute\Column;
use FishyBoat21\ExtendOrm\Attribute\PrimaryKey;
#[Table('users')]
class User extends Model {
#[PrimaryKey]
#[Column('id')]
public int $id;
#[Column('username')]
public string $username;
#[Column('email')]
public string $email;
#[Column('created_at')]
public ?string $created_at = null;
}| Attribute | Description | Example |
|---|---|---|
#[Table('name')] |
Defines the database table name | #[Table('users')] |
#[Column('field')] |
Maps property to database column | #[Column('user_name')] |
#[PrimaryKey] |
Marks the primary key field | #[PrimaryKey] #[Column('id')] |
#[Relation(...)] |
Defines relationships | See below |
ExtendOrm supports three relationship types using the #[Relation] attribute.
use FishyBoat21\ExtendOrm\Attribute\Relation\RelationType;
RelationType::HasOne // One-to-one
RelationType::HasMany // One-to-many
RelationType::BelongsTo // Many-to-oneA user has one profile:
use FishyBoat21\ExtendOrm\Attribute\Relation;
use FishyBoat21\ExtendOrm\Attribute\Relation\RelationType;
#[Table('users')]
class User extends Model {
#[PrimaryKey]
#[Column('id')]
public int $id;
#[Column('username')]
public string $username;
#[Relation(
type: RelationType::HasOne,
target: Profile::class,
foreignKey: 'user_id',
localKey: 'id',
ownerKey: 'id'
)]
public ?Profile $profile;
}A user has many posts:
#[Table('users')]
class User extends Model {
#[PrimaryKey]
#[Column('id')]
public int $id;
#[Relation(
type: RelationType::HasMany,
target: Post::class,
foreignKey: 'user_id',
localKey: 'id',
ownerKey: 'id'
)]
public array $posts;
}A post belongs to a user:
#[Table('posts')]
class Post extends Model {
#[PrimaryKey]
#[Column('id')]
public int $id;
#[Column('user_id')]
public int $user_id;
#[Relation(
type: RelationType::BelongsTo,
target: User::class,
foreignKey: 'user_id',
localKey: 'user_id',
ownerKey: 'id'
)]
public ?User $author;
}#[Table('posts')]
class Post extends Model {
#[PrimaryKey]
#[Column('id')]
public int $id;
#[Column('user_id')]
public int $user_id;
#[Column('category_id')]
public ?int $category_id = null;
#[Column('title')]
public string $title;
#[Column('content')]
public string $content;
#[Column('published_at')]
public ?string $published_at = null;
// BelongsTo: Post belongs to User (author)
#[Relation(
type: RelationType::BelongsTo,
target: User::class,
foreignKey: 'user_id',
localKey: 'user_id',
ownerKey: 'id'
)]
public ?User $author;
// BelongsTo: Post belongs to Category
#[Relation(
type: RelationType::BelongsTo,
target: Category::class,
foreignKey: 'category_id',
localKey: 'category_id',
ownerKey: 'id'
)]
public ?Category $category;
// HasMany: Post has many Comments
#[Relation(
type: RelationType::HasMany,
target: Comment::class,
foreignKey: 'post_id',
localKey: 'id',
ownerKey: 'id'
)]
public array $comments;
}$user = new User($qb);
$user->username = 'johndoe';
$user->email = 'john@example.com';
$user->save(); // Performs INSERT
echo $user->id; // Auto-generated primary keyuse FishyBoat21\ExtendOrm\Criteria;
use FishyBoat21\ExtendOrm\Criterion;
use FishyBoat21\ExtendOrm\QueryBuilder2\QueryBuilderOperator;
// Find one record
$criteria = new Criteria();
$criteria->Add(new Criterion('id', QueryBuilderOperator::Equals, 1));
$user = User::FindOne($criteria, $qb);
// Find many records
$criteria = new Criteria();
$criteria->Add(new Criterion('email', QueryBuilderOperator::Like, '%@gmail.com'));
$users = User::FindMany($criteria, $qb);
foreach ($users as $user) {
echo $user->username . "\n";
}
// Pagination (limit, offset, criteria, queryBuilder)
$posts = Post::Paging(10, 20, $criteria, $qb); // Page 3, 10 per page// Find the record first
$criteria = new Criteria();
$criteria->Add(new Criterion('id', QueryBuilderOperator::Equals, 1));
$user = User::FindOne($criteria, $qb);
// Modify and save
$user->username = 'updated_name';
$user->email = 'newemail@example.com';
$user->save(); // Performs UPDATE (primary key exists)$criteria = new Criteria();
$criteria->Add(new Criterion('id', QueryBuilderOperator::Equals, 1));
$user = User::FindOne($criteria, $qb);
if ($user) {
$user->delete();
}use FishyBoat21\ExtendOrm\QueryBuilder2\QueryBuilderOperator;
QueryBuilderOperator::Equals // =
QueryBuilderOperator::NotEqual // !=
QueryBuilderOperator::LessThan // <
QueryBuilderOperator::MoreThan // >
QueryBuilderOperator::LessThanEquals// <=
QueryBuilderOperator::MoreThanEquals// >=
QueryBuilderOperator::Like // LIKE
QueryBuilderOperator::NotLike // NOT LIKE
QueryBuilderOperator::Is // IS (for NULL checks)use FishyBoat21\ExtendOrm\Sort;
use FishyBoat21\ExtendOrm\QueryBuilder2\QueryBuilderSortType;
$criteria->AddSort(new Sort('created_at', QueryBuilderSortType::Descending));
$criteria->AddSort(new Sort('name', QueryBuilderSortType::Ascending));use FishyBoat21\ExtendOrm\Criteria;
use FishyBoat21\ExtendOrm\Criterion;
use FishyBoat21\ExtendOrm\Sort;
$criteria = new Criteria();
// Add multiple conditions
$criteria->Add(new Criterion('status', QueryBuilderOperator::Equals, 'active'));
$criteria->Add(new Criterion('age', QueryBuilderOperator::MoreThanEquals, 18));
$criteria->Add(new Criterion('email', QueryBuilderOperator::Like, '%@example.com'));
// Add sorting
$criteria->AddSort(new Sort('created_at', QueryBuilderSortType::Descending));
$criteria->AddSort(new Sort('name', QueryBuilderSortType::Ascending));
// Execute query
$users = User::FindMany($criteria, $qb);Execute multiple operations in a transaction with automatic rollback on error:
use FishyBoat21\ExtendOrm\Database;
$db = Database::GetInstance();
$db->Transaction(function($pdo) use ($qb) {
// All operations in this closure run in a transaction
$user = new User($qb);
$user->username = 'newuser';
$user->email = 'new@example.com';
$user->save();
$profile = new Profile($qb);
$profile->user_id = $user->id;
$profile->phone = '+1-555-0123';
$profile->save();
// If any exception occurs, everything rolls back automatically
// No need to manually commit or rollback
});<?php
require_once 'vendor/autoload.php';
use FishyBoat21\ExtendOrm\Database;
use FishyBoat21\ExtendOrm\QueryBuilder2\QueryBuilder2;
use FishyBoat21\ExtendOrm\Model;
use FishyBoat21\ExtendOrm\Attribute\Table;
use FishyBoat21\ExtendOrm\Attribute\Column;
use FishyBoat21\ExtendOrm\Attribute\PrimaryKey;
use FishyBoat21\ExtendOrm\Attribute\Relation;
use FishyBoat21\ExtendOrm\Attribute\Relation\RelationType;
use FishyBoat21\ExtendOrm\Criteria;
use FishyBoat21\ExtendOrm\Criterion;
use FishyBoat21\ExtendOrm\QueryBuilder2\QueryBuilderOperator;
use PDO;
// Setup
$pdo = new PDO('mysql:host=localhost;dbname=blog', 'user', 'pass');
Database::Boot($pdo);
$qb = new QueryBuilder2();
// Define Models
#[Table('users')]
class User extends Model {
#[PrimaryKey] #[Column('id')] public int $id;
#[Column('username')] public string $username;
#[Column('email')] public string $email;
#[Relation(
type: RelationType::HasMany,
target: Post::class,
foreignKey: 'user_id',
localKey: 'id',
ownerKey: 'id'
)]
public array $posts;
}
#[Table('posts')]
class Post extends Model {
#[PrimaryKey] #[Column('id')] public int $id;
#[Column('user_id')] public int $user_id;
#[Column('title')] public string $title;
#[Column('content')] public string $content;
#[Column('published_at')] public ?string $published_at = null;
#[Relation(
type: RelationType::BelongsTo,
target: User::class,
foreignKey: 'user_id',
localKey: 'user_id',
ownerKey: 'id'
)]
public ?User $author;
}
// Create a user and post
$user = new User($qb);
$user->username = 'johndoe';
$user->email = 'john@example.com';
$user->save();
$post = new Post($qb);
$post->user_id = $user->id;
$post->title = 'My First Post';
$post->content = 'Hello, world!';
$post->published_at = date('Y-m-d H:i:s');
$post->save();
// Query with relationships
$criteria = new Criteria();
$criteria->Add(new Criterion('user_id', QueryBuilderOperator::Equals, $user->id));
$posts = Post::FindMany($criteria, $qb);
foreach ($posts as $post) {
echo $post->title . " by " . $post->author->username . "\n";
}// Find published posts with more than 100 views, sorted by date
$criteria = new Criteria();
$criteria->Add(new Criterion('published', QueryBuilderOperator::Equals, 1));
$criteria->Add(new Criterion('views', QueryBuilderOperator::MoreThan, 100));
$criteria->AddSort(new Sort('published_at', QueryBuilderSortType::Descending));
// Get page 3 with 10 items per page
$posts = Post::Paging(10, 20, $criteria, $qb);
foreach ($posts as $post) {
echo $post->title . "\n";
}| Method | Description | Returns |
|---|---|---|
save() |
Insert or update record based on primary key | self |
delete() |
Delete record from database | void |
FindOne(Criteria $criteria, IQueryBuilder2 $qb) |
Find single record matching criteria | ?static |
FindMany(Criteria $criteria, IQueryBuilder2 $qb) |
Find multiple records matching criteria | array |
Paging(int $limit, int $offset, Criteria $criteria, QueryBuilder2 $qb) |
Get paginated results | array |
GetTableName() |
Get the table name for the model | string |
| Method | Description | Returns |
|---|---|---|
Boot(PDO $connection) |
Initialize the ORM with PDO connection | void |
GetInstance() |
Get the singleton database instance | Database |
GetConnection() |
Get the PDO connection | PDO |
Transaction(callable $function) |
Execute operations in a transaction | mixed |
| Method | Description | Returns |
|---|---|---|
Add(Criterion $criterion) |
Add a where condition | self |
AddSort(Sort $sort) |
Add sorting order | self |
| Value | SQL Equivalent |
|---|---|
Equals |
= |
NotEqual |
!= |
LessThan |
< |
MoreThan |
> |
LessThanEquals |
<= |
MoreThanEquals |
>= |
Like |
LIKE |
NotLike |
NOT LIKE |
Is |
IS (for NULL) |
| Value | Description |
|---|---|
Ascending |
ORDER BY ... ASC |
Descending |
ORDER BY ... DESC |
ExtendOrm/
βββ src/
β βββ Attribute/
β β βββ Column.php
β β βββ PrimaryKey.php
β β βββ Table.php
β β βββ Relation.php
β β βββ Relation/
β β βββ RelationType.php
β βββ QueryBuilder2/
β β βββ QueryBuilder2.php
β β βββ IQueryBuilder2.php
β β βββ QueryBuilderOperator.php
β β βββ QueryBuilderSortType.php
β βββ Model.php
β βββ ModelMap.php
β βββ Database.php
β βββ Criteria.php
β βββ Criterion.php
β βββ Sort.php
β βββ ExtendORMException.php
βββ composer.json
βββ README.md
βββ LICENSE
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
- Follow PSR-12 coding standards
- Add tests for new features
- Update documentation as needed
- Keep commits atomic and descriptive
- Note: If adding support for non-MySQL databases, please include appropriate tests and documentation
This project is open-sourced software licensed under the GNU General Public License v3.0.
Copyright (C) 2024 FishyBoat21
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
- Inspired by Laravel Eloquent and Doctrine ORM
- Built with β€οΈ using PHP 8.4+ features (Attributes, Enums, Typed Properties)
- Thanks to all contributors and users!
- Issues: GitHub Issues
- Source: GitHub Repository
- Author: FishyBoat21
Made by FishyBoat21
β Star this repo if you find it useful!