মুহুর্তেই তৈরি করে ফেলুন আপনার নিজের লিংক শর্টেনার সার্ভিস

আমাদের প্রতিদিন ছোটখাটো অনেক কাজে bit.ly, rebrand.ly, cutt.ly এর মতো নানান সার্ভিস ব্যবহার করে থাকি। কিন্তু এইগুলোই কাষ্টম ডোমেইন ব্যবহার করতে চাইলে আবার কিছু টাকা খরচ করা লাগে। তারা যদিও বেশ কিছু ফিচার দিয়ে থাকে। তবে যদি আপনি চান নিজের একটা একই ধরণের সার্ভিস তৈরি করবেন তাহলে এই আর্টিকেল তা আপনার জন্য।

আমরা এই টুলটা তৈরি ব্যবহার করবো PHP (>=৭.৪) এবং SQLite সার্ভারলেস ডেটাবেজ। এই দুইটা পছন্দ করার প্রথম কারণ হলো পিএইচপি হোস্ট করা সহজ। আর যেখানে PHP হোস্ট করা যায় সেখানে SQLite নিয়ে কিছুই করা লাগে না। এছাড়াও যেহেতু এইটা একটা মাইক্রো সার্ভিসের মতো, তাই আমাদের SQLite কে পছ্ন্দ করা উচিৎ হবে বলে মনে করছি।

এই টুটলটি তৈরি করার জন্য আমরা মাত্র দুইটা ফাইল ব্যবহার করবো। প্রথমটা হচ্ছে index.php এবং অন্যটা হবে links.db যেটা আমাদের সব লিংক গুলা সংরক্ষণ করে রাখা হবে।

প্রথম ধাপ: লিংক ম্যানেজ করার জন্য LinkManager

এখানে আমরা একটা সিম্পল ক্লাস লিখবো যেটাতে আমরা ২ টা পাবলিক মেথড রাখব।

  • store(): যেটা দিয়ে আমরা নতুন যেকোনো লিংক শর্ট করে ডেটাবেজ-এ সেভ করবো
  • find(): যেটা দিয়ে আমরা তৈরি করা লিংক ডেটাবেজ থেকে খুঁজে করে ভিজিট করবো

ক্লাসের পূর্বরূপ: আমরা index.php ফাইলের পূর্বরূপ দেখছি

class LinkManager
{
    public function store(string $url): string
    {
        //
    }
    public function find(string $str): ?string
    {
        //
    }
}

এই ক্লাসে আপতত কিছুই নাই, তবে আমরা যা করতে যাচ্ছি তা এখানে এখানে লিখে রেখে দিলাম এতে করে আমরা কি করতে চাচ্ছি তা হয়ত ভুলে যাব না।

দ্বিতীয় ধাপ: ডেটাবেজ এর সাথে সংযোগ স্থাপন

প্রথমেই বলেছি আমরা SQLite ডেটাবেজ ব্যবহার করবো। তাই আমাদের LinkManager ক্লাস যেনো আমাদের links.db তে ডেটা লেখা ও মুছে ফেলতে পারে এর জন্য আমাদের __construct এ ডেটাবেজ লোকেশন দেখিয়ে দিয়ে একটা SQLite3 অবজেক্ট তৈরি করে নিতে হবে নিচের মতো করে।

class LinkManager
{
    private SQLite3 $db;
    
    public function __construct(?string $dbPath = null)
    {
        $this->db = new SQLite3($dbPath ?? __DIR__ . '/links.db');
    }

    public function store(string $url): string
    {
        //
    }
    public function find(string $str): ?string
    {
        //
    }
}

যদি আমরা index.php ফাইলটা চালু করি, তাহলে একই ডিরেক্টরিতে যেখানে index.php ফাইলটি আছে সেখানে links.db নাম দিয়ে একটা ডেটাবেজ ফাইল তৈরি হয়ে যাবে। তবে আপনি চাইলে যেকোনো সময় $dbPath দিয়ে ডেটাবেজ লোকেশন পরিবর্তন করতে পারেন এতে আপনার সিকিউরিটি নিয়ে চিন্তা কমে যাবে।

তৃতীয় ধাপ: ডেটাবেজ ডিজাইন করা

আমরা আমাদের লিঙ্কগুলো সংরক্ষণ করার জন্য links নাম দিয়ে একটা টেবিল তৈরি করবো। একই সাথে সেখানে আমরা id, url, clicks, created_at, updated_at কলাম গুলা রাখব। এর জন্য আমরা LinkManager-এ initDb() নাম দিয়ে একটা মেথড তৈরি লিখবো এবং সেখানে $db কে ব্যবহার করে একটা স্টেটমেন্ট চালু করবো।

private function initDb(): void
{
    $this->db->exec('CREATE TABLE IF NOT EXISTS links (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        url TEXT NOT NULL,
        clicks INTEGER DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )');
}

তৃতীয় ধাপ: লিংক শর্ট করার জন্য আইডি এনকোড ও ডিকোড

আমরা দেখতে পারি শর্টেনার সার্ভিস গুলা a-z, A-Z এবং 0-9 এর মিশ্রণ ব্যবহার করে একটা ইউনিক স্ট্রিং জেনারেট করে। আবার অপশন থাকে কাষ্টম স্ট্রিং ব্যবহার করেও লিংক তৈরি করা যায়। তবে আমাদের সিস্টেমে শুধুমাত্র অটো জেনারেটেড স্ট্রিং এর ব্যবহার করা হবে, কাষ্টম স্ট্রিং ব্যবহার এর সুবিধা থাকছে না।

আমরা ধরে নিচ্ছি আমরা abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 এই অক্ষরগুলো অনুমোদন করবো। এই অক্ষরগুলা LinkManager ক্লাসে নতুন প্রপার্টি হিসেবে অ্যাড করে রাখব এবং আমরা নতুন দুইটা মেথড লিখবো।

class LinkManager
{
    private string $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    
    // ...

    private function encodeId(int $id): string
    {
        $length = strlen($this->chars);
        $code   = '';

        while ($id > 0) {
            $code .= $this->chars[$id % $length];
            $id   = (int) ($id / $length);
        }

        return $code;
    }

    private function decodeId(string $str): int
    {
        $length = strlen($this->chars);
        $id     = 0;

        for ($i = 0; $i < strlen($str); $i++) {
            $id += strpos($this->chars, $str[$i]) * pow($length, $i);
        }

        return $id;
    }
    
    // ...
}

এখানে encodeId() মেথডটি যেকোন নাম্বার থেকে ওটার সমতুল্য একটা স্ট্রিং $chars এর মধ্যে থেকে তৈরি করে দিবে। আর decodeId() টা আবার উল্টোটা করবে, মানে যেকোন স্ট্রিং থেকে নাম্বার বের করে দিবে। এই ছোট মজার দুইটা মেথড আসলে আপনাকে অফুরন্ত স্ট্রিং তৈরি করতে সাহায্য করবে।

চতুর্থ ধাপ: লিংক গুলাকে ডেটাবেজে সেভ করা

লিংক সেভ করার করার জন্য আমাদের একটা স্টেটমেন্ট চালু করতে হবে তার জন্য আমাদের $db অবজেক্ট কে ব্যবহার করতে হবে। লিংক সেভ হলে সেটার ইউনিক আইডি নিয়ে আমাদের কে encodeId() দিয়ে স্ট্রিং তৈরি করে রিটার্ন করতে হবে।

public function store(string $url): string
{
    $query = $this->db->prepare("INSERT INTO links (url) VALUES (:url)");
    $query->bindValue(':url', $url);
    $query->execute();

    // Return the encoded id.
    return $this->encodeId($this->db->lastInsertRowID());
}

পঞ্চম ধাপ: স্ট্রিং দিয়ে লিংক খুজে করা

সেভ করা লিংক খুঁজে বের করা আসলে জটিল কিছু না, তবে আমরা তো লিংকের সাথে জেনারেট করা স্ট্রিং সেভ করিনি। এই কারণেই আমাদেরকে স্ট্রিং থেকে আবার ডিকোড করে নাম্বারে বের করতে হবে এর জন্য আমরা decodeId() মেথডটা করবো যেটা আগেই লিখে ফেলেছিলাম।

public function find(string $str): ?string
{
    $id     = $this->decodeId($str);
    $search = $this->db->prepare("SELECT url FROM links WHERE id = :id");
    $search->bindValue(':id', $id, SQLITE3_INTEGER);
    $result = $search->execute()->fetchArray(SQLITE3_ASSOC);

    // we can't find the link.
    if (!$result) {
        return null;
    }

    // Increase the click count and update the updated_at column.
    $update = $this->db->prepare("UPDATE links SET clicks = clicks + 1, updated_at = CURRENT_TIMESTAMP WHERE id = :id");
    $update->bindValue(':id', $id, SQLITE3_INTEGER);
    $update->execute();

    return $result['url'];
}

এই মেথড তা আসলে দুইটা কাজ করবে: ১) লিংক খুঁজে বের করবে ২) খুঁজে পাইলে সেটাতে একটা কাউন্ট বাড়িয়ে দিবে একই সাথে updated_at এ নতুন একটা সময় বসিয়ে দিবে। এতে করে আমরা সাধারণ কিছু পরিসংখ্যান পাবো। যেমন: কতবার ক্লিক হলো ও শেষবার কোন সময়ে ভিজিট করা হয়েছে।

ষষ্ঠ ধাপ: ফরম, রিডাইরেক্ট ও গুছানো

আমাদের ক্লাসটা রেডি বলা চলে, তবে যেহেতু উপরের কোডগুলো গুছানো নাই, তাই আমরা একটু আমাদের কোডগুলা গুছিয়ে নিলে নিচের মতো দেখাবে:

class LinkManager
{
    private string $chars;

    private SQLite3 $db;

    public function __construct(?string $dbPath = null, ?string $allowedChars = null)
    {
        $this->db    = new SQLite3($dbPath ?? __DIR__ . '/links.db');
        $this->chars = $allowedChars ?? 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

        $this->initDb();
    }

    /**
     * Initialize default data.
     *
     * @return void
     */
    private function initDb(): void
    {
        $this->db->exec('CREATE TABLE IF NOT EXISTS links (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            url TEXT NOT NULL,
            clicks INTEGER DEFAULT 0,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
        )');
    }


    /**
     * Encode the ID to a string based on the allowed characters.
     *
     * @param int $id
     *
     * @return string
     */
    private function encodeId(int $id): string
    {
        $length = strlen($this->chars);
        $code   = '';

        while ($id > 0) {
            $code .= $this->chars[$id % $length];
            $id   = (int) ($id / $length);
        }

        return $code;
    }

    /**
     * Decode the string to an ID.
     *
     * @param string $str
     *
     * @return int
     */
    private function decodeId(string $str): int
    {
        $length = strlen($this->chars);
        $id     = 0;

        for ($i = 0; $i < strlen($str); $i++) {
            $id += strpos($this->chars, $str[$i]) * pow($length, $i);
        }

        return $id;
    }

    /**
     * Store the URL and return the encoded ID.
     *
     * @param string $url
     *
     * @return string
     */
    public function store(string $url): string
    {
        $query = $this->db->prepare("INSERT INTO links (url) VALUES (:url)");
        $query->bindValue(':url', $url);
        $query->execute();

        // Return the encoded id.
        return $this->encodeId($this->db->lastInsertRowID());
    }

    /**
     * Find the URL by the encoded ID.
     *
     * @param string $str
     *
     * @return string|null
     */
    public function find(string $str): ?string
    {
        $id     = $this->decodeId($str);
        $search = $this->db->prepare("SELECT url FROM links WHERE id = :id");
        $search->bindValue(':id', $id, SQLITE3_INTEGER);
        $result = $search->execute()->fetchArray(SQLITE3_ASSOC);

        // we can't find the link.
        if (!$result) {
            return null;
        }

        // Increase the click count and update the updated_at column.
        $update = $this->db->prepare("UPDATE links SET clicks = clicks + 1, updated_at = CURRENT_TIMESTAMP WHERE id = :id");
        $update->bindValue(':id', $id, SQLITE3_INTEGER);
        $update->execute();

        return $result['url'];
    }
}

এখন এই ক্লাসটা ব্যবহার করে লিংক জেনারেট ও ভিজিট করার জন্য নিচের কোড টুকু লিখতে পারি।

$manager = new LinkManager();

$path = 'https://example.com';
if (!empty($_REQUEST['url']) && filter_var($_REQUEST['url'], FILTER_VALIDATE_URL)) {
    $code = $manager->store($_GET['url']);

    echo "Your Link is: $path/$code";

    exit;
}

$path = parse_url($_SERVER['REQUEST_URI'])['path'];

if ($path === '/') {
    echo '<form method="get">
        <input type="text" name="url" placeholder="Enter your URL">
        <button type="submit">Shorten</button>
    </form>';

    exit;
}

$url = $manager->find(substr($path, 1));

header('Location: ' . $url);

এখন আমরা যদি https://example.com/?url=https://google.com এ ভিজিট করি তাহলে দেখব আমাদের জন্য একটা শর্ট লিংক তৈরি হয়েছে। এখন যদি আমরা সেই লিংকে ভিজিট করি তাহলে আমদেরকে রিডাইরেক্ট করে https://google.com নিয়ে যাবে।

তবে সার্ভার সেটাপ করার ক্ষেত্রে অবশ্যই সব ট্রাফিক যেনো index.php যায় এটা খেয়াল রাখতে হবে। আমার গিটহাব রিপোজিটরিতে .htaccess ফাইল রয়েছে সেটা সহ আপলোড করলে Apache, বা Litespeed সার্ভারে সুন্দরভাবে কাজ করবে।

সোর্স কোড: https://github.com/AminulBD/simple-url-shortener

শেষের কথা:

অনেকদিন পরে বাংলাতে কিছু লেখার চেষ্টা করলাম। বাংলা না আসলে অনেকদিন পরে কিছু একটা লিখলাম। এই কোডে,বানানে বা ভাষাগত কিছু ভুল থাকতে পারে। ভুল হলে মতামত দিয়ে জানিয়ে দিলে উপকৃত হব এবং একই সাথে উপরের দেয়া গিটহাব রিপোজিটরিতে ইস্যু ওপেন করতে পারেন অথবা কন্ট্রিবিউট করতে পারেন পুল রিকুয়েস্ট দিয়ে।

2 thoughts on “মুহুর্তেই তৈরি করে ফেলুন আপনার নিজের লিংক শর্টেনার সার্ভিস

Leave a Reply

Your email address will not be published. Required fields are marked *