A lightweight Object-Relational Mapping (ORM) library for PHP.
- Simple and intuitive declaration of entities by adding
ColumnAttributes to class properties - Supports various property types including Uuid, DateTime and Enums
- Handles one-to-many, many-to-one, one-to-one, and many-to-many relationships
- Query provider for database interactions
- Migration module for creating and updating database schema
- Very fast in comparison to other ORM libraries - see benchmarks
- MySQL
- PostgreSQL
- SQLite
Install via Composer:
composer require marekskopal/orm//Create DB connection - MySQL
$database = new MysqlDatabase('localhost', 'root', 'password', 'database');
//Create DB connection - PostgreSQL
$database = new PostgresDatabase('localhost', 'postgres', 'password', 'database');
//Create DB connection - SQLite
$database = new SqliteDatabase('/path/to/database.sqlite');
//Create schema
$schema = new SchemaBuilder()
->addEntityPath(__DIR__ . '/Entity')
->build();
$orm = new ORM($database, $schema);
//Create new entity
$user = new User(
'John',
'Doe',
);
$orm->getRepository(User::class)->persist($user);
//Find entity by id
$user = $orm->getRepository(User::class)
->findOne(['id' => 1]);
//Update entity
$user->firstName = 'Jane';
$orm->getRepository(User::class)->persist($user);
//Delete entity
$orm->getRepository(User::class)->delete($user);You can declare entities by adding Entity attribute to class and Column attribute to class properties.
use MarekSkopal\ORM\Attribute\Column;
use MarekSkopal\ORM\Attribute\ColumnEnum;
use MarekSkopal\ORM\Attribute\Entity;
use MarekSkopal\ORM\Enum\Type;
#[Entity]
final class User
{
#[Column(type: Type::Int, primary: true, autoIncrement: true)]
public int $id;
public function __construct(
#[Column(type: Type::Timestamp)]
public DateTimeImmutable $createdAt,
#[Column(type: Type::String)]
public string $name,
#[Column(type: Type::String, nullable: true, size: 50)]
public ?string $email,
#[Column(type: Type::Boolean)]
public bool $isActive,
#[ColumnEnum(enum: UserTypeEnum::class)]
public UserTypeEnum $type,
) {
}
}Table and column names are derived from class name and parameters, but can be customized by providing additional parameters to attributes.
#[Entity(table: 'users')]
final class User
{
#[Column(type: Type::String, name: 'my_last_name_column')]
public string $lastName;
}#[Entity]
final class User
{
#[ManyToOne(entityClass: Address::class)]
public Address $address;
#[OneToMany(entityClass: User::class)]
public \Iterator $children;
}Use #[OneToOne] for a unique relationship between two entities. The owning side holds the foreign key column; the inverse side uses mappedBy pointing to the owning property name.
#[Entity]
final class User
{
// Owning side — stores profile_id column
#[OneToOne(entityClass: Profile::class)]
public Profile $profile;
}
#[Entity]
final class Profile
{
// Inverse side — no column, loaded via User::$profile FK
#[OneToOne(entityClass: User::class, mappedBy: 'profile')]
public ?User $user;
}Use #[ManyToMany] for a join-table relationship. The owning side declares the join table and column names; the inverse side uses mappedBy.
Column names default to the entity short name with an Id suffix (e.g. user_id, tag_id) when not specified explicitly.
#[Entity]
final class User
{
// Owning side — manages the user_tags join table
#[ManyToMany(
entityClass: Tag::class,
joinTable: 'user_tags',
joinColumn: 'user_id', // defaults to user_id
inverseJoinColumn: 'tag_id', // defaults to tag_id
)]
public Collection $tags;
}
#[Entity]
final class Tag
{
// Inverse side — loaded via User::$tags join table info
#[ManyToMany(entityClass: User::class, mappedBy: 'tags')]
public Collection $users;
}Relations support cascade operations via the cascade parameter. Supported values are CascadeEnum::Persist and CascadeEnum::Remove.
use MarekSkopal\ORM\Schema\Enum\CascadeEnum;
#[Entity]
final class Author
{
#[OneToMany(entityClass: Post::class, cascade: [CascadeEnum::Persist, CascadeEnum::Remove])]
public Collection $posts;
}CascadeEnum::Persist — when persist() is called on the owning entity, all related entities are persisted automatically:
$post1 = new Post('First Post', $author);
$post2 = new Post('Second Post', $author);
$author = new Author('John', new Collection([$post1, $post2]));
// Persists author and both posts in the correct order
$orm->getRepository(Author::class)->persist($author);CascadeEnum::Remove — when delete() is called on the owning entity, all related entities are deleted automatically before the owner (to avoid FK constraint violations):
$author = $orm->getRepository(Author::class)->findOne(['id' => 1]);
// Deletes all posts belonging to the author, then deletes the author
$orm->getRepository(Author::class)->delete($author);Cascade is supported on OneToMany, ManyToOne, OneToOne, and ManyToMany relations. For ManyToMany, cascade remove deletes the join table rows; cascade persist syncs the join table after persisting.
You can use DateTime or DateTimeImmutable properties in entities. The library will automatically convert datetime or timestamp columns values to those to objects.
#[Entity]
final class User
{
#[Column(type: Type::Timestamp)]
public DateTimeImmutable $createdAt;
#[Column(type: Type::DateTime)]
public DateTime $updatedAt;
}You can use native PHP enums in entities. The library will automatically convert enum column values to enum objects.
use MarekSkopal\ORM\Attribute\ColumnEnum;
#[Entity]
final class User
{
#[ColumnEnum(enum: UserTypeEnum::class)]
public UserTypeEnum $type;
}You can declare you repositories by extending AbstractRepository class and providing repository class in Entity attribute on entity class.
use MarekSkopal\ORM\Repository\AbstractRepository;
/** @extends AbstractRepository<User> */
class UserRepository extends AbstractRepository
{
}#[Entity(repositoryClass: UserRepository::class)]
final class User
{
}You can use QueryProvider to create queries.
$queryProvider = $orm->getQueryProvider();You can create select queries using Select builder.
$user = $queryProvider->select(User::class)
->where(['id' => 1])
->fetchOne();You can use where method to filter results.
Multiple AND conditions can be crated either by passing array of conditions or by chaining where method.
//Array of conditions
$user = $queryProvider->select(User::class)
->where([
'id' => 1,
'isActive' => true
])
->fetchOne();
//Chained conditions
$user = $queryProvider->select(User::class)
->where(['id' => 1])
->where(['isActive' => true])
->fetchOne();OR conditions can be created by using orWhere method.
$user = $queryProvider->select(User::class)
->where(['id' => 1])
->orWhere(['first_name' => 'John'])
->fetchOne();You can also use where method with nested conditions by passing function.
// Create nested condition: (id = 1 AND (first_name = 'John' OR last_name = 'Doe'))
$user = $queryProvider->select(User::class)
->where(['id' => 1])
->where(function (Where $where) {
$where->where(['first_name' => 'John'])
->orWhere(['last_name' => 'Doe']);
})
->fetchOne();You can pass another instance of Select object to where method to create subquery.
$subquery = $queryProvider->select(Address::class)
->columns(['user_id'])
->where(['city' => 'Brno']);
$user = $queryProvider->select(User::class)
->where('address_id', 'in', $subquery)
->fetchOne();You can insert entities using Insert builder.
$user = new User(
'John',
'Doe',
);
$queryProvider->insert(User::class)
->entity($user)
->execute();
//created entity will have id set automaticallyYou can insert multiple entities at once in one insert query.
$userA = new User(
'John',
'Doe',
);
$userB = new User(
'Jane',
'Doe',
);
$queryProvider->insert(User::class)
->entity($userA)
->entity($userB)
->execute();You can update entities using Update builder.
$user = $queryProvider->select(User::class)
->where(['id' => 1])
->fetchOne();
$user->firstName = 'Jane';
$queryProvider->update(User::class)
->entity($user)
->execute();You can delete entities using Delete builder.
$user = $queryProvider->select(User::class)
->where(['id' => 1])
->fetchOne();
$queryProvider->delete(User::class)
->entity($user)
->execute();You can delete multiple entities at once in one delete query.
$userA = $queryProvider->select(User::class)
->where(['id' => 1])
->fetchOne();
$userB = $queryProvider->select(User::class)
->where(['id' => 2])
->fetchOne();
$queryProvider->delete(User::class)
->entity($userA)
->entity($userB)
->execute();Use getTransactionProvider() to run operations inside a database transaction. The callback is committed on success and automatically rolled back if any exception is thrown.
$orm->getTransactionProvider()->transaction(function () use ($orm): void {
$orm->getRepository(User::class)->persist($userA);
$orm->getRepository(User::class)->persist($userB);
});You can also manage the transaction manually:
$tp = $orm->getTransactionProvider();
$tp->beginTransaction();
try {
$orm->getRepository(User::class)->persist($user);
$tp->commit();
} catch (\Throwable $e) {
$tp->rollback();
throw $e;
}Nesting transactions is not supported — calling transaction() inside an active transaction throws a TransactionException.
If you are using ORM in long-running PHP applications like FrankenPHP, Roadrunner or Swoole, you should call clear method on ORM cache after each request to free memory.
$orm->getEntityCache()->clear();