PHPackages                             acidclick/acidorm - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. acidclick/acidorm

ActiveProject[Database &amp; ORM](/categories/database)

acidclick/acidorm
=================

v3.0.6(1mo ago)015BSD-3-ClausePHPPHP &gt;= 8.4.0

Since Jun 27Pushed 1mo agoCompare

[ Source](https://github.com/acidclick/acidorm)[ Packagist](https://packagist.org/packages/acidclick/acidorm)[ Docs](http://www.acidclick.cz)[ RSS](/packages/acidclick-acidorm/feed)WikiDiscussions main Synced 2w ago

READMEChangelogDependencies (7)Versions (25)Used By (0)

AcidORM
=======

[](#acidorm)

A lightweight PHP ORM built on top of [dibi](https://dibiphp.com/) and [Nette](https://nette.org/), using **PHP 8 native attributes** to define entity mappings and relationships.

**Requires PHP 8.4+**

> **Migrating from v1.0?** See [Migration from docblock annotations](#migration-from-docblock-annotations) below.

Installation
------------

[](#installation)

```
composer require acidclick/acidorm
```

Quick Start
-----------

[](#quick-start)

### 1. Bootstrap the Engine

[](#1-bootstrap-the-engine)

```
$engine = new AcidORM\Engine();

$engine->setDb(new \Dibi\Connection([
    'driver'   => 'mysqli',
    'host'     => 'localhost',
    'username' => 'root',
    'password' => '',
    'database' => 'mydb',
]));

$engine->setCacheProvider(new Nette\Caching\Cache(
    new Nette\Caching\Storages\FileStorage('/tmp/cache')
));

$engine->setParameters([
    'appDir'         => __DIR__,
    'databaseDriver' => 'mysqli',
]);

$engine->startup();
```

### 2. Define an Entity

[](#2-define-an-entity)

Entities live in `model/Data/` and extend `AcidORM\BaseObject`.

```
// model/Data/Article.php
namespace Model\Data;

use AcidORM\BaseObject;
use AcidORM\Attributes\Name;
use AcidORM\Attributes\Plural;
use AcidORM\Attributes\Label;
use AcidORM\Attributes\DontMap;
use AcidORM\Attributes\OneToOne;
use AcidORM\Attributes\OneToMany;
use AcidORM\Attributes\ManyToMany;

#[Name('Article')]
#[Plural('Articles')]
class Article extends BaseObject
{
    public ?int    $id        = null;

    #[Label('Title')]
    public ?string $title     = null;

    #[Label('Body')]
    public ?string $body      = null;

    public ?int    $authorId  = null;

    #[Label('Published')]
    public ?string $published = null;

    #[DontMap]
    public ?string $computed  = null;

    #[OneToOne(className: 'User', propertyName: 'authorId', canBeNull: true)]
    public ?User $author = null;

    #[OneToMany(className: 'Comment', foreignKey: 'articleId')]
    public ?array $comments = null;

    #[ManyToMany(className: 'Tag', table: 'article_tag', foreignKey: 'tagId', column: 'articleId')]
    public ?array $tags = null;
}
```

### 3. Create a Mapper

[](#3-create-a-mapper)

Mappers live in `model/Mappers/` and extend `AcidORM\BaseMapper`. The class name must follow the pattern `{Entity}Mapper`.

```
// model/Mappers/ArticleMapper.php
namespace Model\Mappers;

use AcidORM\BaseMapper;

class ArticleMapper extends BaseMapper {}
```

### 4. Create a Persistor

[](#4-create-a-persistor)

Persistors live in `model/Persistors/` and extend `AcidORM\BasePersistor`. The class name must follow the pattern `{Entity}Persistor`.

```
// model/Persistors/ArticlePersistor.php
namespace Model\Persistors;

use AcidORM\BasePersistor;

class ArticlePersistor extends BasePersistor {}
```

### 5. Create a Facade

[](#5-create-a-facade)

Facades live in `model/Facades/` and extend `AcidORM\BaseFacade`. They provide a high-level API including dynamic method resolution.

```
// model/Facades/ArticleFacade.php
namespace Model\Facades;

use AcidORM\BaseFacade;

class ArticleFacade extends BaseFacade
{
    public function getPublished(int $limit = 10): array
    {
        return $this->getPersistor()->getAllByProperty('published', '1', false, null, $limit);
    }
}
```

CRUD Operations
---------------

[](#crud-operations)

All operations go through a persistor, accessible via the engine.

```
$persistor = $engine->getPersistor('Article');

// Fetch by ID
$article = $persistor->getById(1);

// Fetch all (with optional limit and offset)
$articles = $persistor->getAll(10, 0);

// Fetch by a single property
$article = $persistor->getByProperty('title', 'Hello World');

// Insert or update (id === null → INSERT, id set → UPDATE)
$article = new \Model\Data\Article();
$article->title = 'Hello World';
$article->body  = 'My first article.';
$persistor->insertUpdate($article);
// $article->id is now set after insert

// Delete by ID
$persistor->delete($article->id);
```

Facades and Dynamic Methods
---------------------------

[](#facades-and-dynamic-methods)

Facades expose a dynamic call API derived from the entity name. For a `UserFacade` bound to a `User` entity:

```
$facade = $engine->getFacade('User');

// → getById(1) + mapDependencies()
$user = $facade->getUserById(1);

// → getAllByProperty('status', 'active') + mapDependencies()
$users = $facade->getUsersByStatus('active');

// Compound property filter
$user = $facade->getUserByNameAndEmail('John', 'john@example.com');

// Insert or update
$facade->insertUpdateUser($user);

// Delete
$facade->deleteUser($user->id);
```

Facades automatically resolve one-to-many and many-to-many relationships via `mapDependencies()`.

### Pagination and Sorting

[](#pagination-and-sorting)

Dynamic `get*` methods accept additional trailing arguments for limit, offset, sort column, sort direction (`0` = ASC, `1` = DESC), and an optional total-count callback.

**Fetch all with pagination:**

```
// Signature: getUsers($limit, $offset, $orderBy, $direction, $countCallback)

// First page, 10 records, sorted by name ASC
$users = $facade->getUsers(10, 0, 'name', 0);

// Second page
$users = $facade->getUsers(10, 10, 'name', 0);

// With total count (for building a paginator)
$total = 0;
$users = $facade->getUsers(10, 0, 'name', 0, function (int $count) use (&$total) {
    $total = $count;
});
```

**Filter + pagination:**

```
// Signature: getUsersByStatus($status, $limit, $offset, $orderBy, $direction, $countCallback)

$total = 0;
$users = $facade->getUsersByStatus(
    'active',
    10,
    0,
    'name',
    0,
    function (int $count) use (&$total) { $total = $count; }
);
```

**Compound filter + pagination:**

```
$users = $facade->getUsersByRoleAndStatus('admin', 'active', 25, 0, 'email', 0);
```

Relationships
-------------

[](#relationships)

### One-to-One

[](#one-to-one)

The foreign key lives on the owning entity. Set `canBeNull: true` to produce a `LEFT JOIN` instead of `INNER JOIN`.

```
#[OneToOne(className: 'User', propertyName: 'authorId', canBeNull: true)]
public ?User $author = null;
```

### One-to-Many

[](#one-to-many)

```
#[OneToMany(className: 'Comment', foreignKey: 'articleId')]
public ?array $comments = null;
```

### Many-to-Many

[](#many-to-many)

```
#[ManyToMany(className: 'Tag', table: 'article_tag', foreignKey: 'tagId', column: 'articleId')]
public ?array $tags = null;
```

Optimizing Dependency Loading
-----------------------------

[](#optimizing-dependency-loading)

By default every persistor query loads **only the entity itself** — no JOINs, no extra queries. Dependencies are opt-in:

LayerRelationship typeMechanism`getDependencies``OneToOne`SQL JOIN added to the main query`mapDependencies``OneToMany`, `ManyToMany`Separate query per relationship after the main fetch### getDependencies — controlling JOINs (OneToOne)

[](#getdependencies--controlling-joins-onetoone)

```
$persistor = $engine->getPersistor('Article');

// No JOINs — fastest
$article = $persistor->getById(1);

// All OneToOne JOINs
$article = $persistor->getById(1, true);

// Only the 'author' JOIN
$deps    = $persistor->getDependencies(['author']);
$article = $persistor->getById(1, true, $deps);
```

### mapDependencies — controlling collections (OneToMany / ManyToMany)

[](#mapdependencies--controlling-collections-onetomany--manytomany)

```
$facade  = $engine->getFacade('Article');
$article = $facade->getPersistor()->getById(1);

// Load all collections
$facade->mapDependencies($article);

// Load only comments, skip tags
$facade->mapDependencies($article, false, ['comments']);
```

### Combining both layers

[](#combining-both-layers)

```
$deps    = $persistor->getDependencies(['author']);
$article = $persistor->getById(1, true, $deps);

$facade->mapDependencies($article, false, ['comments']);
```

HistoryComparer
---------------

[](#historycomparer)

`AcidORM\Utils\HistoryComparer` compares two versions of an entity and produces a human-readable change summary. Only properties with `#[Label]` are compared.

### hasChanges

[](#haschanges)

```
$old = $persistor->getById(5);
$new = clone $old;
$new->name = 'Updated name';

if (HistoryComparer::hasChanges($old, $new)) {
    // something changed
}
```

### getChanges

[](#getchanges)

```
$html = HistoryComparer::getChanges($old, $new);
// → "Name: Updated name"
```

### getValue

[](#getvalue)

Converts a single property value to a human-readable string:

ConditionResult`DateTimeInterface``Y-m-d H:i:s`Object with `__toString()``(string) $value``bool``'Ano'` / `'Ne'`Property has `#[EnumAttr]``{ClassName}::getName($value)`Property has `#[Formatter]``{ClassName}::format($value, $property)`Anything else`(string) $value`**EnumAttr:**

```
use AcidORM\Attributes\EnumAttr;

#[Label('Status')]
#[EnumAttr(className: StatusEnum::class)]
public ?int $status = null;

// StatusEnum must implement static getName($value): string
class StatusEnum
{
    public static function getName(int $value): string
    {
        return match ($value) {
            1 => 'Active',
            0 => 'Inactive',
            default => (string) $value,
        };
    }
}
```

**Formatter:**

```
use AcidORM\Attributes\Formatter;

#[Label('Price')]
#[Formatter(className: PriceFormatter::class)]
public ?float $price = null;

// Formatter must implement static format($value, ReflectionProperty $property): string
class PriceFormatter
{
    public static function format($value, \ReflectionProperty $property): string
    {
        return number_format((float) $value, 2, ',', ' ') . ' Kč';
    }
}
```

**HistoryDontMap** — excludes a property from history even when it has `#[Label]`:

```
use AcidORM\Attributes\HistoryDontMap;

#[Label('Internal note')]
#[HistoryDontMap]
public ?string $internalNote = null;
```

Attributes Reference
--------------------

[](#attributes-reference)

### Property attributes

[](#property-attributes)

AttributeDescription`#[Label('text')]`Human-readable label, returned by `getLabel()` and used in history`#[DontMap]`Excluded from `toArray()` and DB column list`#[OneToOne(className, propertyName, canBeNull)]`Eager-loaded JOIN relationship`#[OneToMany(className, foreignKey)]`Lazy-loaded collection resolved via facade`#[ManyToMany(className, table, foreignKey, column)]`Lazy-loaded collection via pivot table`#[EnumAttr(className)]`Enum class for `HistoryComparer::getValue()``#[Formatter(className)]`Formatter class for `HistoryComparer::getValue()``#[HistoryDontMap]`Excludes property from history comparison### Class attributes

[](#class-attributes)

AttributeDescription`#[Name('text')]`Display name used in history / audit trails`#[Plural('text')]`Plural form used by facade dynamic methods`#[HistoryBinding(key: 'key')]`History log binding keyPHP 8.4 features
----------------

[](#php-84-features)

v3.0 targets PHP 8.4 and takes advantage of the following features:

**Readonly attribute classes** — all `AcidORM\Attributes\*` classes are declared `readonly class`. Attribute instances returned by `AttributeReader` are immutable; any attempt to assign to their properties throws an `Error`:

```
$rel = new OneToOne(className: 'User', propertyName: 'userId');
$rel->className = 'Other'; // Error: Cannot modify readonly property
```

**Deprecated alias** — `AcidORM\AcidORM` triggers `E_USER_DEPRECATED` in its constructor. Use `AcidORM\Engine` instead:

```
// deprecated — triggers E_USER_DEPRECATED at runtime
$engine = new AcidORM\AcidORM();

// correct
$engine = new AcidORM\Engine();
```

**New-without-parentheses chaining** — internal code uses PHP 8.4's `new Foo->method()` syntax to chain calls on freshly created objects without extra parentheses.

Migration from docblock annotations
-----------------------------------

[](#migration-from-docblock-annotations)

v3.0 replaces PHPDoc annotations with PHP 8 native attributes. A migration script is included to convert existing entity files automatically.

```
# Preview changes without modifying files
php migrate-to-attributes.php /path/to/model --dry-run --verbose

# Apply changes in place
php migrate-to-attributes.php /path/to/model
```

The script handles all built-in annotations and automatically adds the required `use AcidORM\Attributes\*;` imports.

**Before (v1.0):**

```
/**
 * @name Article
 * @plural Articles
 */
class Article extends BaseObject
{
    /** @label Title */
    public ?string $title = null;

    /** @oneToMany(className=Comment, foreignKey=articleId) */
    public ?array $comments = null;
}
```

**After (v3.0):**

```
#[Name('Article')]
#[Plural('Articles')]
class Article extends BaseObject
{
    #[Label('Title')]
    public ?string $title = null;

    #[OneToMany(className: 'Comment', foreignKey: 'articleId')]
    public ?array $comments = null;
}
```

Directory Structure
-------------------

[](#directory-structure)

```
app/
└── model/
    ├── Data/           ← Entities  (extend BaseObject)
    ├── Mappers/        ← Mappers   (extend BaseMapper)
    ├── Persistors/     ← Persistors (extend BasePersistor)
    ├── Facades/        ← Facades   (extend BaseFacade)
    ├── Grids/
    ├── Forms/
    ├── Enums/
    └── Interfaces/

```

You can scaffold this structure automatically:

```
$engine->setParameters(['appDir' => __DIR__]);
$engine->createDirStructure();
```

Running Tests
-------------

[](#running-tests)

```
composer install
./vendor/bin/tester tests/
```

Compatibility
-------------

[](#compatibility)

BranchPHPAnnotationsNotesv1.07.4+PHPDoc (`/** @label ... */`)v2.08.0+PHP attributes (`#[Label(...)]`)v3.08.4+PHP attributes (`#[Label(...)]`)`readonly class` on attribute objectsLicense
-------

[](#license)

BSD-3-Clause / GPL-2.0 / GPL-3.0

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance90

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity61

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~156 days

Recently: every ~0 days

Total

23

Last Release

47d ago

Major Versions

0.2.0 → v1.0.12026-06-27

v1.0.10 → v2.0.02026-06-27

v2.0.0 → v3.0.02026-06-27

v2.0.x-dev → v3.0.x-dev2026-06-27

PHP version history (4 changes)0.2.0PHP &gt;= 5.4.0

v1.0.1PHP &gt;= 7.4.0

v2.0.0PHP &gt;= 8.0.0

v3.0.0PHP &gt;= 8.4.0

### Community

Maintainers

![](https://www.gravatar.com/avatar/74a89a82c94ea2b0290070f68dccb25fb9fb67703c48bc596f1b23888c49802b?d=identicon)[acidclick](/maintainers/acidclick)

---

Top Contributors

[![acidclick](https://avatars.githubusercontent.com/u/1314581?v=4)](https://github.com/acidclick "acidclick (37 commits)")

### Embed Badge

![Health badge](/badges/acidclick-acidorm/health.svg)

```
[![Health](https://phpackages.com/badges/acidclick-acidorm/health.svg)](https://phpackages.com/packages/acidclick-acidorm)
```

###  Alternatives

[nette/database

💾 Nette Database: layer with a familiar PDO-like API but much more powerful. Building queries, advanced joins, drivers for MySQL, PostgreSQL, SQLite, MS SQL Server and Oracle.

5437.0M271](/packages/nette-database)[ssch/typo3-rector

Instant fixes for your TYPO3 PHP code by using Rector.

2613.3M487](/packages/ssch-typo3-rector)[nette/caching

⏱ Nette Caching: library with easy-to-use API and many cache backends.

43719.5M415](/packages/nette-caching)[rector/rector-src

Instant Upgrade and Automated Refactoring of any PHP code

136411.0k14](/packages/rector-rector-src)[nette/web-project

Nette: Standard Web Project

11093.7k](/packages/nette-web-project)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
