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

Simple ORM system for PHP.

Installs: 863

Dependents: 3

Suggesters: 0

Security: 0

Stars: 0

Forks: 0

pkg:composer/baukasten/orm

0.1.0 2026-01-08 11:47 UTC

This package is auto-updated.

Last update: 2026-01-08 11:48:14 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