PHPackages                             milpa/data - 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. [Framework](/categories/framework)
4. /
5. milpa/data

ActiveMilpa-capability[Framework](/categories/framework)

milpa/data
==========

Runtime-native persistence primitive for the Milpa PHP framework: a plain-entity + repository contract, with file-JSON, SQLite and MySQL document-store, and in-memory backends behind a common RepositoryInterface — zero ORM, zero migrations.

v0.2.4(3w ago)01.9k4Apache-2.0PHPPHP &gt;=8.3CI passing

Since Jul 9Pushed 1w agoCompare

[ Source](https://github.com/getmilpa/data)[ Packagist](https://packagist.org/packages/milpa/data)[ RSS](/packages/milpa-data/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (6)Dependencies (15)Versions (7)Used By (4)

 [   ![Milpa](https://raw.githubusercontent.com/getmilpa/core/main/art/lockup/milpa-lockup-v-color-light.svg)  ](https://github.com/getmilpa)

Milpa Data
==========

[](#milpa-data)

> Runtime-native **persistence** for Milpa: plain entities (no ORM base class, no attributes) behind a small repository contract, with four interchangeable backends — **file** (JSON), **SQLite** and **MySQL** (document-store) and **in-memory**. Zero Doctrine, zero migrations, zero infrastructure. The persistence primitive an agent-scaffolded entity targets.

[![CI](https://github.com/getmilpa/data/actions/workflows/ci.yml/badge.svg)](https://github.com/getmilpa/data/actions/workflows/ci.yml)[![Packagist](https://camo.githubusercontent.com/db149ea16692d87c1d06de9395e54325a6e468dc3d52816e8612b6f4850af05b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d696c70612f646174612e737667)](https://packagist.org/packages/milpa/data)[![PHP](https://camo.githubusercontent.com/ca03f11ea27dac4dedc8ad56a7bdfc4a9ff5feb825055f9d2983616115076607/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d254532253839254135253230382e332d3737376262342e737667)](https://www.php.net/)[![License](https://camo.githubusercontent.com/798509b4df525f56802b56f8096862487f08023e3d7561c68656f8dab10d0d6e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4170616368652d2d322e302d626c75652e737667)](LICENSE)[![Docs](https://camo.githubusercontent.com/c6dc6a3411e15b0ac7cc4583e8e6a8144181caedb82f5d98753353decda06d77/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646f63732d4150492532307265666572656e63652d626c75652e737667)](https://getmilpa.github.io/data/)

`milpa/data` is the smallest possible seam onto persistence: an entity is any final class that implements `EntityInterface` — no base class to extend, no attributes to annotate — and a repository's only jobs are "find by id", "save", "delete", "list", and "query by equality". **No ORM, no query language, no schema migrations** — construct a repository with a path (or nothing at all) and call `save()`.

Install
-------

[](#install)

```
composer require milpa/data
```

Quick example
-------------

[](#quick-example)

```
use Milpa\Data\EntityInterface;
use Milpa\Data\FileRepository;

final readonly class Article implements EntityInterface
{
    public function __construct(
        public int|string|null $id,
        public string $title,
        public string $status,
    ) {
    }

    public function id(): int|string|null
    {
        return $this->id;
    }

    public function toArray(): array
    {
        return ['id' => $this->id, 'title' => $this->title, 'status' => $this->status];
    }

    public static function fromArray(array $row): static
    {
        return new self($row['id'] ?? null, $row['title'], $row['status']);
    }
}

$repo = new FileRepository('/var/data/articles.json', Article::class);

// Save: no id yet, so the repository assigns one and hands it back.
$id = $repo->save(new Article(null, 'Hello Milpa', 'draft'));

// A fresh instance, pointed at the same file, reads back exactly what was written —
// no shared state, no cache, just the file itself.
$reread = new FileRepository('/var/data/articles.json', Article::class);
$found = $reread->find($id);
printf("%s (%s)\n", $found->title, $found->status);
// Hello Milpa (draft)

$reread->query(['status' => 'draft']); // [Article{id: 1, title: 'Hello Milpa', status: 'draft'}]
```

Four backends, one interface
----------------------------

[](#four-backends-one-interface)

`FileRepository``SqliteRepository``MysqlRepository``InMemoryRepository`**Durability**Read-modify-write on every mutation: the whole collection lives in one JSON file, keyed by id. A fresh instance pointed at the same path — a different process, a different request — reads back exactly what the last write left, because every read and write goes through the file itself rather than an in-memory cache.A real SQLite database file: one table per entity class (`id` + `doc`, the entity's `toArray()` as JSON), created on first use — zero migrations, because `toArray()`/`fromArray()` is the schema. Survives processes and restarts; open it with any SQLite tool.A real MySQL server — the production root: one InnoDB table per entity class (`id VARCHAR` + native `doc JSON`), created on first use, utf8mb4 end to end — still zero migrations, because `toArray()`/`fromArray()` is the schema. Survives processes, restarts and hosts; when the server is unreachable, the error teaches the fix instead of dying raw.An in-process array. Nothing is written to disk; nothing survives past the instance's lifetime.**Concurrency**Every mutation runs under an exclusive `flock` held across the whole read-modify-write cycle, and every read takes a shared lock — concurrent processes over the same file can neither observe a torn write nor lose each other's rows.SQLite's own locking; every save runs inside an immediate transaction held across id assignment and the write, so concurrent savers can neither mint the same fresh id nor lose each other's rows.InnoDB row locking: fresh-id assignment runs a locking read (`SELECT … FOR UPDATE`) inside a transaction, and re-saves ride the upsert's own row lock — concurrent savers can neither mint the same fresh id nor lose each other's rows.Single-process by construction: the array is never shared beyond the instance.**Use it for**Real persistence — no database, no ORM, no infrastructure to stand up.Real persistence with a real database file — still zero migrations and zero infrastructure; needs only `ext-pdo_sqlite`.Production persistence on a shared database server — still zero migrations; needs `ext-pdo_mysql` and a MySQL to talk to.Tests, and zero-file consumers that don't need durability.All four implement the same six-method `RepositoryInterface` (`find()`, `save()`, `delete()`, `all()`, `nextId()`, `query()`), verified by one shared contract test suite (`RepositoryContractTestCase`) so behavior — id assignment, equality querying, insertion order of `all()`, isolation between entities — never drifts between backends.

Choosing a backend
------------------

[](#choosing-a-backend)

Because all four backends honor the same contract, **the backend is one config line**. `RepositoryFactory::fromConfig()` takes a `storage` config array — `driver` picks the backend, the remaining keys are that backend's constructor arguments — and hands back the right repository:

```
use Milpa\Data\RepositoryFactory;

$repo = RepositoryFactory::fromConfig([
    'driver' => 'file',
    'path'   => '/var/data/articles.json',
], Article::class);
```

Change `'file'` to `'sqlite'` and the same entities land in a real database file. Change it to `'mysql'` and they land on a server. **No other line of code moves** — `find()`, `save()`, `query()` and everything else behave identically, because the factory adds zero semantics: each driver delegates straight to the backend's own constructor.

`driver`Its keysExample`file``path` — the JSON collection file`['driver' => 'file', 'path' => '/var/data/articles.json']``sqlite``path` — the SQLite database file`['driver' => 'sqlite', 'path' => '/var/data/app.db']``mysql``dsn`, plus `user` / `password` when the DSN doesn't carry them`['driver' => 'mysql', 'dsn' => 'mysql:host=127.0.0.1;port=3306;dbname=app', 'user' => 'app', 'password' => '…']``memory`—`['driver' => 'memory']`In a Milpa app, this array is the `storage` block of `config/app.php`, and a plugin builds its repository from it in `boot()` (the entities scaffolded by `milpa/devtools`' `make:entity` wire exactly this):

```
// config/app.php
return [
    'storage' => [
        'driver' => 'sqlite',                        // ← the one line
        'path'   => __DIR__ . '/../var/data/app.db',
    ],
];

// in a plugin's boot()
$storage = $this->container->get(Config::class)->get('storage', [
    'driver' => 'file',
    'path'   => $root . '/var/articles.json',       // zero-config default
]);
$this->container->registerService(
    Article::class . 'Repository',
    RepositoryFactory::fromConfig($storage, Article::class),
);
```

Misconfiguration teaches instead of failing raw: a missing or unknown `driver` throws an error naming the four valid values, and a driver missing its key (`file` without `path`, `mysql`without `dsn`) names the exact key with a copy-pasteable example.

Requirements
------------

[](#requirements)

- PHP **≥ 8.3**
- `ext-pdo_sqlite` **only** if you use `SqliteRepository` (suggested, never required)
- `ext-pdo_mysql` — and a MySQL server — **only** if you use `MysqlRepository` (suggested, never required)
- Nothing else — `milpa/data` has no package dependencies, Milpa or otherwise

Documentation
-------------

[](#documentation)

**Full API reference: [getmilpa.github.io/data](https://getmilpa.github.io/data/)** — generated straight from the source DocBlocks and dressed with the Milpa design system.

Contributing
------------

[](#contributing)

Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Please report security issues via [SECURITY.md](SECURITY.md), and note that this project follows a [Code of Conduct](CODE_OF_CONDUCT.md).

License
-------

[](#license)

[Apache-2.0](LICENSE) © Rodrigo Vicente - TeamX Agency.

---

Milpa is designed, built, and maintained by **[Rodrigo Vicente - TeamX Agency](https://teamx.agency/?utm_source=github&utm_medium=readme&utm_campaign=milpa&utm_content=data)**.

###  Health Score

46

—

FairBetter than 92% of packages

Maintenance97

Actively maintained with recent releases

Popularity22

Limited adoption so far

Community16

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 68.4% 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 ~5 days

Total

6

Last Release

21d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1993784?v=4)[rodrigomx](/maintainers/rodrigomx)[@rodrigomx](https://github.com/rodrigomx)

---

Top Contributors

[![rodrigoteamx](https://avatars.githubusercontent.com/u/269849276?v=4)](https://github.com/rodrigoteamx "rodrigoteamx (13 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (6 commits)")

---

Tags

databasedoctrineentityfile-storageframeworkmilpaormpersistencephprepositoryphpframeworkpersistencedatabaseormdoctrinerepositorymilpa

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/milpa-data/health.svg)

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

###  Alternatives

[opulence/opulence

The Opulence PHP framework

71929.0k2](/packages/opulence-opulence)[zemit-cms/core

Build Phalcon REST APIs faster with database-first scaffolding, model relationships, eager loading, identity, permissions, CLI, and WebSocket support.

138.5k1](/packages/zemit-cms-core)

PHPackages © 2026

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