Laravel: The solution for third-party migrations and models

A good experience out of the box, without reinventing the wheel

Italo Baeza Cabrera
5 min read4 days ago

I’ve always had problems with how to approach third-party migrations — packages requiring a new table in your database so the included Eloquent Models can work. I know two ways to deal with this, when you’re a package author:

  • Just register the migration into the developer application.
  • Tell the user to publish a default migration file.

The first option was documented as the preferred way in Laravel, but it came with the problem of not having control on the migration itself. The most common problem was having a conflicting table name, or problems with migrating a new database table version. On any case, you were required to drop the table and replicate it as you saw fit.

Currently, the latter option is now the official procedure. This allows the developer to edit the migration, since the package hands off the file to the user. This is not enough.

Why is not enough? Because most of the time these tables are tied to an Eloquent Model, which have their table, connection, and many other attributes that are hardcoded. Publishing the migration is an incomplete way to deal with tying the application database to a package Eloquent Model.

I decided to tackle the problem with a simple package, as my pull request was shot down recently.

Doing the Customizable Model route

The end-developer, the one that is installing your pacakge into its application, should have the ability to do two things:

  1. Configure the Model when is instanced
  2. Configure the Migration partially.

The first step is practically non-existent in Laravel. After creating an Eloquent Model from your package, the end-developer doesn’t have any mechanism to change the Model behavior out of the box, especially if you instance it inside your package.

The second point is already done with the publishedMigrations() method from Laravel. This will automatically publish the migration from your package into the application database/migrations/ directory. Personally, I like having more control on what and what not the developer can do into a migration file, but I’ll leave that for other people.

So, lets start with fixing the first problem, customizing the Eloquent Model.

Customizable Model

Contrary to one would think, Eloquent Models sit in front of the database instead of the inverse, so the Model configuration dictates how to connect to the database.

We can easily allow the end-developer to customize a Model through a callback that is executed when the Model is instanced. By registering an static Closure inside the Model, we can allow the developer to change its behavior when its application boots.

We don’t need to create an especial Model whatsoever, only add a trait called CustomizableModel with the initializeCustomizableModel method on it. This method will be automatically executed when the Eloquent Model is instanced. The trait itself will be marked as @internal to avoid the end-developer picking up the trait as usable for its project.

namespace Vendor\Package;

use Closure;

/**
* @internal
*/
trait CustomizableModel
{
/**
* A callback that customizes the model instance when is instanced.
*
* @var \Closure(static):void
*/
protected static Closure $customize;

/**
* Initialize the current trait.
*/
protected function initializeCustomizableModel(): void
{
isset(static::$customize) && (static::$customize)($this);
}

/**
* A callback that customizes the model instance when is instanced.
*
* @param \Closure(static):void
*/
public static function customize(Closure $callback): void
{
static::$customize = $callback;
}
}

Once our trait is created, we can add it to the Eloquent Model we want the end-developer to be able to be customized.

namespace Vendor\Package\Models;

use Illuminate\Database\Eloquent\Model;
use Vendor\Package\CustomizableModel;

class PackageModel extends Model
{
use CustomizableModel;

// ...
}

Finally, we can guide the end-developer to use a callback to customize the Model. For example, he can change the table name, database connection, add casts, or even hide some properties from serialization. These should be done in the boot() method of the AppServiceProvider file or app.php file.

I can guess most of the end-developers will modify the table name, as sometimes the application will be using a table that may collide with another one required by the package.

use Illuminate\Foundation\Application;
use Vendor\Package\Models\PackageModel;

return Application::configure(basePath: dirname(__DIR__))
->booted(function () {

PackageModel::customize(function ($model) {
$model->setTable('my_custom_table');
});

})->create();

Since the Model dictates how to connect to the database, the migration can pick the table and connection name and use that to create the table. Depending on the Laravel version, you will need to use other methods.

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Vendor\Package\Models\PackageModel;

return new class extends Migration
{
public function up()
{
$model = new PackageModel;

Schema::connection($model->getConnectionName())
->create($model->getTable(), function (Blueprint $table) {
// ...
});
}
}

Custom Migration

Another problem that I decided to tackle was adding migrations that cannot be edited but extended through a simple callback.

For example, you may create a table with some specific columns. Once done, you let the developer add new columns. We can do this by creating an especial migration that does that, but also exposes a way to add a custom callback.

namespace Vendor\Package;

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Closure;

class CustomMigration extends Migration
{
/**
* Add additional columns while creating the table.
*
* @var \Closure(\Illuminate\Database\Schema\Blueprint):void
*/
protected Closure $additional;

/**
* Add additional columns while creating the table.
*
* @param \Closure(\Illuminate\Database\Schema\Blueprint):void $add
* @return $this
*/
public function add(Closure $add): static
{
$this->additional = $add;

return $this;
}

/**
* Execute the migration.
*/
public function up(): void
{
$model = new PackageModel;

Schema::connection($model->getConnectionName())
->create($model->getTable(), function (Blueprint $table) {
$table->id();

// ...

$table->timestamps();

// Execute the developer callback if it's set.
isset($this->additional) && ($this->additional)($table);
});
}

/**
* Rollback all migration changes.
*/
public function down()
{
$model = new PackageModel;

Schema::connection($model->getConnectionName())
->dropIfExists($model->getTable());
}
}

Note that we’re using the Model instance to get the table and connection name. If the end-developer customized the Model, we will get the changes automatically.

After that, we will just create a migration, but instead of using returning an anonymous class that extends the Migration class, we can we will use our own class. The developer won’t have control on your table schema but only be able to add things to it.

use Vendor\Package\CustomMigration;

return (new CustomMigration)->add(function (Blueprint $table) {
// Put here your additional column tables...
//
// $table->string('title');d
});

The developer can still have its way to copy-paste your migration file and do its own, but this just makes it more convenience and safer since the developer won’t meddle with your hardcoded columns.

How to download this?

Because I don’t like to repeat myself, I created Laragear’s Meta Model package. The “Meta” is because it’s intended to be used inside a Vendor Package (something you will distribute) rather than an application.

I also added instructions and supercharged the Custom Migration to make it more convenience to use. You’re free to download it and fix your Models & Migrations shenanigans yourself.

--

--

Italo Baeza Cabrera
Italo Baeza Cabrera

Written by Italo Baeza Cabrera

Graphic Designer graduate. Full Stack Web Developer. Retired Tech & Gaming Editor. https://italobc.com

No responses yet