24

I am developing a event organization website. Here when the user registers for an event he will be given a unique random number(10 digit), which we use to generate a barcode and mail it to him. Now,

  1. I want to make the number unique for each registered event.
  2. And also random

One solution is to grab all the random numbers in an array and generate a random number using Php rand(1000000000, 9999999999) and loop through and check all the values. Grab the first value that doesn't equal to any of the values in the array and add it to the database.

But I am thinking that there might be a better solution to this. Any suggestion?

Nasif Md. Tanjim
  • 3,862
  • 4
  • 28
  • 38
  • By *"unique for each registered event"* do you mean that a random number for event A needs to be distinguishable from a random number for event B? – Mark Miller Feb 15 '15 at 08:49
  • I didn't think of that interpretation, but it seems logical. If yes, my answer would be easy to modify though. – Joel Hinz Feb 15 '15 at 08:55

11 Answers11

36

You can use php's uniqid() function to generate a unique ID based on the microtime (current time in microseconds)

Example:

<?php
echo uniqid();
?>

Output:

56c3096338cdb
jezmck
  • 1,138
  • 3
  • 18
  • 38
Abhijeet Ashok Muneshwar
  • 2,480
  • 2
  • 31
  • 32
  • 3
    *Warning* This function does not guarantee uniqueness of return value. Since most systems adjust system clock by NTP or like, system time is changed constantly. Therefore, it is possible that this function does not return unique ID for the process/thread. Use more_entropy to increase likelihood of uniqueness. – Diego Vieira Aug 03 '20 at 10:14
31

Your logic isn't technically faulty. However, if your application attracts lots of users, fetching all of the random numbers may well become unnecessarily expensive, in terms of resources and computation time.

I would suggest another approach, where you generate a random number and then check it against the database.

function generateBarcodeNumber() {
    $number = mt_rand(1000000000, 9999999999); // better than rand()

    // call the same function if the barcode exists already
    if (barcodeNumberExists($number)) {
        return generateBarcodeNumber();
    }

    // otherwise, it's valid and can be used
    return $number;
}

function barcodeNumberExists($number) {
    // query the database and return a boolean
    // for instance, it might look like this in Laravel
    return User::whereBarcodeNumber($number)->exists();
}
Joel Hinz
  • 24,719
  • 6
  • 62
  • 75
  • with mt_rand(1000000000, 9999999999) I am getting only numbers that starts with 11..., 10... and 12... That is one strange random function – Mladen Janjetovic Jul 28 '15 at 07:31
  • @Joel Hinz Always quering against database for uniqueness might be cumbersome . So what abount generating current timestamp and appending it with some other string or random nubmers according to transaction id format.. .:) – Sanjay Mar 28 '19 at 09:57
  • @SanjayKhadka It's a span of 1000000000, 9999999999... unless the database contains millions of products, that isn't going to be a problem. Plus, I was going by OP's specifications. – Joel Hinz Mar 28 '19 at 10:54
  • in laravel 8 I've got the error "undefine function call". As a solution call the function as below. if( $this->barcodeNumberExists($number)) { } – asela daskon Mar 12 '21 at 14:27
  • @aseladaskon They're just functions. If you put them in a class, you need to use `$this->` to call them. – Joel Hinz Mar 12 '21 at 20:32
  • Just a suggestion, using EPOCH time might help. Probably, EPOCH + mt_rand() could be one of the good possibility. So if atall, a DB call is required, we can be sure that it wont exceed more than 1 service call. $number = time() . mt_rand(1000, 999); – Harry Jul 04 '21 at 19:10
6

This is good:

do {
   $refrence_id = mt_rand( 1000000000, 9999999999 );
} while ( DB::table( 'transations' )->where( 'RefrenceID', $refrence_id )->exists() );
Hadi Note
  • 1,386
  • 17
  • 16
  • 1
    LIMIT number of loops/iteration of do-while block by using count in WHILE condition blocks otherwise it may took long time/infinite looping if there are large number of entries and range length is short (4-6 for OTP). – Satish Jul 14 '20 at 01:40
4

To avoid the problem of having to check to see if a matching code exists every time a new one is created, I just catch MySQL's duplicate record exception (error code 1062). If that error is caught, I just call the function again until the save is successful. That way, it only has to generate a new code if it collides with an existing one. Runs a lot faster -- but obviously gets a bit slower as your number of users approaches the number of possible barcodes.

function generateBarcode($user_id) {
    try {
        $user = User::find($user_id);
        $user->barcode = mt_rand(1000000000, 9999999999);
        $user->save();

    } catch (Exception $e) {
        $error_info = $e->errorInfo;
        if($error_info[1] == 1062) {
            generateBarcode($user_id);
        } else {
            // Only logs when an error other than duplicate happens
            Log::error($e);
        }

    }
}

So just loop through all the users you want to assign a code to:

foreach(User::all() as $user) {
    generateBarcode($user->id);
}

You could also add some logic to escape the function loop if a maximum number of attempts are made, but I've never bothered because collisions are unlikely.

3

Looping through the array won't be that efficient. If your database becomes too large then it slow down the entire process and also there might be a rare situation when 2 threads are looping through the array for the same random number and it will be found available and return same number to both the tickets.

So instead of looping through the array you can set the 10 digit registration id as primary key and instead of looping through the array you can insert the registration details along with randomly generated number, if the database insert operation is successful you can return the registration id but if not then regenerate the random number and insert.

Alternate solution which will be more effective Instead of 10 digit random numbers you can use timestamp to generate a 10 digit unique registration number and to make it random you can randomize the first 2 or 3 digits of the timestamp

Pavan Jiwnani
  • 274
  • 1
  • 5
3

One Solution could be like this:

use Illuminate\Support\Facades\Validator;
private function genUserCode(){
    $this->user_code = [
        'user_code' => mt_rand(1000000000,9999999999)
    ];

    $rules = ['user_code' => 'unique:users'];

    $validate = Validator::make($this->user_code, $rules)->passes();

    return $validate ? $this->user_code['user_code'] : $this->genUserCode();
}

Its generating a random number between 1000000000 and 9999999999. After that, it validates the number against the table. If true then it returns the number, otherwise runs the function again.

2

I made something like this

/**
 * Generate unique shipment ID
 * 
 * @param int $length
 * 
 * @return string
 */ 
function generateShipmentId($length)
{
    $number = '';

    do {
        for ($i=$length; $i--; $i>0) {
            $number .= mt_rand(0,9);
        }
    } while ( !empty(DB::table('shipments')->where('id', $number)->first(['id'])) );

    return $number;
}
Mladen Janjetovic
  • 13,844
  • 8
  • 72
  • 82
2
<?php
declare(strict_types=1);

namespace App\Helpers;


use App\Exceptions\GeneratorException;

class GeneratorHelper
{
    public static $limitIterations = 100000;

    /**
     * @param string $column
     * @param string $modelClass
     * @return string
     * @throws GeneratorException
     */
    public static function generateID(string $modelClass, string $column): string
    {
        return self::run(
            $modelClass,
            $column,
            self::IDGenerator(),
            'Generation id is failed. The loop limit exceeds ' . self::$limitIterations
        );
    }

    /**
     * @param string     $modelClass
     * @param string     $column
     * @param \Generator $generator
     * @param string     $exceptionMessage
     * @param array      $whereParams
     * @return string
     * @throws GeneratorException
     */
    protected static function run(string $modelClass, string $column, \Generator $generator, string $exceptionMessage, array $whereParams = []): string
    {
        try {
            foreach ($generator as $id) {
                $query = $modelClass::where([$column => $id]);
                foreach ($whereParams as $param) {
                    $query->where(...$param);
                }
                if (!$query->first()) {
                    return $id;
                }
            }
        } catch (\Throwable $e) {
            $exceptionMessage = $e->getMessage();
        }

        throw new GeneratorException($exceptionMessage);
    }

    protected static function IDGenerator(): ?\Generator
    {
        for ($i = 1; $i <= self::$limitIterations; $i++) {
            yield (string)random_int(1000000000, 9999999999);            
        }
        return null;
    }
}

sample usage

$card->number = GeneratorHelper::generateID(Card::class, 'number');
Takamura
  • 302
  • 1
  • 4
  • 16
  • 1
    This is a really good answer and should be accepted as an answer. However, what does the GeneratorException.php file look like? – rebellion Mar 18 '19 at 10:29
1

Helper (app/Helpers/Constants.php)

<?php
namespace App\Helper;

class Constants
{
    public static function getUniqueId($model){
        $id =  mt_rand(1000000000, 9999999999);
        if($model->find($id))
            return self::getUniqueId($model);
        return $id;
    }
}
?>

Model (app/Models/User.php)

<?php

namespace App;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public static function boot(){
        parent::boot();
        $creationCallback = function ($model) {
            if (empty($model->{$model->getKeyName()}))
                $model->{$model->getKeyName()} = Constants::getUniqueId(new self());
        };
        static::creating($creationCallback);
    }
}
?>

Explanation: Instead of calling the getUniqueId method in every controller. You can write it inside model.

Soubhagya Kumar Barik
  • 1,979
  • 20
  • 26
1

From the @Joel Hinz answer :

public function set_number() {
    $number = mt_rand(1000000000, 9999999999);

    return User::where('number', $number)->exists() ? $this->set_number() : $number;
}

Hope that helped.

1

for me, I prefer using MySQL way, because when you have a large amount of data in your DB, you will have too much quires to check your number uniqueness, for example, a code like this:

 do {
     $code = random_int(100000, 99999999);
    } 
while (AgentOrder::where("order_number", "=", $code)->exists());

so , this "do while" loop would be excueted too many times.

to avoid that, you can use MySQL way like:

  private function getUniqueCodeNumber()
    {
        $value = AgentOrder::query()->selectRaw('FLOOR(1000000 + RAND() * 10000000) AS generatedCode')
            ->whereRaw("'generatedCode'  NOT IN (SELECT order_number FROM agent_orders WHERE agent_orders.order_number IS NOT NULL)")
            ->limit(1)->first();
        if ($value == null) return 100000000;
        $value = (int)$value->generatedCode;
        return $value;
    }

this answer is inspired from this answer.

OMR
  • 11,736
  • 5
  • 20
  • 35