Building Your Own URL Shortener

Most of us are familiar with seeing URLs like bit.ly or t.co on our Twitter or Facebook feeds. These are examples of shortened URLs, which are a short alias or pointer to a longer page link. For example, I can send you the shortened URL http://bit.ly/SaaYw5 that will forward you to a very long Google URL with search results on how to iron a shirt. It would be much easier to text the 20-character bit.ly URL to your son who is in college and preparing for his first big job interview.

In this article you’ll learn how to create a fully functional URL shortener for your website that will work whether you use a front controller/framework or not. If you use a front controller, I’ll discuss you how to easily integrate this URL shortener without having to dig into the controller’s programming.

Answering Some Common Questions

So with bit.ly and many other URL shorteners like it out there and freely available, why should we bother building our own? Most of these shortening services even have an easy-to-use API so that we can programmatically generate a shortened URL, and use it within our PHP scripts.

The best reasons are for convenience, aesthetics and brand recognition. If for example your website has an application that creates a large amount of reports, a very active blog or a large photo album, there will be a lot of links. A URL shortener will allow you to programmatically create a clean, simple link that can be emailed to your readers or published on your website. The obvious advantage to having your own is that your readers have instant brand recognition with your website.

You may wonder why you always see letters mixed with numbers in shortened URL’s. By having more than ten options (0-9) per digit, we are able to have dramatically more combinations while keeping the code as short as possible.

The characters we’ll be using are the digits 1-9 along with various upper/lowercase letters. I have removed all of the vowels to prevent having links created which are unintended bad words, and I have removed any characters that could be confused with each other. This gives us a list of about 50 characters available for each digit, which means that with two characters, we have 2,500 possible combinations, 125,000 possibilities with three characters, and a whopping 6.5 million combinations with just four characters!

Planning the Database

Let’s set up the short_urls table. It’s a simple table and the create statement is found below:

CREATE TABLE IF NOT EXISTS short_urls (
  id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
  long_url VARCHAR(255) NOT NULL,
  short_code VARBINARY(6) NOT NULL,
  date_created INTEGER UNSIGNED NOT NULL,
  counter INTEGER UNSIGNED NOT NULL DEFAULT '0',

  PRIMARY KEY (id),
  KEY short_code (short_code)
)
ENGINE=InnoDB;

We have our standard auto-incrementing primary key and fields for the full URL, the shortened code for the URL (indexed for faster retrieval), a timestamp when the row was created, and the number of times the short URL has been accessed.

Note that the long_url field has a maximum length of 255 characters, which should be sufficient for most applications. If you need to store longer URLs then you’ll need to change its definition to TEXT.

Now on to the PHP!

Creating a URL Short Code

The code to create and decode short URL codes will be in a class named ShortUrl. First, let’s look at the code responsible for creating the short codes:

<?php
class ShortUrl
{
    protected static $chars = "123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ";
    protected static $table = "short_urls";
    protected static $checkUrlExists = true;

    protected $pdo;
    protected $timestamp;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
        $this->timestamp = $_SERVER["REQUEST_TIME"];
    }

    public function urlToShortCode($url) {
        if (empty($url)) {
            throw new Exception("No URL was supplied.");
        }

        if ($this->validateUrlFormat($url) == false) {
            throw new Exception(
                "URL does not have a valid format.");
        }

        if (self::$checkUrlExists) {
            if (!$this->verifyUrlExists($url)) {
                throw new Exception(
                    "URL does not appear to exist.");
            }
        }

        $shortCode = $this->urlExistsInDb($url);
        if ($shortCode == false) {
            $shortCode = $this->createShortCode($url);
        }

        return $shortCode;
    }

    protected function validateUrlFormat($url) {
        return filter_var($url, FILTER_VALIDATE_URL,
            FILTER_FLAG_HOST_REQUIRED);
    }

    protected function verifyUrlExists($url) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_NOBODY, true);
        curl_setopt($ch,  CURLOPT_RETURNTRANSFER, true);
        curl_exec($ch);
        $response = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        return (!empty($response) && $response != 404);
    }

    protected function urlExistsInDb($url) {
        $query = "SELECT short_code FROM " . self::$table .
            " WHERE long_url = :long_url LIMIT 1";
        $stmt = $this->pdo->prepare($query);
        $params = array(
            "long_url" => $url
        );
        $stmt->execute($params);

        $result = $stmt->fetch();
        return (empty($result)) ? false : $result["short_code"];
    }

    protected function createShortCode($url) {
        $id = $this->insertUrlInDb($url);
        $shortCode = $this->convertIntToShortCode($id);
        $this->insertShortCodeInDb($id, $shortCode);
        return $shortCode;
    }

    protected function insertUrlInDb($url) {
        $query = "INSERT INTO " . self::$table .
            " (long_url, date_created) " .
            " VALUES (:long_url, :timestamp)";
        $stmnt = $this->pdo->prepare($query);
        $params = array(
            "long_url" => $url,
            "timestamp" => $this->timestamp
        );
        $stmnt->execute($params);

        return $this->pdo->lastInsertId();
    }

    protected function convertIntToShortCode($id) {
        $id = intval($id);
        if ($id < 1) {
            throw new Exception(
                "The ID is not a valid integer");
        }

        $length = strlen(self::$chars);
        // make sure length of available characters is at
        // least a reasonable minimum - there should be at
        // least 10 characters
        if ($length < 10) {
            throw new Exception("Length of chars is too small");
        }

        $code = "";
        while ($id > $length - 1) {
            // determine the value of the next higher character
            // in the short code should be and prepend
            $code = self::$chars[fmod($id, $length)] .
                $code;
            // reset $id to remaining value to be converted
            $id = floor($id / $length);
        }

        // remaining value of $id is less than the length of
        // self::$chars
        $code = self::$chars[$id] . $code;

        return $code;
    }

    protected function insertShortCodeInDb($id, $code) {
        if ($id == null || $code == null) {
            throw new Exception("Input parameter(s) invalid.");
        }
        $query = "UPDATE " . self::$table .
            " SET short_code = :short_code WHERE id = :id";
        $stmnt = $this->pdo->prepare($query);
        $params = array(
            "short_code" => $code,
            "id" => $id
        );
        $stmnt->execute($params);

        if ($stmnt->rowCount() < 1) {
            throw new Exception(
                "Row was not updated with short code.");
        }

        return true;
    }
...

When we instantiate our ShortUrl class, we’ll pass it our PDO object instance. The constructor stores this reference and sets the $timestamp member.

We call the urlToShortCode() method passing it the long URL that we wish to shorten. The method wraps up everything needed to create the short URL code, which we will appended to our domain name.

urlToShortCode() calls validateUrlFormat() which simply uses a PHP filter to make sure that the URL is properly formatted. Then, if the static variable $checkUrlExists is true, verifyUrlExists() will be called which uses cURL to contact the URL and make sure that it doesn’t return a 404 (Not Found) error. You could alternatively check for a 200 (OK) status, but this could cause issues if the page were to unexpectedly return a 301 (Moved) or 401 (Unauthorized) response code.

It doesn’t make sense to have duplicate entries, so the code checks for that with urlExistsInDb() which queries the database for the long URL. If it finds the URL, it will return the corresponding short code, otherwise it returns false so we know we need to create it. Note that http://www.example.com and http://example.com are different URLs, so if you want to prevent this kind of duplication then you will have to add some regular expressions.

createShortCode() delegates the following tasks to specific methods:

  1. insertUrlInDb() to insert the long URL into the database and return the new row’s ID.
  2. convertIntToShortCode() to convert the new row’s ID to our base-50 number scheme.
  3. insertShortCodeInDb() to update the row with the newly created short code.

When we want to create a short URL, all we have to do is instantiate the class, passing a PDO instance to the constructor, call the urlToShortCode() method with the long URL we wish to shorten, and append the returned short code to the domain and pass it back to the controller that requested it.

<?php
include "../include/config.php";
include "../include/ShortUrl.php";

try {
    $pdo = new PDO(DB_PDODRIVER . ":host=" . DB_HOST .
        ";dbname=" . DB_DATABASE,
        DB_USERNAME, DB_PASSWORD);
}
catch (PDOException $e) {
    trigger_error("Error: Failed to establish connection to database.");
    exit;
}

$shortUrl = new ShortUrl($pdo);
try {
    $code = $shortUrl->urlToShortCode($_POST["url"]);
    printf('<p><strong>Short URL:</strong> <a href="%s">%1$s</a></p>',
        SHORTURL_PREFIX . $code);
    exit;
}
catch (Exception $e) {
    // log exception and then redirect to error page.
    header("Location: /error");
    exit;
}

Decoding a Short Code

The code to decode a short code and obtain the long URL is part of the ShortUrl class too. We call the shortCodeToUrl() method and pass it the short code we have extracted from the URI. shortCodeToUrl() also accepts an optional parameter, $increment, which defaults to true. It then delegates the following:

  1. validateShortCodeFormat() makes sure that the provided short code only contains letters and numbers.
  2. getUrlFromDb() queries the database for the supplied short code and returns the record’s id, long_url, and counter fields.
  3. If the $increment parameter is true, incrementCounter() is called to increment the row’s counter field.

Here’s the rest of the class:

...
    public function shortCodeToUrl($code, $increment = true) {
        if (empty($code)) {
            throw new Exception("No short code was supplied.");
        }

        if ($this->validateShortCode($code) == false) {
            throw new Exception(
                "Short code does not have a valid format.");
        }

        $urlRow = $this->getUrlFromDb($code);
        if (empty($urlRow)) {
            throw new Exception(
                "Short code does not appear to exist.");
        }

        if ($increment == true) {
            $this->incrementCounter($urlRow["id"]);
        }

        return $urlRow["long_url"];
    }

    protected function validateShortCode($code) {
        return preg_match("|[" . self::$chars . "]+|", $code);
    }

    protected function getUrlFromDb($code) {
        $query = "SELECT id, long_url FROM " . self::$table .
            " WHERE short_code = :short_code LIMIT 1";
        $stmt = $this->pdo->prepare($query);
        $params=array(
            "short_code" => $code
        );
        $stmt->execute($params);

        $result = $stmt->fetch();
        return (empty($result)) ? false : $result;
    }

    protected function incrementCounter($id) {
        $query = "UPDATE " . self::$table .
            " SET counter = counter + 1 WHERE id = :id";
        $stmt = $this->pdo->prepare($query);
        $params = array(
            "id" => $id
        );
        $stmt->execute($params);
    }
}

Bringing It All Together

Building/altering a front controller or tailoring this package to an existing framework are outside the scope of this article, and so I’ve opted to contain our decoding logic in a file named r.php (r standing for redirect). We can write our shortened URLs as http://example.com/r/X4c where r.php (or r/index.php depending on your design) will be the controller. This format will be easy to integrate into just about any framework without touching the existing front controller.

On a related note, if you would like to learn how to build your own front controllers, check out the excellent series An Introduction to the Front Controller Pattern.

One advantage of this design is that, if you wanted to, you can have a separate controller for different parts of your site using different tables to keep the short codes organized and as short as possible. http://example.com/b/ could be for blog posts, and http://example.com/i/ could be for images.

“But what if I don’t use a front controller or framework?” you ask, “Did I just read this whole article for nothing?” Although it’s not as pretty, you can use the format http://example.com/r?c=X4c where r/index.php contains the decoding script.

Here’s what r.php looks like:

<?php
include "../include/config.php";
include "../include/ShortUrl.php";

// How are you getting your short code?

// from framework or front controller using a URL format like
// http://.example.com/r/X4c
// $code = $uri_data[1];

// from the query string using a URL format like
// http://example.com/r?c=X4c where this file is index.php in the
// directory http_root/r/index.php
$code = $_GET["c"];

try {
    $pdo = new PDO(DB_PDODRIVER . ":host=" . DB_HOST .
        ";dbname=" . DB_DATABASE,
        DB_USERNAME, DB_PASSWORD);
}
catch (PDOException $e) {
    trigger_error("Error: Failed to establish connection to database.");
    exit;
}

$shortUrl = new ShortUrl($pdo);
try {
    $url = $shortUrl->shortCodeToUrl($code);
    header("Location: " . $url);
    exit;
}
catch (Exception $e) {
    // log exception and then redirect to error page.
    header("Location: /error");
    exit;
}

Depending on how you are getting the short code, the variable $code is set along with your other configuration settings. We establish our PDO connection, instantiate an instance of ShortUrl, and call shortCodeToUrl() passing it the short code and leaving the counter setting the default value. If the short code is valid, you’ll have a long URL which you can redirect the user to.

In Closing

So there you have it, your very own URL shortener that is incredibly easy to add to your existing site. Of course, there are plenty of ways that this package could be improved, such as:

  • Abstract your database interaction to remove redundant code.
  • Add a way to cache shortened URL requests.
  • Add some analytics to the requested short URLs beyond the counter field.
  • Add a way to filter out malicious pages.

I’d would like to take this opportunity to thank Timothy Boronczyk for his patient advice throughout my writing process. It was an honor to write this article for SitePoint and to work with him.

Feel free to fork this article’s sample code on GitHub and share your contributions and improvements.

Thanks for reading and happy PHPing!

Image via Fotolia

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://9px.ir علیرضا

    thanks.
    but it seems better to use base_convert() php function and base “36″, so you can delete last field from your db table :)

    • http://alexfraundorf.com Alex Fraundorf

      That certainly is an option that would make the entire process a LOT easier, and it is one that I considered. I chose to use this approach so that I would have complete control over what characters were used in the base conversion. I wanted to make sure that I eliminated vowels to prevent bad words from accidentally showing up in shortened URLs. There’s a lot of bad three letter words out there!
      Another benefit of using 50 characters instead of 36, is that you have 125,000 URL’s you can shorten with only 3 characters instead of 46,656.
      Thank you very much for reading and for the feedback! Happy PHPing!

  • Łukasz Krzyszczak

    I wonder if the insertShortCodeInDb() method is needed. Now there are two queries made to the DB: one two insert long url and second to insert the short one. Why not to use just one query, which would be better performance wise?

    • http://alexfraundorf.com Alex Fraundorf

      Hi Lukasz,
      That is an excellent point, but one that I could not find a safe way to avoid.
      When the row is initially created the long URL, counter and timestamp are inserted and the auto-incrementing ID is returned. It is this ID that is used for conversion to the base-50 number. I could not find any other way to do it that would prevent possible collisions, especially if the URL shortener is open to the public and getting a lot of use.
      In summary, I felt is was better to have a little extra processing and an extra database call in the creation process to maintain the integrity of the conversion. I was able to keep the retrieval process simple and fast (although it also uses two database calls if the counter is enabled), which is what will be used more often.
      If you do come up a more efficient way to handle the creation process, please make sure to share it on GitHub.
      Thank you very much for reading and for the feedback! Happy PHPing!

      • skydiver

        I think if each $id is associated with a $short_url, it’s not needed to store $short_url in the database. When we want to look up a short url, we can convert it to id and find it in database. So we only need one query in the process.

        • http://alexfraundorf.com Alex Fraundorf

          Skydiver,
          Thanks for reading and for the comment. Your solution would certainly work. The reason I didn’t use a process like that is because it would require a conversion every time a shortened URL is looked up. I opted for a little extra processing when the shortened URL is being created and less every time it is being retrieved, as I was expecting far more retrievals than creations. I think it is more a matter of preference than anything else.
          Thanks for the feedback and happy PHPing!

      • ELK

        Did you consider using SQL functions to convert the auto_increment field to/from a string?
        This way, you no longer need to keep the short url field in the DB.

        • http://alexfraundorf.com Alex Fraundorf

          ELK,
          Thanks for reading. I did not even look into if an SQL base-number conversion function was available. I come from the school of thought that you should keep as much of your logic as possible, especially your complicated logic, in PHP instead of SQL unless it has a noticeable effect on the processing time.
          Thanks for the comment and happy PHPing!

  • sydo

    I think the auto incremented ‘id’ field is not needed in the database. You can use the short_code field as a primary key because it’s an unique identifier.

    • http://alexfraundorf.com Alex Fraundorf

      Hi Sydo,
      I actually looked into that; however, as shown in the above comment, the auto incremented ID is used in the conversion process to create the base-50 short code.
      The only way I was able to guarantee that there were not collisions was to tie the value of the short code to the ID. Without the ID column in the table, you would have to retrieve the most recent short code, decode it to its integer value, increment it and re-convert it back to a short-code, which would also potentially open the door to a duplicate collision.
      On a related note, and to hopefully head off the question, the reason that the short_code field is indexed, but not UNIQUE, is because during the creation process it is for a very short time NULL. If there are two or more entries created at the same time which is possible in a high volume situation, or if an error causes a row with a NULL value, there would be more than one row with a NULL value in the short_code column which would create an SQL error if that column was UNIQUE.
      This was the best way that I could find to run the creation process with my primary goal being to prevent collisions and maintain the integrity of the conversion.
      If you do find a better way to do it, please make sure to share your findings.
      Thank you very much for reading and for the feedback! Happy PHPing!

  • http://www.itoctopus.com itoctopus

    Hi Alex,
    This is a very comprehensive post – but the fact of the matter is, a self-owned URL shortener is only needed by very large companies such as Google, Amazon, and the likes. And in this situation, the code above will not work really well as it’s not optimized.
    I personally don’t see a reason for using URL shorteners outside Twitter – which now shortens all URLs by default.

    • http://alexfraundorf.com Alex Fraundorf

      Hi itoctopus,
      Thank you for your feedback and for the compliment on the article.
      I guess the need/want for a self-owned URL shortener is a matter of opinion. Some clients will want one, and some couldn’t care less. As I said in the opening section, there are a number of great public shorteners out there, and most have a well documented API that make them easy to integrate.
      I think the most compelling reason to have your own is domain/brand recognition, but this is not important to everyone.
      I think it is worth mentioning that with your own shortener it is now possible to generate SLIGHTLY shorter URLs than you can with bit.ly or t.co which are now using 6 or more characters for their code. I have been able to easily find 4 digit .ws or .us domain names for clients which result in 11 character URLs (xxxx.ws/Cx7) instead of 13 or more using a public shortener. Again, I think it comes down to a personal preference, and what works best for your particular client.
      As for the optimization, you are absolutely right. I parred down this code to the bare essentials for the purpose of showing a basic, working example that had as few outside dependencies as possible. There is plenty of room for improvement and optimization in the example shown, although it does function perfectly well out of the box. The version that I use in production is more optimized and integrates with the rest of my framework.
      Thank you very much for reading and for the feedback. I appreciate you sharing your point of view.
      Happy PHPing!

      • Roy

        Alex, your statement “I think the most compelling reason to have your own is domain/brand recognition, but this is not important to everyone.” I agree with 100% that IS the reason to want your own branded URL shortner.
        I wish I had time to mess around with code now days, but just way to busy, haven’t coded anything from scratch since my VIC 20 days of the early 1980′s, and that was a check book :)

        Nice artical, thanks for posting it.

        Roy

        • http://alexfraundorf.com Alex Fraundorf

          Hi Roy,
          I’m very happy to hear that you enjoyed the article, and I really appreciate the feedback.
          Maybe we can get you back into coding, even if its just as a fun distraction. I’m sure you know what a slippery slope it can be ;-)
          Thanks for reading!

  • Case Sensitivity

    You mention using upper and lowercase letters, but doesn’t MySQL ignore case (at least by default)?
    SELECT id, long_url FROM short_urls WHERE short_code = “bcdf” LIMIT 1;
    and
    SELECT id, long_url FROM short_urls WHERE short_code = “BcDf” LIMIT 1;
    would return the same results. This would greatly reduce the number of possible short_codes by disallowing all permutations of b, c, d and f in my example (e.g. bcdf, bcdF, bcDf, bCdF, BCdf, BCdF).

    How does bitly handle this? For example, http://bit.ly/SaaYw5 and http://bit.ly/sAAyW5 are not the same, but if I’m not mistaken, would be exactly the same in your implementation.

    • http://alexfraundorf.com Alex Fraundorf

      That is an excellent point, and an issue that I was not aware of until writing this article.
      To avoid that issue, the short_code column needs to have a BINARY attribute so that MySQL (or what ever database engine you use) will recognize case sensitivity.
      When I ran my table export it came out as “CHARACTER SET utf8 COLLATE utf8_bin”, but this may vary by database.
      Thank you very much for bringing up this point as it is vital to the proper functioning of the module.
      Thanks for reading and for the feedback. I really do appreciate it!
      Happy PHPing!

      • ELK

        Well, it should work with any collation which is not case insensitive (the ones ending with _cs or _bin).

        • http://alexfraundorf.com Alex Fraundorf

          ELK,
          I am not an expert in SQL, but as shown in my 09/25/2012 comment, I had to have a column type of VARBINARY (BINARY would work as well) in order to have case sensitivity function properly. For whatever reason, just having the character set did not do the trick.
          Thanks for reading and for the feedback.

    • http://alexfraundorf.com Alex Fraundorf

      I have looked into this further, and found that I had made a mistake in the database table’s schema. To make sure that the short_code field is treated as binary, and therefore case sensitive, the type needs to be VARBINARY.
      We will get the code sample and GitHub repo updated as soon as possible.
      Thank you very much to this poster for helping me to catch this bug!

  • http://www.nwidesigns.com Kevin

    A follow up article that shows how to build a tool to decipher these shortened URL’s would also help those that would inadvertently click a link where an undesirable used this tutorial to create links that circumvent AV tools and other online database tools that record websites that host malicious files. While this program could be used for good it will more than likely be used in malicious ways.

    • http://alexfraundorf.com Alex Fraundorf

      Hi Kevin,
      I do have a function that decodes the short code to its integer value. I will update this reply later tonight to include it for anyone who is interested. We left it out of the example code intentionally to keep it as simple and unbloated as possible.
      I agree with you 100% that the possibility of using a tool like this to disguise malicious pages is a real threat. Hopefully more good will come of it than mis-use.
      Thank you very much for reading and the feedback. You brought up a very good point.
      Happy PHPing!

      Update:
      Obviously it is not necessary to decode the short code to retrieve the long URL; however, it can be fun to play with. For anyone who is interested, here is the method used to convert a short code (base-50 numeral) back to a base-10 integer. Happy PHPing!


      /**
      * Converts the base-x numeral (short code) to its base-10 integer
      * equivalent. If successful, the base-10 integer will be returned.
      * If there is an error, an exception will be thown and false will be
      * returned.
      * Note: This method is not used by the package. It is only used for
      * testing and for having some fun.
      *
      * @param string $short_code the short code to convert to an integer
      * @uses string self::$characters the list of allowed characters to use
      * @return integer|boolean the base-10 integer eqivalent of the short code
      * on success - false on failure
      * @throws Exception if an error occurs
      *
      * @version updated 09/16/2012
      * @since 08/04/2012
      */
      public function codeToInteger($short_code) {
      // validate input
      if($short_code == null) {
      // $short_code is null
      // throw an exception and return false
      throw new Exception('The short code to decode is missing.');
      return false;
      }
      // get the length of the available characters
      $characters_length = strlen(self::$characters);
      // get the length of the string to decode
      $code_length = strlen($short_code) - 1;
      // split up the input string
      $code_array = str_split($short_code);
      // extract the last character in the code
      // note: we do this because the last character does not need to be
      // multiplied by the base factor
      $last_char = array_pop($code_array);
      // make sure the last character is valid
      if(! strpos(self::$characters, $last_char)) {
      // the last character is an invalid character
      // throw an exception and return false
      throw new Exception('There is an invalid character in the short
      code.');
      return false;
      }
      else {
      // determine the value of the last character and set its value to
      // the $output
      $output = strpos(self::$characters, $last_char);
      }
      // loop through each character (except the last one) and decode its
      // numeric value
      foreach($code_array as $i => $char) {
      // check to make sure $char is a valid character
      if(! strpos(self::$characters, $char)) {
      // the short code contains an invalid character
      // throw an exception and return false
      throw new Exception('There is an invalid character in the short
      code.');
      return false;
      }
      // calculate the numeric value of $char by determining is position
      // within self::$characters, and multiply that by the calculated
      // base of the numbering system we are using to determine its
      // true base 10 value
      // add this value to $output
      $output += strpos(self::$characters, $char) *
      pow($characters_length, $code_length - $i);
      }
      // ensure that $output is an integer
      $output = intval($output);
      // return the integer
      return $output;
      } // end of codeToInteger

  • http://www.webstix.com Tony

    Yeah, I like the branding idea. With your own URL shortener, you’re promoting your own brand on websites like Twitter and Facebook. If you’re a website design company, you also look more capable with your own system like that – even if it’s just used internally and not meant for free, public use. Good article idea to write about. I like it.

    • http://alexfraundorf.com Alex Fraundorf

      Hi Tony,
      Thank you very much for reading and for the feedback. I’m really glad to hear that you liked the article.
      Happy PHPing!

  • http://headsnet.com Ben

    One thing to note is that with recent versions of MySQL you can extend the length of VARCHAR fields to be much more than 255 characters. I’ve been using VARCHAR fields of 2048 characters in a recent application. My understanding of this is that it will be much more efficient than using a TEXT field for really long URLs

    • http://alexfraundorf.com Alex Fraundorf

      I was not aware of that improvement. Thank you very much for sharing that!

  • Sky

    Thx for sharing this nice bit of code Alex.

    • http://alexfraundorf.com Alex Fraundorf

      Sky,
      Thank you for the feedback. I’m very glad that you enjoyed the article. Thanks for reading and happy PHPing!

  • Alex Gervasio

    Hey Alex,
    Maybe I getting a little bit late to the party, but I have to say I really enjoyed the article. Concise, clear and straight to the point, so great work indeed. Just as a humble contribution, and assuming you don’t mind, here’re some improvements you may want considering to add to your nifty URL shortener:
    1) Get rid of the static fields, ASAP. In the past we’ve all been avid consumers of static properties, specially with Singletons, (not to mention those mutable, horrifying static methods). But OOP is about interconnecting objects where each one carries a different state. So using static members doesn’t make much sense in this case in particular.
    2) While the responsibility of each method is admittedly clearly separated, IMHO the class is doing too many things at the same time, which the big powers say is bad, as it infringes the Single Responsibility Principle, thus ignoring the venerable Separation of Concerns mantra. Shielding several unrelated tasks behind the same API, the class accesses the database via PDO, builds the required queries here and there, and additionally implements the URL shortening algorithm. Perhaps you might want to encapsulate the whole shortening algorithm in a separate, polymorphic class, whose instance not only could be injected in the constructor or in a setter, but would let you implement different shortening algorithms, all without having to modify your client code. More flexibility, while coding to interfaces, all in the same package :).
    We all are here to learn from each other, so I don’t want to sound picky, trust me. It’s just a couple of suggestions I thought you might want to keep in mind. Keep up the good work :)

    • http://alexfraundorf.com Alex Fraundorf

      Alex,
      Thank you very much for your feedback and point of view. As anyone who is a regular reader of this site knows, your advice should carry a lot of weight, and it does with me.
      I agree completely that breaking up the class into a set of more single-responsibility classes working together is the way to go for production.
      I would really appreciate hearing more about your suggestion for replacing the static properties. I felt it was an appropriate solution for values within the object that would not be changing through its lifespan, but I am here to learn as well, and am certainly open to correction :-)
      I will definitely be taking your advice to heart as I work on my next article.
      Thank you again for your feedback. I REALLY appreciate it! Happy PHPing!

      • Alex Gervasio

        Hey Alex,
        Thanks for the extensive reply indeed. About your question on static class members, the fact of declaring them static doesn’t mean that their values will remain constant at all. Static fields are (when declared public, which is an OOP horror as this exposes data directly to the outside word, hence breaking up encapsulation) accessible without having to create instances of the originating class. Plus, it’s worth mentioning they maintain their state across multiple instances.
        If you ever need to make your fields constant, then just use constants or simply appeal to plain old instance-level properties. As a rule of thumb, try to stay away from any “static” thing. OOP is all about objects, which means working with actual instances, not with classes, which are mere blueprints of them. Keep up the good work :)

        • http://alexfraundorf.com Alex Fraundorf

          Alex,
          Thank you very much for the reply, the explanation and for correcting me on my mis-use of static properties.
          It certainly looks like class constants are the way to go for these types of values.
          Hopefully in a future version, PHP will allow for an option to set the visibility of these class constants to protected or private.
          Thank you again for your insight and for sharing your expertise. I’m sure that many will learn from it as I have.
          All the best!
          P.S. Sorry for the delayed reply.

          • Alex Gervasio

            Glad to hear that. Private, protected constants in upcoming PHP releases? Can’t wait to see it :)

  • http://www.netkili.com netkili

    i tried to do what u wrote above.but i could nt succed.i do not know where i made a mistake.but anyway really good to learn some new information.thanks.

    • http://alexfraundorf.com Alex Fraundorf

      Thanks for reading. Feel free to email me through my website (shown on my bio). I will do my best to help you figure out where you are having a problem.
      Happy PHPing!

  • help

    can you add the access permission,only admin can use the shorturl!

    • http://alexfraundorf.com Alex Fraundorf

      We have not provided a working example of the shortener that is open to the public (for security reasons). You can download the source code from GitHub and install it on your own server. I hope that helps.

  • Chris

    Hello, and thanks for the really nice article!! Even though i really want to praise your ( extra-ordinary ) ability to reply to all comments, i will cut to the point as i dont want to spare much more of your time :) . Wouldn’t be nice to remove the DB alltogether and substitute it with memcache or redis etc with a certain lifespan for each entry, and a get-like url on the key that points to the actual url found on memcache/redis register ? thanks again for the great post!

    • http://alexfraundorf.com Alex Fraundorf

      Hi Chris,
      Thank you for reading and for the compliment. I’m really glad that you enjoyed the article.
      That is a very interesting idea on using caching instead of the database. My own personal preference is to use the database to keep track of everything and to ensure that all of the shortened URL’s are stored unless explicitly deleted, but there certainly are other ways to skin this particular cat.
      I didn’t touch on caching in this article because I wanted to keep it brief and simple. There is plenty of room for improvement on my example, and adding some type of caching would be a good thing.
      The script that inspired my project did employ some caching, so you might find it interesting. It can be found at http://briancray.com/posts/free-php-url-shortener-script. It has several features that I cut out the keep this example brief.
      Thanks again for reading and for your feedback. I REALLY appreciate it.
      Happy PHPing!

  • http://brig.co dave

    Im liking this tutorial you have created i made my own url shortener its up and running at brig.co.
    Thanks

  • http://alexfraundorf.com Alex Fraundorf

    Dave,
    I’m glad that you enjoyed the tutorial. Congratulations on your completed URL shortener. Did you build it using this article as an example or did you take a different approach?
    Thank you for reading and for your feedback. I really appreciate it.
    Happy PHPing!

  • Rohit

    hola ! alex i really like this short and sweet tut … can u now guide me to building its frontend (as to how to use these classes) ! as i am a noob in php i really dont know how to create a object in php class ( i do have a sound background in c++/java) thanks!

    • http://alexfraundorf.com Alex Fraundorf

      Rohit,
      I’m glad that you enjoyed the article. There is a very basic example of building a front end for the class (and creating its objects) on the github page for this project, but I will email you about it as well.
      Thanks for reading and happy PHPing!

  • Rohit

    Hi alex I have emailed You about What I am trying To achieve.. Please have a look @ your email and reply soon :)

    • http://alexfraundorf.com Alex Fraundorf

      Rohit,
      Please do not continue to post here about this. That is not what this comments section is for.
      I will try to assist you via email, but you need to be patient and allow at least 24 hours for me to get back to you with this FREE help!

  • http://www.qrmii.com QRmii

    Next step: http://www.qrmii.com
    A QRmii is a QR code which holds a short URL which is automatically redirected. Flashing a QRmii with a smartphone displays the page of the original URL.

    • http://alexfraundorf.com Alex Fraundorf

      There are several online services that will create QR codes for free. Thank you for sharing your link.
      For anyone interested in creating QR codes programmatically with PHP, please check out this article:
      http://phpmaster.com/generate-qr-codes-in-php/

  • Pritesh

    Hi Alex,
    Is it 100% secure? Can it be hacked via SQL injection?

    • http://alexfraundorf.com Alex Fraundorf

      Pritesh,
      I would never be so bold to say that anything I write is 100% secure, and keep in mind that what is secure today, might not be next month. We all need to do our very best to stay on top of security best practices as they continue to evolve.
      I used PDO with prepared statements for all database interaction which provides a great level of protection against SQL injection, and before the the URL is inserted into the database, it is validated.
      That being said, you should always treat user input as suspect and apply any additional filtering and validating as you see fit.
      Sandeep has a new article on the subject that you should find very interesting: http://phpmaster.com/8-practices-to-secure-your-web-app/
      I hope that helps.
      Thank you for reading the article, and for your comment. I really appreciate it.
      Happy PHPing!

  • http://GreatTut King

    Hello Alex,
    Thanks for the great tutorial, I really appreciate it.
    I wanted to build a service like this.

    • http://alexfraundorf.com Alex Fraundorf

      King,
      Thank you for reading and for your kind feedback. I am very glad that you enjoyed the article and found it useful.
      Happy PHPing!

  • Boaz

    Regarding the script that inspired your script:
    http://briancray.com/posts/free-php-url-shortener-script

    Just wondering is it possible
    to control the number of characters
    after the “/” ?, currently it shortens
    with the domain and has two charaters
    after the “/”,

    Example:

    MyWebSite.com/XX

    is it possible to control via
    the script the number of characters
    like say:

    MyWebSite.com/XXXXXX

    Thanks,
    C.W.

    • http://alexfraundorf.com Alex Fraundorf

      Hi C.W.,
      Thanks for the comment and question. Because there is a direct correlation between the database table’s primary ID and the shortened code, you can simply enter a dummy line with an ID of 125000 into your table. The next row entered by the script will be 125001, which should translate to a 4 digit short code, and you will be able to generate a little over six million short codes before it jumps to 5 digits.
      This is of course based on my script. If you are using Brian Cray’s version, your solution will probably need some tweaking, but should be similar.
      I hope that answers your question. If not please feel free to follow up here or contact me through my website.
      Thanks for reading and happy PHPing!

  • C.W.

    Thanks Chief,
    Wow that sounds simple enough even for me to implement,
    Moving forward with your program, thanks for answering.
    Sincerely,
    C.W.

    • http://alexfraundorf.com Alex Fraundorf

      C.W.,
      I’m glad I could help and that the shortener looks to be useful for you!
      Alex