---
title: "ORM Project"
description: "Doctrine ORM and DBAL configured from an array, and the repositories of Derafu over Doctrine entities. A proof of concept without a stable API"
type: "docs"
category: "doc"
tags: [php]
authors: [Anonymous]
date: "2026-10-26"
last_update: "2026-10-26"
time_minutes: 1
draft: false
unlisted: false
url: "https://www.derafu.dev/docs/data/orm"
---

# Derafu ORM



---

## Introduction

Doctrine ORM and DBAL configured from an array, and the repositories of Derafu over Doctrine entities. A proof of concept without a stable API

# Models and Entities

[![GitHub](https://img.shields.io/badge/github-derafu%2Form-blue?logo=github)](https://github.com/derafu/orm)
![GitHub last commit](https://img.shields.io/github/last-commit/derafu/orm/main)
![CI Workflow](https://github.com/derafu/orm/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/orm)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/orm)
![Total Downloads](https://poser.pugx.org/derafu/orm/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/orm/d/monthly)

&gt; Since 2006, [I participated in FLISoL](https://flisol.info/FLISOL2006/Chile/Valparaiso). In 2014, I presented [SowerPHP at UTEM (Santiago, Chile)](https://www.cnsl.cl/index.php/noticias/81-blog/isabel/1063-programa-flisol-santiago-2014). Years before, I presented MiPaGiNa at the University of Chile. On that occasion, someone in the audience said to me &quot;Oh, you built an ORM,&quot; and I said, &quot;What is that?&quot; That day, during the presentation I was giving, I learned what an ORM was. This project compiles the ideas that have appeared in my mind over the years. Always with the goal of making everything &quot;automagical.&quot;
&gt;
&gt; -- &lt;cite&gt;Esteban De La Fuente Rubio&lt;/cite&gt;

`derafu/orm` does not implement an ORM: it uses [Doctrine ORM](https://www.doctrine-project.org/projects/orm.html) and [DBAL](https://www.doctrine-project.org/projects/dbal.html). It is a library, not a framework bundle, and its API is not stable yet.

## Installation

PHP 8.5 or later is required.

```bash
composer require derafu/orm
```

## Components

| Class | Purpose |
|-------|---------|
| `EntityManagerFactory` | Create an `EntityManager` from an array of options (`EntityManagerFactoryInterface`) |
| `ConnectionParametersFactory` | Create the parameters of the connection, with its DSN (`ConnectionParametersFactoryInterface`) |
| `DoctrineRepositoryFactory`, `DoctrineRepository` | Give the entities of Doctrine through the repository manager of [derafu/repository](https://www.derafu.dev/docs/data/repository) |
| `DoctrineCliConfigurationFactory` | Create what the CLI of Doctrine Migrations needs, from a DSN or an array |

## Guides

- [Entity manager](https://www.derafu.dev/docs/data/orm/entity-manager): the `dbal` and `orm` options, with the types, the caches and the errors.
- [Repositories](https://www.derafu.dev/docs/data/orm/repositories): one repository manager for the entities of Doctrine and the data sources of `derafu/repository`.
- [Migrations](https://www.derafu.dev/docs/data/orm/migrations): the configuration of the CLI of Doctrine Migrations, without a framework.
- [Symfony integration](https://www.derafu.dev/docs/data/orm/symfony): wire the entity manager and the repository factory with Symfony DependencyInjection.




---

## Entity Manager

Create an entity manager from an array, with the options of Doctrine DBAL and ORM

# Entity Manager

`EntityManagerFactory` creates an `EntityManager` from an array of options. It implements `Derafu\Orm\Contract\EntityManagerFactoryInterface`, so it can be a service that is injected and replaced.

```php
use Derafu\Orm\Factory\EntityManagerFactory;

$entityManager = (new EntityManagerFactory())-&gt;create([
    &#039;dbal&#039; =&gt; [
        &#039;url&#039; =&gt; &#039;mysql://app:secret@127.0.0.1:3306/app?serverVersion=8.0&amp;charset=utf8mb4&#039;,
    ],
    &#039;orm&#039; =&gt; [
        &#039;paths&#039; =&gt; [__DIR__ . &#039;/src/Entity&#039;],
        &#039;dev_mode&#039; =&gt; false,
        &#039;naming_strategy&#039; =&gt; Doctrine\ORM\Mapping\UnderscoreNamingStrategy::class,
    ],
]);
```

The configuration has two sections, `dbal` and `orm`, with the options that Doctrine has. The entities are mapped with PHP attributes, and Doctrine&#039;s native lazy objects are always enabled.

The parameters of the connection are created by a `ConnectionParametersFactoryInterface`, which `EntityManagerFactory` receives in its constructor. `ConnectionParametersFactory` is the default one.

## `dbal`

Every parameter of the connection that DBAL&#039;s `DriverManager` understands, with its official name (`driver`, `path`, `memory`, `host`, `port`, `user`, `password`, `dbname`, `charset`, `serverVersion`, `defaultTableOptions`, `driverOptions`...). Doctrine checks them. The section also has these options:

| Option | Value |
|--------|-------|
| `url` | A DSN, read with DBAL&#039;s `DsnParser`. The schemes are `mysql`, `mysqli`, `pgsql`, `postgresql`, `postgres`, `sqlite`, `sqlserver` and `mssql`. The parameters of the DSN win over the other ones |
| `types` | Custom types, as `name =&gt; class` of a `Doctrine\DBAL\Types\Type`. The `uuid` type is always registered |
| `schema_filter` | A regular expression of the tables that Doctrine keeps |

A SQLite connection needs a `path`, unless it is `memory`. A DSN like `sqlite:///:memory:` is in memory, and a relative path in a SQLite DSN is relative to the current directory.

The DBAL type registry is global. A type that is already registered is replaced, so the same configuration can be used more than once.

## `orm`

| Option | Value |
|--------|-------|
| `paths` | List of directories with the entities |
| `dev_mode` | `true` or `false` |
| `naming_strategy` | A class, or an instance, of `Doctrine\ORM\Mapping\NamingStrategy` |
| `resolve_target_entities` | `interface =&gt; class` of the entity that stands for the interface |
| `dql` | Custom functions of DQL: `string_functions`, `numeric_functions` and `datetime_functions`, each as `name =&gt; class` of a `FunctionNode` |
| `metadata_cache`, `query_cache`, `result_cache`, `hydration_cache` | A [PSR-6](https://www.php-fig.org/psr/psr-6/) pool each |

Without the caches, `ORMSetup` of Doctrine chooses one: in memory in development mode, and APCu, Memcached or Redis if they are available otherwise.

## Errors

The options are read with [derafu/config](https://www.derafu.dev/docs/data/config), through the typed readers of derafu/array, so one with another type raises their translatable error, naming the option (for example `The key &quot;orm.paths&quot; must be of type list&lt;string&gt;, string given.`). `DatabaseConfigurationException` is for what the package itself checks: a database that is not configured, a DSN that is not valid or has no known driver, a SQLite connection without a file, a regular expression that is not valid, a class that is not of the expected type, and a map whose keys or values are not strings.




---

## Repositories

One repository manager for the entities of Doctrine and the data sources of derafu/repository

# Repositories of Derafu over Doctrine

`DoctrineRepositoryFactory` creates a repository of `derafu/repository` for a class that Doctrine maps. Registered in the repository manager under those classes, one manager gives the repository of every class, whether its data is in a table or in a data source:

```php
use Derafu\Orm\Factory\DoctrineRepositoryFactory;
use Derafu\Repository\Service\DataProvider;
use Derafu\Repository\Service\RepositoryManager;

$manager = new RepositoryManager(
    dataProvider: new DataProvider([App\Entity\Country::class =&gt; __DIR__ . &#039;/countries.php&#039;]),
    factories: [App\Entity\Invoice::class =&gt; new DoctrineRepositoryFactory($entityManager)],
);

$invoices = $manager-&gt;getRepository(App\Entity\Invoice::class);  // Doctrine.
$countries = $manager-&gt;getRepository(App\Entity\Country::class); // Data source.
```

Both are a `RepositoryInterface`, with the same queries (see [querying](https://www.derafu.dev/docs/data/repository/querying)). An entity in the database can refer to one that is not by its identifier, in a plain column, and get it through the manager.

The factory takes any `EntityManagerInterface`, not only the one of `EntityManagerFactory`. It rejects an identifier that is not a class that Doctrine maps with a `ManagerException`, and each class has to be registered, as the manager does not look for the classes that Doctrine maps by itself.

`DoctrineRepository` gives the queries of `RepositoryInterface` over a Doctrine repository. The lock arguments of `find()` reach Doctrine, which requires a transaction for a pessimistic lock, as when it is used directly. The methods that are only of Doctrine&#039;s repository, such as `createQueryBuilder()`, are available through the entity manager.




---

## Migrations

The configuration of the CLI of Doctrine Migrations from a DSN or an array, without a framework

# Migrations

The CLI of [Doctrine Migrations](https://www.doctrine-project.org/projects/migrations.html), `vendor/bin/doctrine-migrations`, has to know the database that it connects to and the directory of the migrations. In an application with `doctrine-migrations-bundle` the bundle gives it, and its commands are used instead. Without a framework, the convention of Doctrine is a `cli-config.php` file that returns a `DependencyFactory`. Without that file the command stops with `It was not possible to locate any configuration file.`

`DoctrineCliConfigurationFactory` creates that `DependencyFactory` from a DSN or from an array, so the file is a single line.

The CLI looks for a file called `cli-config.php` in the directory where the command runs, and then in its `config/` subdirectory, so it can be in the root of the project or in `config/`. The file returns the factory:

```php
use Derafu\Orm\Factory\DoctrineCliConfigurationFactory;

return (new DoctrineCliConfigurationFactory())-&gt;create(&#039;postgresql://app:secret@127.0.0.1:5432/app?serverVersion=16&amp;charset=utf8&#039;);
```

The credentials do not have to be in the file: `create((string) getenv(&#039;DATABASE_URL&#039;))` reads the DSN from the environment.

## DSN

| Database | DSN |
|----------|-----|
| PostgreSQL | `postgresql://app:secret@127.0.0.1:5432/app?serverVersion=16&amp;charset=utf8` |
| MySQL, MariaDB | `mysql://app:secret@127.0.0.1:3306/app?serverVersion=8.0&amp;charset=utf8mb4` |
| SQLite | `sqlite:///var/data.db`, relative to the current directory, or `sqlite:////path/to/data.db`, absolute |

The DSN is read with DBAL&#039;s `DsnParser`, as the `url` option of the [entity manager](https://www.derafu.dev/docs/data/orm/entity-manager#dbal), so the other schemes and the rules are the same: `pgsql` and `postgres` are the same driver as `postgresql`, the parameters after `?` are parameters of the connection (`serverVersion`, `charset`, `sslmode`...), and a special character in the user or the password is written URL-encoded (`p@ss` is `p%40ss`).

## Array

With an array, which has the sections `dbal` and `orm` of the entity manager and `migrations`:

```php
return (new DoctrineCliConfigurationFactory())-&gt;create([
    &#039;migrations&#039; =&gt; [
        &#039;paths&#039; =&gt; [&#039;App\\Migrations&#039; =&gt; __DIR__ . &#039;/migrations&#039;],
        &#039;all_or_nothing&#039; =&gt; true,
        &#039;transactional&#039; =&gt; true,
        &#039;check_database_platform&#039; =&gt; true,
    ],
    &#039;dbal&#039; =&gt; [&#039;driver&#039; =&gt; &#039;pdo_sqlite&#039;, &#039;path&#039; =&gt; __DIR__ . &#039;/var/data.db&#039;],
    &#039;orm&#039; =&gt; [&#039;paths&#039; =&gt; [__DIR__ . &#039;/src/Entity&#039;]],
]);
```

In `config/cli-config.php`, `__DIR__` is the `config/` directory, so the paths are built from `dirname(__DIR__)`.

The three options of `migrations` are enabled by default. With a DSN, the migrations are in `migrations/` and the entities in `src/Entity/`, relative to the current directory, so the commands are run from the root of the project. The directories of the migrations, the entities and the SQLite file are created if they do not exist. The commands are those of Doctrine: `vendor/bin/doctrine-migrations diff`, `migrate` and `status`.

## Usage

The commands are run from the root of the project, with the file in place:

1. `vendor/bin/doctrine-migrations generate` creates an empty migration class in the directory of the migrations, with the `up()` and `down()` methods to complete.
2. `vendor/bin/doctrine-migrations diff` creates the migration by comparing the database with the entities of `orm.paths`: its `up()` has the SQL that makes the database like the entities (a `CREATE TABLE` for an entity whose table does not exist).
3. `vendor/bin/doctrine-migrations status` shows the database that it uses and the executed and available migrations.
4. `vendor/bin/doctrine-migrations migrate` executes the pending migrations and ends with `[OK] Successfully migrated to version: App\Migrations\Version20260101000000`.
5. `vendor/bin/doctrine-migrations up-to-date` tells if there is nothing left to execute: `[OK] Up-to-date! No migrations to execute.`

The executed migrations are recorded in the table `doctrine_migration_versions` of the database, which Doctrine creates. No entity maps that table, so `diff` leaves it out of the comparison and never proposes to drop it, and the other commands always see it, even if the `schema_filter` of `dbal` would leave it out.

`migrate` always asks for a confirmation, and `diff` asks for one when there are migrations that are not executed. Add `--no-interaction` to run them in a script or in a CI, where nobody can answer.

| Command | Purpose |
|---------|---------|
| `migrations:generate` (`generate`) | Create an empty migration |
| `migrations:diff` (`diff`) | Create a migration from the differences between the entities and the database |
| `migrations:migrate` (`migrate`) | Execute the migrations up to a version, or the latest |
| `migrations:execute` (`execute`) | Execute one or more versions, up or down |
| `migrations:status` (`status`) | Show the configuration and the state of the migrations |
| `migrations:list` (`list-migrations`) | List the migrations and their state |
| `migrations:current` (`current`), `migrations:latest` (`latest`) | Show the current version and the latest one |
| `migrations:up-to-date` (`up-to-date`) | Tell if the database is up to date |
| `migrations:version` (`version`) | Add or remove versions in the table of versions, without executing them |
| `migrations:rollup` (`rollup`) | Replace the recorded versions with a single one |
| `migrations:dump-schema` (`dump-schema`) | Write the schema of the database as a migration |
| `migrations:sync-metadata-storage` (`sync-metadata-storage`) | Update the table of versions to the latest format |

## Tables that no entity maps

`diff` makes the database like the entities, so it proposes to drop the tables of the database that no entity maps (a `DROP TABLE legacy_x` for a table of another application, for example). To keep it away from them, limit the tables that Doctrine reads with the `schema_filter` of the `dbal` section, a regular expression of the tables to keep:

```php
&#039;dbal&#039; =&gt; [
    &#039;url&#039; =&gt; &#039;postgresql://app:secret@127.0.0.1:5432/app?serverVersion=16&amp;charset=utf8&#039;,
    &#039;schema_filter&#039; =&gt; &#039;~^(?!legacy_)~&#039;,
],
```

The `--filter-expression` option of `diff` does the same for one run. When both are used, a table has to pass both.

With SQLite, a table whose identifier is a single integer without a generated value is proposed again by every `diff`, as Doctrine reads that table back as an auto-increment key. It is a behavior of Doctrine with SQLite, not of the factory.




---

## Symfony Integration

Wire the entity manager and the repository factory with Symfony DependencyInjection

# Symfony Integration

For an application that uses `symfony/dependency-injection`, `resources/config/orm-services.yaml` defines these services:

| Service | Notes |
|---------|-------|
| `EntityManagerFactory` and `ConnectionParametersFactory` | With their contracts, `EntityManagerFactoryInterface` and `ConnectionParametersFactoryInterface`, as aliases. An application replaces a factory by pointing the alias to its own service |
| `Doctrine\ORM\EntityManagerInterface` | Created by the factory of the entity manager, from the parameters `doctrine.dbal` and `doctrine.orm` |
| `DoctrineRepositoryFactory` | To register in the repository manager, see [repositories](https://www.derafu.dev/docs/data/orm/repositories) |
| The provider of the translations of the package | Tagged `derafu_translation.resource_provider` |

Import it from the file of services of the application and set the parameters:

```yaml
imports:
    - { resource: &#039;../vendor/derafu/orm/resources/config/orm-services.yaml&#039; }

parameters:
    doctrine.dbal:
        url: &#039;%env(DATABASE_URL)%&#039;
    doctrine.orm:
        paths: [&#039;%kernel.project_dir%/src/Entity&#039;]
        naming_strategy: Doctrine\ORM\Mapping\UnderscoreNamingStrategy
```

Their defaults are a SQLite file in `var/app.db` and the entities in `src/Entity`. An application can ignore the file and define its own services, with its own configuration or with Doctrine directly.





---
Last updated on 26/10/2026
#php
