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

MohamedTammam's avatar

How can I generate a dynamic ID?

In my app I want to generate an ID for every order like 2010-5.

Which is [year][month]-[order number in that month]

And of course I want to make sure it's unique.

What do you recommend the best way to do it? generate it in store method every time I create new order? or create a DB trigger that handles that? how can I ensure the uniqueness?

Please share your ideas with me.

0 likes
39 replies
yazeedayyash's avatar

you need to create observer in model

   protected static function boot()
    {
        parent::boot();
        static::saving(function ($model) {
            $model->id= 'your structure here';
        });
    }

automica's avatar

@MohamedTammam

use Carbon\Carbon;
protected static function boot()
{
    parent::boot();
    static::saving(function ($model) {

        $prefix = Carbon::now()->format('Y-m');

        $count = $model->where('created_at', 'LIKE', $prefix . '%')->count();
        $model->slug = $prefix . '-' . $count++;
    });
}

This is assuming you arent deleting any orders.

MohamedTammam's avatar

@automica Yeah, that's exactly what I reached so far and yea I don't physically delete any orders from DB.

The only problem I have now is, how to ensure the uniqueness of it and not getting an error for unique constrain if I get too many orders at once.

bugsysha's avatar

I would use UUID.

static::saving(fn($model) => $model->uuid = \Ramsey\Uuid\Uuid::uuid4());
bugsysha's avatar

Or older syntax.

static::saving(function ($model) {
    $model->uuid = \Ramsey\Uuid\Uuid::uuid4();
});
bugsysha's avatar

@MohamedTammam you can always pull it through sha1().

static::saving(function ($model) {
    $model->uuid = sha1(\Ramsey\Uuid\Uuid::uuid4());
});
automica's avatar

@bugsysha I'd stayed away from defining the actual id, as that should either be incremental or uuid.

I would suggest OP is actually trying to define an order reference which would be more appropriate as in

YYYY-MM-{id} format

1 like
automica's avatar

@bugsysha I’d be using autoincrement integer or uuid for model id and what I suggested previously tor order reference

bugsysha's avatar

@automica what would be the benefit of such approach? Auto-incrementing ID can be used to keep things more readable. UUID can be used on a presentation layer. One doesn't exclude the other. But still can't figure out why would you want to have such a format for the orders.

automica's avatar

@bugsysha Year, Month and current order number that month is much easier to parse for a human than a UUID. its obvious that

2021-10-001 and 2021-10-002 are orders from the same month.

bugsysha's avatar

@automica I understand the readability side, but I was hoping there was something else I was missing. For me that is only important for invoicing systems. For all other use cases, I would go with something "random". If you check Amazon you will see that they don't use such an approach. But thanks for clarifying.

bugsysha's avatar

@MohamedTammam business requirements are sometimes stupid and you should point those situations out. Like I said, only invoicing systems to some extent make sense for such a format. Where you need people to be able to roughly guess the invoice number. But even that is debatable. If you are going with the mentioned format then shouldn't you somehow represent a user also in the invoice number? That way you can remember some important customers and find their orders even easier.

1 like
jlrdw's avatar

@MohamedTammam I agree with @bugsysha above, there are times when you have to explain these things and go with plan b. It will be hard to get unique numbers like you asked. You could monthly have a table of numbers, use one and delete from table, but that will not always prevent duplicates.

Just saying when you have multi-users at same time, this has been a problem for many web programmers. Note also that even using random sometimes repeats.

The table per month with numbers could work but you would have to implement locking on that table.

MohamedTammam's avatar

@jlrdw I will of course try to explain that to them, but for now I'm testing all approach on another column not the ID column. so at least I'm preventing errors with relationship if I had duplicates.

Snapey's avatar

can't think of a good way to do this without introducing the possibility of a race condition where two orders are being created at the same time and get the same number.

Ignoring race condition for now, you need to find the highest number so far in this month (database query) then increase it and then construct the new reference.

The problem is that between your read of the database to find the highest record, and your saving of the order reference, another user in another thread could also read the database and get the same answer.

The only solution I know to avoid this is to lock the table between the query and the insert.

1 like
bugsysha's avatar

@Snapey thank you. One more point to why it needs to be scoped with the user-id in the order number.

Snapey's avatar

A safer option...

Don't store the order reference at all. Insert all your orders using autoincrement id and then calculate the orderRef on the fly when you need to display it.

You can get all the order id's for the month of the order, and then find the position of the order in the collection.

eg, in your order model;

public function getOrderRefAttribute()
{
    $year = $this->created_at->format('Y');
    $month = $this->created_at->format('m');

    $monthOrders = Order::whereYear('created_at',$year)
	    		->whereMonth('created_at',$month)
		    	->orderBy('id',ASC)
			    ->pluck('id');

    $position = $monthOrders->search($this->id);

    $orderRef = $year . $month . '-' . $position+1;

    return $orderRef;
}

Warnings:

  1. This will perform a database query on every use of the accessor
  2. You cannot use route model binding or search records using this method.. You could by writing the number back to the model after its first access.
  3. if you allow orders to be deleted, then the order references will change.
1 like
newbie360's avatar

i think you can use sleep in queue, for example

for ($i = 0; $i < 10; $i++) {
    dispatch(function () use ($i) {
        sleep(10); // make sure sleep is top of other code
        Log::error($i);
    });
}

result, look the seconds part

[2021-10-08 09:37:23] local.ERROR: 0
[2021-10-08 09:37:33] local.ERROR: 1
[2021-10-08 09:37:43] local.ERROR: 2
[2021-10-08 09:37:53] local.ERROR: 3
[2021-10-08 09:38:03] local.ERROR: 4
[2021-10-08 09:38:13] local.ERROR: 5
[2021-10-08 09:38:23] local.ERROR: 6
[2021-10-08 09:38:33] local.ERROR: 7
[2021-10-08 09:38:43] local.ERROR: 8
[2021-10-08 09:38:53] local.ERROR: 9
newbie360's avatar

@bugsysha

maybe something like this, will this work ? imagine create 1000 invoice records at the same time, but the create action is proccess one by one

// use queue on create Invoice
dispatch(function () {
    sleep(5); // make sure sleep is top of other code

    $lastInvoiceNumber = Invoice::whereYear('created_at', now()->year)
        ->whereMonth('created_at', now()->month)
        ->orderByDesc('id')
        ->value('invoice_number'); // get first record value

    $newInvoiceNumber = is_null($lastInvoiceNumber)
        ? now()->format('Y-m-000001') // starting a new month format: 2021-11-000001
        : ++$lastInvoiceNumber;

    Invoice::create(['inovice_number' => $newInvoiceNumber]);
});
Snapey's avatar

@newbie360 what on earth are you on about?

sleep ?

have you heard of a counter?

This is really worrying considering you give others advice

newbie360's avatar

@Snapey

you mean this ?

++$lastInvoiceNumber;

$a = '2021-10-000011';
dd(++$a); // '2021-10-000012

yes i'm newbie for coding, i think remove sleep(5) is ok

and the code block isn't inside a loop, just imagine 100 peoples adding inovice at the same time, all goes to jobs table then proccess one by one

bugsysha's avatar

@newbie360 know that sleep(), usleep() and other similar purpose features are not scalable solutions. That should "never" be part of your code. Also, as pointed out by @snapey, that might have concurrency issues.

MohamedTammam's avatar

The best way I got so far.

I added unique constrain to the order number then I add UUID as a default value, that way I will make sure that the order will be created without errors with a unique value.

CreateOrdersTable

Schema::create('orders', function (Blueprint $table) {
            $table->id();
			// .... 
            $table->string('order_number')->unique()->default(DB::raw('UUID()'));
            $table->timestamps();
        });

Then I created an observer and in created method I try to update the order number 3 times, if for some reason it didn't pass after 3 times I will keep the UUID value.

CreateOrdersTable.php

public function created(Order $order) {
        $saved = false;
        $counter = 0;
        do {
            if($counter >= 3)
                break;

            $prefix = Carbon::now()->format('ym') . '-';
            $orderNumber = Order::whereMonth('created_at', date('m'))->whereYear('created_at', date('Y'))->count();
            $order->order_number = $prefix . $orderNumber;
                
            $counter += 1;

            try {
                $order->save();
                $saved = true;
            } catch (QueryException $e) {
                echo 'Exception \n';
                $saved = false;
            }
        } while (!$saved);
}

Now it works so far. I'm thinking of making a job that keep checking everyday or so if we have UUID value instead of the desired format then change it.

Please share your opinion with that approach.

Snapey's avatar

@MohamedTammam catching up afterwards will be a problem if you value the sequence of the numbers since the catch up order number will be at the end, and possibly also in a different month.

1 like
MohamedTammam's avatar

@Snapey I have created at and the normal ID which is auto_increment that I can validate using them.

That what got in my mind so far. Because that number only business-related stuff so I can only use them if I want to show the order ID.

Please or to participate in this conversation.