There is no license information available for the latest version (0.1.0) of this package.

Simple ORM system for PHP.

Maintainers

👁 saschahuber

Package info

gitlab.com/php-baukasten/orm

Issues

pkg:composer/baukasten/orm

Statistics

Installs: 1 269

Dependents: 3

Suggesters: 0

Stars: 0

0.1.0 2026-01-08 11:47 UTC

Requires

Requires (Dev)

Suggests

None

Provides

None

Conflicts

None

Replaces

None

Unknown License d5f6034e2eb09ecef463a0c5a244e393700439e8

  • Sascha Huber <kontakt.woop@sascha-huber.com>

This package is auto-updated.

Last update: 2026-06-08 12:37:29 UTC


README

A lightweight PHP ORM with a Spring Boot JPA-inspired repository pattern, featuring annotation-based entity mapping, magic finder methods, and automatic query generation.

Features

  • Spring Boot JPA-like Repository Pattern - Familiar pattern with #[Entity], #[Column], and #[EntityClass] annotations
  • Magic Finder Methods - Automatically generate queries from method names (e.g., findByNameAndEmail())
  • Complete CRUD Operations - Built-in findById(), save(), delete(), and more
  • Type-Safe Column Definitions - ColumnType enum for type safety
  • Flexible Query Approaches - Query builder, raw SQL with hydration, or direct PDO access
  • Relationship Management - OneToMany, ManyToOne, ManyToMany with lazy loading
  • Query Builder - Fluent API for complex queries with raw SQL support
  • Query Logging - Built-in query logging for debugging

Installation

composer require baukasten/orm

Quick Start

1. Define an Entity

use Baukasten\ORM\Annotation\{Column, Entity, PrimaryKey, Filterable, Lazy, ManyToOne, ManyToMany};
use Baukasten\ORM\ColumnType;
use Baukasten\ORM\LazyLoadable;

#[Entity(table: 'post')]
class Post
{
 use LazyLoadable;

 #[PrimaryKey(autoIncrement: true)]
 #[Column(name: 'post_id', type: ColumnType::INTEGER)]
 private ?int $post_id;

 #[Column(name: 'title', type: ColumnType::TEXT)]
 #[Filterable]
 private string $title;

 #[Column(name: 'permalink', type: ColumnType::TEXT)]
 #[Filterable]
 private string $permalink;

 #[Column(name: 'content', type: ColumnType::TEXT, nullable: true)]
 private ?string $content;

 #[Column(name: 'status', type: ColumnType::TEXT, default: 'draft')]
 #[Filterable]
 private string $status;

 #[Column(name: 'post_date', type: ColumnType::DATETIME)]
 private string $post_date;

 #[Lazy]
 #[ManyToOne(targetEntity: Author::class)]
 #[Column(name: 'author_id', type: ColumnType::INTEGER, nullable: true)]
 private ?Author $author = null;

 #[ManyToMany(targetEntity: PostTaxonomy::class, joinTable: "post__taxonomy_mapping", joinColumn: "post_id", inverseJoinColumn: "taxonomy_id")]
 private ?array $taxonomies = null;

 // Getters and setters
 public function getId(): ?int { return $this->post_id; }
 public function getTitle(): string { return $this->title; }
 public function setTitle(string $title): void { $this->title = $title; }
 public function getPermalink(): string { return $this->permalink; }
 public function setPermalink(string $permalink): void { $this->permalink = $permalink; }
 public function getStatus(): string { return $this->status; }
 public function setStatus(string $status): void { $this->status = $status; }
 // ... more getters/setters
}

2. Create a Repository

use Baukasten\ORM\Annotation\EntityClass;
use Baukasten\ORM\Repository;

#[EntityClass(Post::class)]
class PostRepository extends Repository
{
 // That's it! No constructor needed.
 // CRUD methods and magic finders work automatically.
}

3. Use the Repository

use Baukasten\ORM\EntityManager;

// Initialize EntityManager
$em = EntityManager::fromPDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');

// Create repository
$postRepo = new PostRepository();

// CRUD Operations
$post = new Post();
$post->setTitle('My First Blog Post');
$post->setPermalink('my-first-blog-post');
$post->setStatus('published');
$postId = $postRepo->save($post); // Insert

$post = $postRepo->findById(5); // Find by ID
$posts = $postRepo->findAll(); // Get all
$postRepo->delete($post); // Delete

Magic Finder Methods

Call methods that don't exist - they're automatically parsed into SQL queries!

// Simple equality
$posts = $postRepo->findByPermalink('my-first-post');
// SQL: SELECT * FROM post WHERE permalink = 'my-first-post'

// Multiple conditions with AND
$posts = $postRepo->findByTitleAndStatus('My Post', 'published');
// SQL: SELECT * FROM post WHERE title = 'My Post' AND status = 'published'

// Multiple conditions with OR
$posts = $postRepo->findByStatusOrPostDate('draft', '2024-01-01');
// SQL: SELECT * FROM post WHERE status = 'draft' OR post_date = '2024-01-01'

// Comparison operators
$posts = $postRepo->findWherePostDateIsGreaterThan('2024-01-01');
// SQL: SELECT * FROM post WHERE post_date > '2024-01-01'

$posts = $postRepo->findWherePostDateIsBefore('2024-12-31');
// SQL: SELECT * FROM post WHERE post_date < '2024-12-31'

// Complex combinations
$posts = $postRepo->findByStatusAndPostDateGreaterThan('published', '2024-01-01');
// SQL: SELECT * FROM post WHERE status = 'published' AND post_date > '2024-01-01'

Supported Operators:

  • IsGreaterThan, GreaterThan>
  • IsLessThan, LessThan<
  • IsGreaterThanOrEqual>=
  • IsLessThanOrEqual<=
  • IsEqual, Equals=
  • IsNotEqual, NotEqual!=
  • IsBefore, Before<
  • IsAfter, After>
  • LikeLIKE
  • No operator → =

Custom Queries

For complex queries, you have three options:

Option 1: Query Builder (Recommended)

#[EntityClass(Post::class)]
class PostRepository extends Repository
{
 public function findPublishedPosts(): array
 {
 return $this->em->queryBuilder()
 ->select($this->entity_class)
 ->where('status', 'published')
 ->orderBy('post_date', 'DESC')
 ->execute();
 }

 public function findRecentPosts(int $limit = 10): array
 {
 return $this->em->queryBuilder()
 ->select($this->entity_class)
 ->where('status', 'published')
 ->orderBy('post_date', 'DESC')
 ->limit($limit)
 ->execute();
 }
}

Option 2: Raw SQL with Hydration

#[EntityClass(Post::class)]
class PostRepository extends Repository
{
 public function findPostsAfterDate(string $date): array
 {
 return $this->selectAll(
 'SELECT * FROM post WHERE post_date >= :date ORDER BY post_date DESC',
 ['date' => $date]
 );
 }

 public function findMostRecentPost(): ?object
 {
 return $this->selectOne(
 'SELECT * FROM post WHERE status = "published" ORDER BY post_date DESC LIMIT 1'
 );
 }

 public function searchPosts(string $titlePattern, string $minDate, string $status): array
 {
 return $this->selectAll(
 'SELECT * FROM post
 WHERE title LIKE :titlePattern
 AND post_date >= :minDate
 AND status = :status',
 [
 'titlePattern' => $titlePattern,
 'minDate' => $minDate,
 'status' => $status
 ]
 );
 }
}

Option 3: Direct PDO Access

public function getPostStatistics(): array
{
 $sql = 'SELECT status, COUNT(*) as count, AVG(views) as avg_views
 FROM post
 GROUP BY status';

 $stmt = $this->em->getPdo()->query($sql);
 return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}

CRUD Operations

All repositories include these methods:

// Create / Update
$postRepo->save($post); // Insert if new, update if exists
$postId = $postRepo->insert($post); // Explicit insert
$postRepo->update($post); // Explicit update

// Read
$post = $postRepo->findById(5); // Find by ID
$post = $postRepo->findOneBy(['permalink' => 'my-post']); // Find one
$posts = $postRepo->findBy(['status' => 'published']); // Find all matching
$posts = $postRepo->findAll(); // Get all

// Delete
$postRepo->delete($post); // Delete entity
$postRepo->deleteById(5); // Delete by ID

// Utilities
$count = $postRepo->count(); // Count all
$exists = $postRepo->existsById(5); // Check existence

ColumnType Enum

Use type-safe column types:

ColumnType::STRING // VARCHAR, TEXT
ColumnType::INT // INTEGER
ColumnType::FLOAT // FLOAT
ColumnType::DATETIME // DATETIME
ColumnType::DATE // DATE
ColumnType::BOOLEAN // BOOLEAN
ColumnType::JSON // JSON
ColumnType::TIMESTAMP // TIMESTAMP
// ... and more

Examples

  • Repository Pattern: examples/repository-pattern-example.php - Complete demonstration
  • Many-to-Many: examples/many-to-many-example.php - Relationship example
  • Query Builder: examples/query-builder-operations.php - Advanced queries
  • Encapsulation: examples/encapsulated-entity-example.php - Best practices

Documentation

Testing

Unit tests are available for all classes. See the tests/README.md file for more information on running tests and test coverage.

./vendor/bin/phpunit

# Run specific test suite
./vendor/bin/phpunit tests/Unit/MagicMethodFinderTest.php

# With Docker wrapper (if direct execution doesn't work)
/bin/bash ./run-phpunit.sh

Requirements

  • PHP 8.0 or higher
  • PDO extension

Key Concepts

Entities

Entities are simple PHP classes with attributes:

  • #[Entity('table_name')] - Marks a class as an entity
  • #[Column("col_name", type: ColumnType::TYPE)] - Maps properties to columns
  • #[PrimaryKey(autoIncrement: true)] - Defines the primary key
  • Private properties with getters/setters for encapsulation

Repositories

Repositories handle database operations:

  • Extend Repository base class
  • Use #[EntityClass(Entity::class)] to specify the entity
  • Inherit CRUD operations automatically
  • Magic finders parse method names into SQL
  • Custom queries using query builder or raw SQL with hydration helpers

Magic Finders

Method names are automatically parsed:

  • findBy{Property} - Simple equality
  • findBy{Property1}And{Property2} - Multiple conditions with AND
  • findBy{Property1}Or{Property2} - Multiple conditions with OR
  • findWhere{Property}IsGreaterThan - Comparison operators
  • Property names in CamelCase, converted to snake_case automatically

Method Resolution Order:

  1. Existing methods in your repository - Custom implementations take precedence
  2. Parent class methods - Built-in CRUD methods (findById, findAll, save, etc.)
  3. Magic finders - Automatically parsed if method doesn't exist
#[EntityClass(Post::class)]
class PostRepository extends Repository
{
 // Custom implementation - called instead of magic finder
 protected function findByPermalink(string $permalink): array
 {
 return $this->selectAll(
 'SELECT * FROM post WHERE LOWER(permalink) = LOWER(:permalink)',
 ['permalink' => $permalink]
 );
 }

 // Magic finders still work for undefined methods:
 // $repo->findByTitle($title) - automatically generates SQL
 // $repo->findByPostDateGreaterThan($date) - automatically generates SQL
}

// Meanwhile, parent methods always work:
$post = $repo->findById(5); // From Repository parent class
$posts = $repo->findAll(); // From Repository parent class

License

MIT License