Small Entity Schema Now Supports Laravel Eloquent

php dev.to

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The detection also follows intermediate application base classes.

For example:

abstract class BaseModel extends Model
{
}
Enter fullscreen mode Exit fullscreen mode

and:

class Product extends BaseModel
{
}
Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

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,
];
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

They are converted into a canonical relationship representation.

For example:

public function category()
{
    return $this->belongsTo(Category::class);
}
Enter fullscreen mode Exit fullscreen mode

becomes conceptually:

Product
    manyToOne
        Category
Enter fullscreen mode Exit fullscreen mode

while:

public function products()
{
    return $this->hasMany(Product::class);
}
Enter fullscreen mode Exit fullscreen mode

becomes:

Category
    oneToMany
        Product
Enter fullscreen mode Exit fullscreen mode

belongsToMany() also preserves pivot information when it is explicitly provided:

return $this->belongsToMany(
    Role::class,
    'user_roles',
    'user_id',
    'role_id'
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Relations are translated back into their corresponding Eloquent methods.

The exporter currently handles:

manyToOne  -> belongsTo
oneToOne   -> hasOne
oneToMany  -> hasMany
manyToMany -> belongsToMany
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Add another ORM and the number of converters keeps increasing.

Instead, the architecture is:

              Doctrine
                 |
                 v

Eloquent -> Canonical Schema <- Small Swoole

                 |
                 v

             UI / Editor
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

https://git.small-project.dev/lib/small-entity-schema

Source: dev.to

arrow_back Back to Tutorials