Simple, lightweight ORM for PHP with multi-language support, relations, and migrations.
- ✅ Active Record & Repository Pattern - Choose the pattern that fits your needs
- ✅ Type-Safe - Full PHP 8.1+ type support with custom types
- ✅ Relations - HasOne, HasMany, BelongsTo, BelongsToMany
- ✅ Multi-Language - Built-in internationalization for labels, placeholders, help texts
- ✅ Migrations - Database schema versioning
- ✅ Eager Loading - Solve N+1 query problems
- ✅ Change Tracking - Only update modified fields
- ✅ Validation - Built-in and custom validators
- ✅ Database Agnostic - Works with MySQL, PostgreSQL, SQLite
- ✅ PDO Based - No external dependencies
composer require prochst/bs-ormFor detailed documentation, guides, and examples, see docs/README.md.
<?php
use prochst\bsOrm\Entity;
use prochst\bsOrm\Table;
use prochst\bsOrm\Column;
use prochst\bsOrm\Types\StringType;
use prochst\bsOrm\Types\IntegerType;
#[Table(name: 'users', label: 'Users')]
class User extends Entity
{
#[Column(
type: new IntegerType(),
primaryKey: true,
autoIncrement: true
)]
private ?int $id = null;
#[Column(
name: 'email',
type: new StringType(maxLength: 255),
label: 'Email Address',
nullable: false,
unique: true
)]
private string $email;
#[Column(
type: new StringType(maxLength: 100),
label: 'Name'
)]
private string $name;
// Getters and setters
public function getId(): ?int { return $this->id; }
public function getEmail(): string { return $this->email; }
public function getName(): string { return $this->name; }
public function setEmail(string $email): void {
$this->email = $email;
$this->markFieldAsModified('email');
}
public function setName(string $name): void {
$this->name = $name;
$this->markFieldAsModified('name');
}
}If you're using PHP 8.4 or later, you can leverage the new public private(set) syntax for cleaner code without explicit getters:
<?php
use prochst\bsOrm\Entity;
use prochst\bsOrm\Table;
use prochst\bsOrm\Column;
use prochst\bsOrm\Types\StringType;
use prochst\bsOrm\Types\IntegerType;
#[Table(name: 'users', label: 'Users')]
class User extends Entity
{
#[Column(
type: new IntegerType(),
primaryKey: true,
autoIncrement: true
)]
public private(set) ?int $id = null;
#[Column(
name: 'email',
type: new StringType(maxLength: 255),
label: 'Email Address',
nullable: false,
unique: true
)]
public private(set) string $email;
#[Column(
type: new StringType(maxLength: 100),
label: 'Name'
)]
public private(set) string $name;
// Only setters needed - properties are publicly readable
public function setEmail(string $email): void {
$this->email = $email;
$this->markFieldAsModified('email');
}
public function setName(string $name): void {
$this->name = $name;
$this->markFieldAsModified('name');
}
}
// Usage - direct property access for reading:
$user = $userRepo->find(1);
echo $user->name; // No getter needed!
echo $user->email; // Direct property access
// Writing still requires setters for change tracking:
$user->setName('John Doe');This approach provides:
- Cleaner syntax - No boilerplate getters
- Type safety - Full type hints preserved
- Write protection - Properties can only be modified via setters
- Change tracking - Setters still call
markFieldAsModified()
See examples/basic-usage-84.php for a complete working example.
<?php
use prochst\bsOrm\Dbal;
use prochst\bsOrm\Repository;
// Create PDO connection
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'password');
$dbal = new Dbal($pdo);
// Create repository
$userRepo = new Repository($dbal, User::class);
// Find user by ID
$user = $userRepo->find(1);
// Find all users
$users = $userRepo->findAll();
// Find with criteria
$activeUsers = $userRepo->findBy(
['active' => true],
['name' => 'ASC'],
limit: 10
);
// Create new user
$user = new User();
$user->setEmail('john@example.com');
$user->setName('John Doe');
$userRepo->save($user);
// Update user
$user = $userRepo->find(1);
$user->setEmail('newemail@example.com');
$userRepo->save($user); // Only updates modified fields
// Delete user
$userRepo->delete($user);<?php
use prochst\bsOrm\Relations\HasMany;
use prochst\bsOrm\Relations\BelongsTo;
#[Table(name: 'users')]
class User extends Entity
{
#[Column(primaryKey: true)]
private ?int $id = null;
#[HasMany(entityClass: Post::class, foreignKey: 'user_id')]
private array $posts = [];
public function getPosts(): array { return $this->posts; }
}
#[Table(name: 'posts')]
class Post extends Entity
{
#[Column(primaryKey: true)]
private ?int $id = null;
#[Column]
private int $user_id;
#[BelongsTo(entityClass: User::class, foreignKey: 'user_id')]
private ?User $user = null;
public function getUser(): ?User { return $this->user; }
}
// Eager loading to avoid N+1 queries
$users = $userRepo->findAllWithRelations(['posts']);
foreach ($users as $user) {
echo $user->getName() . " has " . count($user->getPosts()) . " posts\n";
}<?php
use prochst\bsOrm\Types\LocaleManager;
#[Column(
label: 'Email',
labels: [
'cs_CZ' => 'E-mailová adresa',
'en_US' => 'Email Address',
'de_DE' => 'E-Mail-Adresse',
],
placeholder: 'Enter email',
placeholders: [
'cs_CZ' => 'Zadejte e-mail',
'en_US' => 'Enter email',
'de_DE' => 'E-Mail eingeben',
]
)]
private string $email;
// Set locale
LocaleManager::setDefaultLocale('cs_CZ');
// Get translated label
$label = User::getColumnLabel('email'); // "E-mailová adresa"BS ORM includes several built-in types:
StringType- VARCHAR/TEXT with max lengthIntegerType- INT/BIGINTDecimalType- DECIMAL/NUMERIC with precisionBooleanType- BOOLEAN/TINYINT(1)DateTimeType- DATETIME/TIMESTAMPJsonType- JSON dataEnumType- ENUM valuesBlobType- BLOB/BYTEATextType- TEXT/LONGTEXTCurrencyType- Money values with currency code
<?php
$dbal->transaction(function($dbal) use ($userRepo, $postRepo) {
$user = new User();
$user->setEmail('john@example.com');
$userRepo->save($user);
$post = new Post();
$post->setUserId($user->getId());
$post->setTitle('My First Post');
$postRepo->save($post);
});- PHP 8.1 or higher
- PDO extension
- One of: MySQL, PostgreSQL, or SQLite
MIT License - see LICENSE file for details
Contributions are welcome! Please feel free to submit a Pull Request.
prochst - GitHub Profile