Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

mdupor's avatar

Overriding core classes

I'm failing to understand as to how would one extend Laravel core classes. There is this: https://laravel.com/docs/8.x/container#extending-bindings but how would the decorated service look like, and why does it accept original service? What should the decorated service return? How would I override a method from original service?

For example I would like to override Blueprint class timestamps() method. So I'd have:

$this->app->extend(Blueprint::class, function ($service, $app) {

    return new BlueprintExtended($service);
});

And I'm stuck from there. I have no idea how should the class look like.

I tried with:

    private Blueprint $blueprint;

    public function __construct(Blueprint $blueprint)
    {
        $this->blueprint = $blueprint;
    }

    public function timestamps($precision = 0)
    {
        $this->blueprint->timestamps($precision);

        $this->blueprint->string('created_by')->nullable();

        $this->blueprint->string('updated_by')->nullable();
    }

But this does nothing...

0 likes
15 replies
martinbean's avatar

@mdupor The decorator class needs to implement the same interface as the class you’re extending. The Blueprint doesn’t seem to implement an interface, so you can just delegate any unhandled methods to the underlying instance in your decorator class:

namespace App\Extensions\Database;

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Traits;

class BlueprintExtension
{
    use ForwardsCalls;

    protected $blueprint;

    public function __construct(Blueprint $blueprint)
    {
        $this->blueprint = $blueprint;
    }

    public function timestamps($precision = 0)
    {
        // Your timestamps implementation
    }

    public function __call($method, $parameters)
    {
        return $this->forwardCallTo($this->blueprint, $method, $parameters);
    }
}

When extending a binding in the container, you’ll receive the original instance as the first parameter in the callback function:

$this->app->extend(Blueprint::class, function (Blueprint $blueprint) {
    return new BlueprintDecorator($blueprint);
});

Now, whenever the Blueprint class is resolved by the container, your decorator class will be returned instead.

Tippin's avatar

@mdupor Could you not also just override that method by registering a macro using the same method name in your app provider? (Since I see Blueprint is macroable)

Blueprint::macro('timestamps', function ($precision = 0) {  
    // Your timestamps implementation
});
1 like
mdupor's avatar

@martinbean I tried implementing this in a service provider boot() method, however it doesn't work (tried register() also). Unless I've done something wrong, I presume it doesn't work because Blueprint isn't actually bound in the container. Schema is though. So I've tried

$this->app->extend(Schema::class, function ($service, $app) {
    return new SchemaDecorator($service);
});

And then within the decorator doing:

use ForwardsCalls;

protected $schema;

public function __construct(Schema $schema)
{
    $this->schema = $schema;
}

public function __call($method, $parameters)
{
    return $this->forwardCallTo($this->schema, $method, $parameters);
}

protected function createBlueprint($table, Closure $callback = null)
{
    $prefix = $this->schema->connection->getConfig('prefix_indexes')
        ? $this->schema->connection->getConfig('prefix')
        : '';

    if (isset($this->schema->resolver)) {
        return call_user_func($this->schema->resolver, $table, $callback, $prefix);
    }

    return new BlueprintExtension($table, $callback, $prefix);
}

Having the extension look like:

class BlueprintExtension extends Blueprint
{
    public function timestamps($precision = 0)
    {
        parent::timestamps($precision);

        $this->string('created_by')->nullable();

        $this->string('updated_by')->nullable();
    }
}

But this didn't work also. I tried throwing some dd()'s around the new classes and they never get triggered.

@tippin macros work for non-existing methods, using it like this doesn't override or change the original functionality at all.

Tippin's avatar

@mdupor Since I assume you call blueprint only in your migrations, why not just make your own BluePrint class that extends the base Blueprint and overrides the method (since it is public). Then just inject your own blueprint in your migrations. Unless you are trying to find a more "laravel" way.

mdupor's avatar

I really don't want to replace originally injected Blueprint in all migrations. Especially given the fact that this spans across ~10 services, and later onboarding new team members, it will wreak havoc with new migrations etc.

Tippin's avatar

@mdupor Not at my pc to test this, but technically can you not bind any one class to another? So you could still make MyBlueprint extends Blueprint, override the timestamps method. Then in container, bind like so:

$this->app->bind('Illuminate\Database\Schema\Blueprint', function ($app) {
    return $app->make('App\MyBlueprint');
});
1 like
martinbean's avatar
Level 80

@mdupor In that case, I’d define macros with a name more appropriate or intention-revealing. I don’t know the reason for using string columns for timestamps, but maybe define a macro like:

Blueprint::macro('stringTimestamps', function () {
    $this->string('created_at')->nullable();
    $this->string('updated_at')->nullable();
});

And you can then use this macro in your migrations:

$table->stringTimestamps();

Given you’re overriding the framework’s conventions, this will need to be something you need to inform new team members of (“we use string timestamps for Reason X”) and be vigilant of in pull requests.

mdupor's avatar

@tippin usually yes, but as I said to @martinbean Builder class isn't being pulled from container, but rather instantiated in a standard PHP way. So if I bind it to the container it will do nothing since Laravel codebase isn't calling a bound class.

@martinbean one minor detail passed by you, I am not converting the original timestamps, but adding a created_BY and updated_BY to have indication about the user who performed creation/editing. Macro is always an option as @tippin also suggested.

Maybe all things aside, it would be better to have those explicit rather than implicit, so macros may be a good way to go...

martinbean's avatar

@mdupor Ah, sorry, I missed that.

If that’s the case, wouldn’t the columns be integers and a foreign key pointing to a user? If so, you could maybe add a macro for “audit fields”:

Blueprint::macro('auditFields', function () {
    $this->foreignId('created_by')->nullable()->constrained();
    $this->foreignId('updated_by')->nullable()->constrained();
});
$table->timestamps();
$table->auditFields();
mdupor's avatar

Also usually yes :) In this case I have microservices interconnected, and IAM is a single service holding all users. In this setup, created_by references user ID extracted from incoming token

mdupor's avatar

@martinbean is it possible to provide a natural follow-definition for a defined macro without digging into how IDE itself resolves it? Only thing why I'm reluctant to use macro is because from migration perspective it looks as non-existing method

Tippin's avatar

@mdupor So I found a way to override it, but not sure if the effort is what you are looking for as you still need to edit every migration file. I made a custom helper method where I call in the DB connector schema builder, then inject my custom blueprint into the resolver. Then instead of Schema facade in migrations, call my helper.

MyBlueprint

<?php

namespace App;

use Illuminate\Database\Schema\Blueprint;

class MyBlueprint extends Blueprint
{
    /**
     * Add nullable creation and update timestamps to the table.
     *
     * @param  int  $precision
     * @return void
     */
    public function timestamps($precision = 0)
    {
        $this->timestamp('created_at', $precision)->nullable();

        $this->timestamp('updated_at', $precision)->nullable();

        $this->timestamp('testing_at', $precision)->nullable();
    }
}

Helper in my helpers.php

<?php

use App\MyBlueprint;

if ( ! function_exists('mySchema'))
{
    /**
     * @return Illuminate\Database\Schema\Builder
     */
    function mySchema()
    {
        $schema =  app('db')->connection()->getSchemaBuilder();

        $schema->blueprintResolver(function($table, $callback){
            return new MyBlueprint($table, $callback);
        });

        return $schema;
    }
}

Tested in a migration file

<?php

use App\MyBlueprint;
use Illuminate\Database\Migrations\Migration;

class CreateFriendsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        mySchema()->create('friends', function (MyBlueprint $table) {
            $table->uuid('id')->primary();
            $table->uuidMorphs('owner');
            $table->uuidMorphs('party');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        mySchema()->dropIfExists('friends');
    }
}

***Editing to add that I am also not a fan of Macros, but doing it this way still gets your IDE to understand and code complete, and I can easily GoTo declaration and see the class it extends if I was a new dev wondering what you did.

mdupor's avatar

It feels a bit hacky to me. I ended up using the macros for the job because a lot of classes down the road just aren't containerized and I didn't want to meddle up more than needed. Thinking about future upgrades also here. Thanks anyway!

gabelbart's avatar

I'm aware this is quite old but in case someone finds it helpful and in the hope, that if I made a terrible mistake someone will point it out, here is what it did for the special case \Illuminate\Database\Schema\Blueprint:

At least in Laravel 10.43 The blueprint is created via the container: laravel/framework/src/Illuminate/Database/Schema/Builder.php:622. In my case I needed to alter the default approach for generating index names, so neither macro nor decorator where a possible solution. In my AppServiceProvider::register method I did the following:

$this->app->bind(
    \Illuminate\Database\Schema\Blueprint::class,
    \App\Support\Database\Schema\Blueprint::class
);

My blueprint class looks like this:

namespace App\Support\Database\Schema;

use Illuminate\Support\Facades\Config;

class Blueprint extends \Illuminate\Database\Schema\Blueprint
{
    protected function createIndexName($type, array $columns): string
    {
        return Config::get('database.hashed_index_names')
            ? $this->createHashedIndexName($type, $columns)
            : parent::createIndexName($type, $columns);
    }

    // See laravel/framework pull-request #51625
    protected function createHashedIndexName($type, array $columns): string
    {
        $table = $this->prefix . $this->table;

        $indexSegment = str_replace(
            ['-', '.'], '_', strtolower($table . '_'.implode('_', $columns))
        );

        $type = strtolower($type);

        return $type . '_' . md5($indexSegment);
    }
}

Please or to participate in this conversation.