A few days ago, I introduced Doctrine 3 and Symfony compatibility in Small Entity Schema.
That was an important step, but it also raised a broader question:
Can the schema become independent from the ORM used by the application?
With the latest evolution of the project, the answer is increasingly yes.
Small Entity Schema now supports Laravel Eloquent models, alongside:
- Small Swoole Entity Manager entities
- Doctrine ORM entities
- Laravel Eloquent models
The objective is not to create yet another ORM abstraction layer.
The objective is interoperability.
Repository:
https://git.small-project.dev/lib/small-entity-schema
Previous article about Doctrine compatibility:
https://dev.to/sebk69/small-entity-schema-now-supports-symfony-and-doctrine-3-1cdm
One schema, several ORM dialects
Doctrine, Eloquent and Small Swoole Entity Manager represent very similar domain concepts, but they express them differently.
Doctrine uses attributes:
#[ORM\Entity]
class Product
{
#[ORM\Id]
#[ORM\Column(type: 'integer')]
private int $id;
#[ORM\ManyToOne(targetEntity: Category::class)]
private ?Category $category = null;
}
Eloquent uses model configuration and methods:
class Product extends Model
{
protected $table = 'products';
protected function casts(): array
{
return [
'price' => 'float',
'enabled' => 'boolean',
];
}
public function category()
{
return $this->belongsTo(Category::class);
}
}
Small Swoole Entity Manager uses its own entity attributes.
From the point of view of a schema editor, however, all three describe essentially the same concepts:
entities, fields, identifiers and relations.
The architecture of Small Entity Schema now reflects that.
Instead of considering one ORM representation as the canonical model, the application converts each supported ORM into an internal schema representation.
That representation can then be visualized, edited and exported again.
Importing Eloquent models
Eloquent models are detected through inheritance from:
Illuminate\Database\Eloquent\Model
The detection also follows intermediate application base classes.
For example:
abstract class BaseModel extends Model
{
}
and:
class Product extends BaseModel
{
}
are correctly recognized as Eloquent models.
This matters because real Laravel applications frequently introduce their own base model.
Reading Eloquent configuration
Small Entity Schema extracts the main Eloquent model metadata.
For example:
class Product extends Model
{
protected $table = 'catalog_products';
protected $connection = 'mysql';
protected $primaryKey = 'product_id';
protected $keyType = 'string';
public $incrementing = false;
public $timestamps = true;
protected $dateFormat = 'U';
}
The schema can preserve information such as:
- table name
- database connection
- primary key
- primary-key type
- auto-increment configuration
- timestamps
- date format
- custom created-at and updated-at columns
If no table name is explicitly defined, the importer also applies Eloquent-style table-name inference.
Casts, fillable fields and default values
Eloquent models often contain a large part of their field metadata in $casts, $fillable and $attributes.
For example:
protected function casts(): array
{
return [
'price' => 'float',
'enabled' => 'boolean',
'metadata' => 'array',
'published_at' => 'datetime',
];
}
protected $fillable = [
'name',
'price',
'enabled',
];
protected $attributes = [
'enabled' => true,
];
Small Entity Schema translates those declarations into its internal field representation.
Common Eloquent casts are mapped to schema types such as:
boolean
int
float
string
array
object
date
dateTime
timestamp
The original Eloquent-specific information is also retained as dialect metadata when necessary.
This is important for round-trip conversion: a neutral schema should not require throwing away useful ORM-specific information.
Eloquent relations are also supported
Relations were one of the most important parts of this implementation.
Small Entity Schema currently understands the main Eloquent relationship methods:
belongsTo()
hasOne()
hasMany()
belongsToMany()
morphTo()
morphOne()
morphMany()
They are converted into a canonical relationship representation.
For example:
public function category()
{
return $this->belongsTo(Category::class);
}
becomes conceptually:
Product
manyToOne
Category
while:
public function products()
{
return $this->hasMany(Product::class);
}
becomes:
Category
oneToMany
Product
belongsToMany() also preserves pivot information when it is explicitly provided:
return $this->belongsToMany(
Role::class,
'user_roles',
'user_id',
'role_id'
);
Polymorphic relations are represented separately instead of trying to incorrectly force them into a traditional foreign-key relation model.
Static analysis first, Laravel runtime when available
One constraint was important to me:
opening a Laravel project should not require booting the complete application just to discover its models.
The primary importer therefore works through static PHP analysis.
This is where another of my open-source projects, Small Class Manipulator, becomes particularly useful.
Small Class Manipulator provides an intermediate representation of PHP classes that makes it possible to inspect and modify:
- classes
- inheritance
- properties
- methods
- attributes
- imports
- types
without coupling Small Entity Schema directly to the source-code syntax.
This separation between code representation and schema representation has progressively become one of the key interoperability mechanisms across my projects.
When a Laravel artisan executable is available, Small Entity Schema can additionally use:
php artisan model:show App\\Models\\Product --json
to enrich the statically discovered model with runtime information.
The runtime inspection is therefore an enhancement, not a requirement for the basic import process.
Exporting back to Eloquent
Compatibility is not limited to reading Laravel projects.
Small Entity Schema can also generate or update Eloquent models.
Given an entity definition, the Eloquent writer can generate configuration such as:
class Product extends Model
{
protected $table = 'products';
protected $primaryKey = 'id';
protected function casts(): array
{
return [
'price' => 'float',
'enabled' => 'boolean',
];
}
protected $fillable = [
'name',
'price',
'enabled',
];
public function category()
{
return $this->belongsTo(Category::class);
}
}
Relations are translated back into their corresponding Eloquent methods.
The exporter currently handles:
manyToOne -> belongsTo
oneToOne -> hasOne
oneToMany -> hasMany
manyToMany -> belongsToMany
as well as the supported polymorphic relations.
Mixed ORM projects
This is probably the part I find the most interesting.
Small Entity Schema no longer has to assume that an entire project belongs to one ORM ecosystem.
A schema can contain entities originally imported from different sources.
Conceptually:
Small Entity Schema
|
+-- Small Swoole Entity
|
+-- Doctrine Entity
|
+-- Eloquent Model
Each entity keeps track of its source dialect.
In the default export mode, entities can therefore be written back using their original representation.
The export layer can also target a specific ORM when conversion is desired.
This opens interesting use cases for migrations and interoperability.
For example, a developer can reason about:
Doctrine
↓
Canonical schema
↓
Eloquent
without making Doctrine or Eloquent themselves responsible for the conversion.
The schema becomes the intermediary.
Why I prefer this architecture
I do not want Small Entity Schema to become a collection of direct ORM-to-ORM converters.
That approach quickly becomes difficult to maintain.
With three ORM implementations, direct conversion already creates several possible paths:
Swoole <-> Doctrine
Swoole <-> Eloquent
Doctrine <-> Eloquent
Add another ORM and the number of converters keeps increasing.
Instead, the architecture is:
Doctrine
|
v
Eloquent -> Canonical Schema <- Small Swoole
|
v
UI / Editor
Each ORM only needs to understand the canonical schema.
This is simpler and, more importantly, keeps the domain model independent from the framework used to persist it.
About composite primary keys
There is one deliberate limitation worth mentioning.
Eloquent does not natively support composite primary keys.
Small Entity Schema can represent them because Doctrine and Small Swoole Entity Manager can have different identifier strategies.
But exporting an entity with several primary keys to Eloquent is rejected instead of silently generating an incorrect model.
I prefer an explicit incompatibility error over producing code that looks valid but does not correctly represent the schema.
Interoperability between my projects
This evolution is also an example of what I am trying to achieve across the Small open-source ecosystem.
Projects such as:
Small Class Manipulator
deal with the representation and manipulation of PHP code.
Small Entity Schema
deals with the representation of persistence models.
Small Forms
deals with validation and transformation metadata.
Small Swoole Entity Manager
provides one runtime persistence implementation.
The objective is not for every component to depend strongly on all the others.
Instead, each project exposes a representation that makes it easier for another component to interact with it.
That allows features such as Doctrine support, Symfony Validator support and now Eloquent support to be added without rewriting the complete application around a framework-specific abstraction.
What comes next?
Supporting Eloquent makes Small Entity Schema much less tied to its original Small Swoole ecosystem.
It can now work with two of the most common PHP persistence approaches:
Symfony / Doctrine
Laravel / Eloquent
while still supporting Small Swoole Entity Manager.
There is still work to do around increasingly dynamic ORM configurations and more advanced relationships.
But the architectural direction is now much clearer:
Small Entity Schema should describe the model, not dictate how that model is persisted.
If you work with Laravel, Symfony, Doctrine or different persistence technologies in the same organization, I would be particularly interested in feedback about the interoperability problems you encounter in real projects.
Repository: