mindy/orm
Mindy ORM is a Laravel/PHP ORM package for modeling database data and relationships in PHP applications. It offers an alternative ORM layer for common model and query workflows, but has minimal adoption and limited community feedback.
mindy/orm is the model layer of the MindyPHP micro-framework, not an Eloquent plugin. It is a standalone ORM that talks to mindy/db and mindy/query-builder. Treat adopting it in a Laravel app as "adding a second ORM for one bounded context", not "swapping Eloquent".
4.0.4 (latest) changed the version story: the line is now 4.x, not the old 1.0 tag, and it declares mindy/query-builder:>=4.0.4. Pin both together — a stale query-builder is the most likely cause of "class not found" or missing-operator errors after upgrade.
composer require mindy/orm:^4.0 mindy/query-builder:^4.0.4
Let Composer resolve, then check composer show mindy/query-builder actually landed ≥4.0.4. The ORM's new JSON field support depends on the newer builder; mismatched minors can install cleanly and fail at runtime.src/Model.php → src/BaseManager.php / src/QuerySet.php → src/Fields/. The Fields/* classes are the actual API surface (type mapping, casting, validation, and often DDL); Model is mostly glue and magic-property resolution. For 4.0.4, read the JSON field class and its QuerySet/builder counterpart first — that is the only new behaviour.app/Providers/MindyOrmServiceProvider.php, build the Mindy\Query\Connection, and register it where Model/BaseManager expects it (check the exact static/getConnection() in your checkout before assuming).getFields() to double as a validation contract. With 4.0.4, a settings/attributes JSON column is now a viable first slice too.use Mindy\Orm\Model;
use Mindy\Orm\Fields\IntField;
use Mindy\Orm\Fields\CharField;
use Mindy\Orm\Fields\JsonField;
use Mindy\Orm\Fields\ForeignField;
class Article extends Model
{
public static function tableName()
{
return 'articles';
}
public static function getFields()
{
return [
'id' => ['class' => IntField::class, 'primary' => true, 'autoComplete' => true],
'title' => ['class' => CharField::class, 'required' => true],
'meta' => ['class' => JsonField::class], // new in 4.0.4 — confirm class name in src/Fields/
'author' => ['class' => ForeignField::class, 'modelClass' => Author::class],
];
}
}
$articles = Article::objects()
->filter(['title__like' => '%orm%'])
->order(['-id'])
->limit(20)
->all();
getFields() as a config array with a class key. This is the single source of truth for both persistence and input validation (required, validator, etc.) — Mindy's form layer consumes the same config.JsonField and let the field class do the encode/decode; do not hand-json_encode() into a TextField anymore, or you will have two representations of the same column in one codebase. Keep JSON blobs to genuinely schemaless data — flags, per-tenant settings, third-party payloads — and keep anything you filter or join on as a real column.Model::objects() returns a fresh QuerySet. Compose filter(), exclude(), order(), limit(), offset(), then a terminal all(), count(), get(). Operators are suffix-style (title__like, id__in); verify accepted suffixes in QuerySet before you rely on them.getAuthor(), getComments() returning $this->belongsTo(...)/$this->hasMany(...). The base model resolves these as virtual properties. Map mentally to Eloquent's belongsTo/hasMany, but do not assume with() exists — check QuerySet first.ArticleRepository::recent(int $n)) rather than scattering objects()->filter() through controllers. This is also your escape hatch if you later migrate that context to Eloquent.Author::objects()->filter(['id__in' => $ids])->all() and hydrate in PHP. Same N+1 discipline as Eloquent, just manual.beginTransaction() / commit() / rollBack() explicitly around multi-model writes. There is no DB::transaction() helper.Mindy\Orm\Manager and point the model at it to get named scopes (Article::objects()->published()), mirroring Eloquent local scopes.>=4.0.4), so the JSON path is the thing to review in source: the field class, its cast on __get, and how QuerySet builds a JSON where-clause. Nothing else is documented as changed, which is not the same as nothing having changed in a major bump.meta->key, meta__key, JSON_EXTRACT) in QuerySet. Check the builder before designing screens around filtering inside JSON; if it's absent, filter in PHP over a bounded set, or promote hot keys to columns.JSON column or a TEXT/LONGTEXT. Native JSON gives you MySQL-side validation and indexable generated columns; text does not. Own the column type in your Laravel migration either way.null, {}, [], and a scalar. Encoders that turn [] into {} (or vice versa) will silently break consumers, and ->filter(['meta__in' => ...]) against a JSON column is a different query than against text.__get/__set — declare explicit properties on your subclasses if so.getSql() / getDbType()); inspect before writing a schema syncer. With JSON columns this matters more, since Laravel's json() column type and the field's assumed DDL can disagree. Pragmatic path: own the schema in Laravel migrations and let Mindy read it.Connection as a singleton and inject it once.QuerySet/Manager before designing the schema — retrofitting column names onto a convention-driven ORM is miserable.getSql()/toSql() equivalent on QuerySet (or attach a logger to the connection) and dump it. This is now your primary tool for verifying the new JSON clauses — composite __ suffixes are opaque when they silently return empty sets.primary is assumed single-column, don't model join-ish tables on it — use a surrogate id.Illuminate\Contracts anything: no Model::find(), no policies, no API resources, no factories, no route() model binding. JSON columns don't change this — expose them through DTOs, not by handing Mindy models to JsonResource.Field subclasses (type cast + validation + DDL in one place), custom Manager subclasses (scopes), and TreeModel for adjacency-list data (categories, menus). The new JsonField is a working example of the first — copy its shape for any bespoke cast you need.How can I help you explore Laravel packages today?