mindy/orm-fields
Mindy ORM Fields is a Laravel package for adding flexible custom fields to Eloquent models. Define reusable field types, store dynamic attributes, and manage model metadata without changing your core database schema.
Install and sanity-check
composer require mindy/orm-fields
Because this is a small, single-maintainer package with almost no downstream usage, do not treat it as a black box. Before wiring it into a model, open vendor/mindy/orm-fields/ and read the source — it is small enough to scan end-to-end in one sitting, and you need to know exactly which field classes it registers and how they differ from the core Mindy\Orm\Fields\* types.
What you are actually getting
mindy/orm-fields is a collection of extra field types for the MindyPHP ORM — the layer of code between "column in the database" and "PHP property on your model." The core ORM already ships CharField, IntegerField, TextField, BooleanField, DateTimeField, foreign keys, etc. This package adds the field behaviours you would otherwise hand-roll: values that need serialisation/casting on the way in and out, fields that derive their value from other fields, and fields with extra lifecycle behaviour.
Where to look first
composer.json → confirm the supported mindy/orm version range. This is the single most important line in the repo; it tells you whether the field classes match the ORM API you are on.src/ → one directory or file per field class. That listing is the feature set. There is no meaningful documentation surface beyond it.getValue() / setValue(), getDbType(), getFormField(), and any beforeSave() / afterSave() hooks — these four methods are where all the behaviour lives.First use case: a structured column without a side table
The canonical reason to reach for a package like this is storing a bag of structured data on a row without a join table. Declare the extra field in your model alongside the core ones:
use Mindy\Orm\Fields\CharField;
use Mindy\Orm\Fields\IntegerField;
use Vendor\OrmFields\JsonField; // ← verify the real namespace/class name in src/
class Product extends \Mindy\Orm\Model
{
public static function getFields()
{
return [
'name' => ['class' => CharField::class],
'price' => ['class' => IntegerField::class],
'meta' => [
'class' => JsonField::class,
'default' => [],
],
];
}
}
Then confirm two things immediately, before building anything on top: that $product->meta = ['a' => 1]; $product->save(); round-trips, and that a fresh Product::objects()->get(['id' => 1])->meta gives you back the array rather than a string. If the round-trip fails, the field's getDbType() does not match what your schema was actually created with, and nothing else will work.
Treat the field class as a declaration, the schema as the contract
Mindy's ORM is declarative: the getFields() array drives both the model's property behaviour and the schema the ORM expects. The extra field types mainly change the PHP-side representation and the storage format. The standard workflow is:
Do step 3 every time you introduce a new field type from this package. The mismatch case (field says JSON, column says TEXT or VARCHAR(255)) is the most common failure and it degrades silently.
Cast-on-read pattern for evolving payloads
Extra fields are the natural home for "this column started as a string and is now a structure." Rather than migrating, wrap the column in a field that normalises on read:
public function getMeta()
{
$value = parent::getMeta();
if (is_string($value)) {
$value = json_decode($value, true) ?: [];
}
return array_merge(['tags' => [], 'attrs' => []], $value);
}
If the package's field already does the decode, put the defaults merge in an accessor like the above so that old rows missing new keys do not blow up templates. Keep the merge in one place and never repeat it in views.
Derived fields — keep the derivation in the field, not the controller
For fields whose value comes from another field (slugs, hashes, denormalised display names), the correct integration point is the model's beforeSave() or the field's own save hook, not the form handler. That way the CLI importer, the admin panel, and any API endpoint all produce the same value. If you put it in the controller, the second entry point to the same table will get it wrong.
Be explicit about idempotency: a derived field must produce the same value on re-save, or you get churn in audit logs and broken URLs. Test save(); save(); and diff the row.
Form and admin integration
Mindy field classes typically expose a getFormField() and a set of validation rules. When you add an extra field, immediately check whether the generated form renders it. If the package does not supply a form mapping for a given field, you have two options: override getFormField() in a small subclass, or exclude the field from form generation and handle it in the controller. Choose one and apply it consistently — half-mapped fields are a reliable source of "saved but empty" bugs.
Wrap it, don't sprinkle it
Given the maintenance profile of this package, the highest-leverage pattern is a thin project-local layer:
namespace App\Fields;
class Meta extends \Vendor\OrmFields\JsonField
{
// pin behaviour here: defaults, null handling, validation
}
Then every model references App\Fields\Meta. If the upstream package stalls or a field changes behaviour, you patch one file instead of grepping the whole codebase. It also gives you a place to add the strict_types, validation, and null-coalescing the upstream field may skip.
Testing
Write one test per extra field type covering: null in / null out, empty collection in / collection out, malformed stored value (hand-edit the row to "{" and confirm nothing fatal), and save/reload round-trip. That is roughly fifteen minutes of work and it will catch every failure mode this class of package exhibits.
The package is effectively unmaintained — plan for it. One star, zero dependents, and a single-owner repo. Pin an exact version in composer.json ("mindy/orm-fields": "1.0.3", not ^1.0), commit composer.lock, and be prepared to vendor the source into src/ if Packagist availability ever becomes a concern. Do not build a critical path on a field type you are not willing to reimplement in an afternoon — and fortunately, most of these field types are an afternoon's work.
Namespace and class names are not guessable. The package name is mindy/orm-fields but the PHP namespace will not necessarily be Mindy\OrmFields. Check src/ and the autoload block in composer.json before writing a single use statement. An IDE that resolves it from the vendor directory is your fastest reference.
Column type drift is silent. The ORM's schema tool and your actual database can disagree without any error. A field that stores serialised data in a VARCHAR(255) column will truncate — and depending on the driver, may truncate mid-structure. For any structured field, force TEXT/JSON/LONGTEXT explicitly in your migration and confirm the field class does not fight you on it.
Null vs. empty is the recurring trap. Serialising null and deserialising [] (or "") are different operations, and small packages frequently conflate them. Decide your contract per field — "always an array, never null" is usually easiest — and enforce it in your wrapper class's getter. Then assert it in a test.
Getters run far more often than you think. They are called during hydration, in __get, in form rendering, in toArray()/jsonSerialize(), and inside the admin grid for every row on the page. Do not do a database lookup or an expensive decode inside one. If a decode is unavoidable, memoise on the instance.
Driver differences bite structured fields. JSON column support differs across MySQL/MariaDB/Postgres/SQLite, and this package cannot abstract that away. If your test suite runs SQLite and production runs MySQL, test the field round-trip against the production driver at least once before shipping.
Validation is probably thinner than you expect. Extra field types often ship with a storage format and little else — no length checks, no structure validation, no type coercion on input. If the value arrives from a request, validate it in the form layer explicitly. Never assume the field class rejects garbage because it serialises correctly.
Debugging technique: go around the ORM. When a value comes back wrong, bypass everything and read the raw column:
$row = Yii::$app->db->createCommand('SELECT meta FROM product WHERE id = 1')->queryScalar();
var_dump($row);
That single line splits "the field is broken" from "the write never happened" in seconds. Do this before you start reading the field's source.
Extension points to reach for first. In rough order of usefulness: subclass the field and override getValue()/setValue(); override getFormField() for form wiring; use model beforeSave() for derivation and defaults; use the model's rules()/validation array for anything the field does not check. Avoid patching vendor/ directly — composer will erase it.
Have an exit plan. If the package is only supplying two or three field types, consider reimplementing them locally now. A 60-line JsonField and a 40-line SlugField in your own App\Fields namespace are cheap, fully testable, and remove an unmaintained dependency from your supply chain. For a package at this maturity level, that is very often the correct day-one decision — and it is a decision best made before the field types are spread across forty models.
How can I help you explore Laravel packages today?