Build Your Own URL Shortener With PHP and PostgreSQL
Time to read: 11 minutes
Recently, my colleague Niels wrote an excellent post showing how to build URL shortener using .NET, and Redis. On reading it, I was so inspired that I decided to build my own URL shortener with PHP and PostgreSQL instead of .NET and Redis, as I know those technologies far better.
So what’s so fascinating about building a URL shortener? Especially when there are quite a number of other ones, such as Bit.ly, Short.io, or Twilio Messaging Services Link Shortening service. To be honest, the concept just grabbed my attention and inspired me.
It doesn’t have all of the features you’d likely expect in a professional service, such as URL customisation, analytics and click tracking, branded links, or the ability to add CTAs (Calls To Action). However, it does contain the essentials.
Tutorial Prerequisites
To follow along with this tutorial you will need the following:
- Docker Engine and Docker Compose OR PHP 8.1 with the PDO and PDO_PGSQL extensions installed and enabled and PostgreSQL 14 or above
- Your favourite IDE or code editor
- Composer installed globally
I thought I'd approach this tutorial a little differently than my previous ones and offer the choice of running the application with Docker Compose or directly with PHP's built-in web server and PostgreSQL.
The reason behind this is that getting PHP and PostgreSQL set up as required on your local development machine may take more time than it's worth and distract from the tutorial. Whereas, with Docker Compose it can all be done within, about, 60 seconds.
Feel free to set up PHP and PostgreSQL though, if that's what you prefer. You can find the database schema in the GitHub repository which accompanies this tutorial, along with the other instructions that you need in the repo’s README.md file.
If you’re new to Docker Compose and want a bit of extra support, then grab a copy of my free book. It has all that you need to know to get up and running quickly.
Application Overview
Before you dive too deep into the tutorial, let’s go over how the application will work. The application will be composed of three routes:
- The first route (the default) renders a form where the user can enter a longer URL to be shortened. On submission, if the form passes validation, then the URL will be shortened. Then, both the original and shortened URLs will be stored in the database.
- The second route retrieves an un-shortened URL from a shortened one. If the shortened URL is found in the database the user will be redirected there. If not, then the user will be redirected to the application’s 404 page.
- The third route is the application’s 404 page.
The application is a small Slim Framework app composed, largely, of two classes: a URL shortener service (UrlShortenerService
) and a database persistence service (UrlShortenerDatabaseService
). The application only directly interacts with the URL shortener service, as that service contains the database persistence service, a member variable, which handles database interaction.
Now, let’s get building!
Create the project directory
As (almost) always, the first thing to do is to create the project's directory structure, which is reasonably shallow and uncomplicated.
To create it, run the command below.
If you're using Microsoft Windows, use the following command instead.
Set the required environment variables
The next thing to do is to set the environment variables which the application requires to interact with the database.
Download .env.example, from the GitHub repository for this project, into the project's top-level directory and name it .env. Feel free to change the default values for any of the variables starting with DB_
to match your PostgreSQL server's configuration.
The database schema
Here’s the database schema in all its glory. Just one table, named urls
which contains three columns:
- long: This contains the long (original) URL
- short: This contains the shortened URL
- created_at: This is an automatically inserted timestamp of the time that the row was created
If you’re using Docker Compose, the database will be initialised for you when you start the application. If not, then run the SQL above using psql (PostgreSQL’s interactive terminal) or your database tool of choice (such as DataGrip or the database tool in PhpStorm).
Add the required dependencies
The next thing to do is to add all of the dependencies that the project needs. These are:
Dependency | Description |
---|---|
laminas/laminas-db | laminas-db provides an excellent database abstraction layer, and SQL abstraction implementations. |
laminas/laminas-diactoros | laminas-diactoros provides an implementation of PSR HTTP Messages. It’s been included because I find that the custom response classes are an intuitive way of returning responses from requests. |
laminas/laminas-inputfilter | laminas-inputfilter filters and validates data from a range of sources, including files, user input, and APIs. |
laminas/laminas-uri | laminas-uri helps with manipulating and validating URIs (Uniform Resource Identifiers). |
php-di/slim-bridge | Slim Bridge integrates PHP-DI, an excellent dependency injection (DI) container, with Slim. |
slim/psr7 | This library integrates PSR-7 into the application. It's not strictly necessary, but I feel it makes the application more maintainable and portable. |
slim/slim | This is the core of the Slim micro framework |
slim/twig-view | This package integrates the Twig templating engine making it easier to create response bodies. |
vlucas/phpdotenv | PHP dotenv helps keep sensitive configuration details out of the code (and version control). |
To install them, run the command below.
Add a PSR-4 autoloader
The next thing that you need to do is to add a PSR-4 autoloader, which the three classes that you’re going to write will need. To add it, add the configuration below, after the require property in composer.json.
Then, run the command below to update Composer’s autoloader.
Write the code
Now, it’s time to write the code!
UrlShortenerPersistenceInterface
The first thing you’re going to do is to create an interface. Sure, it’s not strictly necessary. But I’m a big believer in program to interfaces not implementations. So I hope you’ll humour me on this point.
In src/UrlShortener create a new file named UrlShortenerPersistenceInterface.php and in that file add the code below.
The interface defines three functions:
getLongUrl()
: retrieves a long URL using the short one providedhasShortUrl()
: checks if a short URL existspersistUrl()
: stores a long and short URL combination in the database
UrlShortenerDatabaseService
The next thing to do is to create another new file in src/UrlShortener named UrlShortenerDatabaseService.php and in that file add the following code.
This class provides a concrete implementation of UrlShortenerPersistenceInterface
, using laminas-db to connect to the PostgreSQL database backend.
getLongUrl()
attempts to retrieve any long URL (contained in the column named long
) from the urls
table which has a short URL (contained in the short
column) that matches the supplied short URL ($shortUrl
).
hasShortUrl()
determines if a short URL exists by retrieving a count of all rows in the urls
table whose short
column value matches the short URL provided. Finally, persistUrl()
inserts a new long and short URL combination into the urls
table.
UrlShortenerService
The third and final class is UrlShortenerService
. Create a new file in src/UrlShortener named UrlShortenerService.php and in that file add the following code.
As mentioned earlier, this is the class that the application directly uses to provide its functionality. It is initialised with a UrlShortenerPersistenceInterface
object so that it can interact with a backend datastore. Its implementations of getLongUrl()
and hasShortUrl()
just proxy to the methods of the same name on the UrlShortenerPersistenceInterface
object.
Its implementation of getShortUrl()
, however, calls the shortenUrl()
method, which shortens the supplied long/original URL before passing the shortened URL to persistUrl()
, persisting both the long and short URL in the database.
shortenUrl()
uses a combination of PHP’s substr, base64_encode, sha1, random_bytes, and uniqid functions to generate a 9-character URL. It starts by generating 32 random bytes. From those random bytes it generates a unique identifier which factors in the current time in microseconds to help ensure uniqueness.
Then, a SHA1 of the unique id is generated, which is then Base64-encoded to create a URL-like representation of the hash. Finally, the string is truncated to just the first nine characters which is within the range of most modern shortened URLs, and that string is returned.
I don’t know, for sure, if there will be collisions and if so how often, but this is not meant to be a super-sophisticated implementation.
A big thank you to Nomad PHP for the core of this function.
Create the bootstrap file
Next, it’s time to create the bootstrap file, where all requests to the application are routed. Create a new file in public named index.php and in that file add the following code.
The code starts off by importing all of the classes that it will make use of, along with Composer's auto-generated autoloader. Then (as I regularly do in my Twilio tutorials) it uses PHP Dotenv to import the required environment variables from the .env file, which you created and populated earlier.
After that, it initialises a new DI container instance ($container
) and registers three services with the container:
- The first is an InputFilter instance which will be used to filter and validate the URL submitted in the default route's form. To successfully validate, the submitted string needs to be a valid URL (because of the Uri validator) and not already exist in the
urls
table (because of the NoRecordExists validator). In addition, the string will be trimmed and stripped of any HTML tags. - The second service provides a laminas-db Adapter object which will be indirectly used by the
UrlShortenerService
so that it can connect to the database. - The third service provides a
UrlShortenerService
object. This takes a TableGateway object, which is initialised with theAdapter
object, returned from the so-named container service, allowing it to interact with the database to store, check for, and retrieve both long and short URLs.
After that, a new Slim App object ($app
) is initialised and passed the DI container object so that each of the application's routes can access the container's services. TwigMiddleware is added to the App
object, so that each registered route can return responses by rendering Twig templates.
Then, two routes are defined. The first one is the application's default route. It accepts both GET and POST requests. If the route is requested with a POST request, the request's POST data is retrieved and validated using the InputFilter
service. If the data fails validation, two template variables are set:
- The URL submitted in the form
- Errors showing why the form failed validation
If the form passes validation, the UrlShortenerService
is retrieved from the container and an attempt is made to shorten the URL and persist the shortened URL, along with the original URL, to the database. If successful, three template variables are set:
- The shortened URL
- The original, longer, URL
- A flag to indicate that the form was submitted successfully
At this point, or if the route is requested with a GET request, src/templates/default.html.twig is rendered with the template variables and returned as the body of the response.
Then, comes the second route which accepts only GET requests. The route's path must match /
followed by nine characters. These can be any combination of lower or upper case letters between A & Z, and numbers between 0 & 9; for example: /NDdmOTM3M
.
If the requested route matches, then the path after the forward slash is stored in a request argument named url
. Then, using the InputFilter
service the URL is validated and filtered. If it passes validation, the UrlShortenerService
checks if the shortened URL exists in the database. If so, the original URL linked to it is retrieved and returned. Otherwise, an HTTP 404 response is returned, telling the user that the shortened URL was not found.
Then, error middleware is added to each request and handled by an anonymous function. This middleware is there to handle the 404 responses that the application two routes can return. It does so by first retrieving the response from the Slim application object and then setting the response's body as the result of rendering src/templates/404.html.twig, which you'll see shortly.
After that, $app
's run()
method is called to launch the application.
Create the templates
You’re just about finished building the logic of the application. Now, it's time to create the Twig templates for the default route and for the 404 page.
Create the core template
In src/templates, create a new file named base.html.twig. In that file, paste the following code.
This template provides content common to both templates i.e., the head, body, and footer elements. If you're not familiar with Twig, note the use of code such as {% block h1_header %}{% endblock %}
.
These blocks form the basis of template inheritance. This is where one template, a base template, can be extended by other templates. When they do so, they can set the content of these blocks in a way that makes sense in that context. You'll see examples of this shortly.
Create the default route's template
Now, create another new file in src/templates, this time named default.html.twig. In that file, paste the code below.
This is the template for the default route. It starts off by extending base.html.twig, allowing it to set the content that will appear in any of the blocks defined in that template. Specifically, it sets the page's title and H1 header to "PHP Url Shortener". Then, it sets the page's main content to a small form where the user can input a URL to shorten.
If a URL is successfully shortened, the shortened URL will be displayed above the form along with the original URL. Should the form not validate successfully, the form validation errors will be displayed above the form instead.
Create the 404 page's template
Finally, create a third new file in src/templates, this time named 404.html.twig. In that file, paste the code below.
As with the default route's template, this template starts off by extending base.html.twig. It sets the page's title to "404 URL Not Found!" and the page's H1 header to "Oops! That URL Wasn't Found". Finally, it sets the body to "Sorry to say it, but that URL isn't available." along with a link to the default route so that the user can attempt to shorten a link.
Download the stylesheet
Now, there's one last thing to do, which is to download the stylesheet from the GitHub repository that accompanies this tutorial, so that the application renders as expected. Download it to public/css and name it styles.css.
Test that the application works
Now that you've finished putting the application together, it's time to test that it works.
Start the application using Docker Compose
Before you can start the application, download a zip archive containing a docker-composer.yml file, and all of the supporting files. Then, extract its contents in the project's top-level directory.
After that, start the application by running the following command:
Otherwise, run the following command to use PHP's built-in web server to run the application.
Regardless of how you started the application, it will now be available on localhost on port 8080. Open it in your browser of choice, where it should look like the screenshot below.
Now, pick a URL to shorten, such as https://www.youtube.com/watch?v=dQw4w9WgXcQ, enter it into the text field, and click "Shorten the URL". You'll then see a confirmation appear above the text field showing the URL that you entered on the left and the shortened, clickable, URL on the right-hand side.
If you enter a URL that has already been shortened or a string that isn't a valid URL, you'll see errors, similar to in the screenshot below, rendered in the form.
Now, attempt to open a non-existent shortened URL, such as http://localhost:8080/ztgxody2n_/. You'll be redirected to the 404 page where you'll see that the URL was not found, as in the example below.
That's how to build your own URL shortener with PHP and PostgreSQL!
While it isn't the most sophisticated URL shortener you could create, it's still a good start. How would you improve it?
Matthew Setter is a PHP Editor in the Twilio Voices team and a PHP and Go developer. He’s also the author of Mezzio Essentials and Docker With Docker Compose. When he’s not writing PHP code, he’s editing great PHP articles here at Twilio. You can find him at msetter[at]twilio.com, as well as on Twitter and GitHub.
"chained" by timlewisnm (used in the background of the tutorial's main image) is licensed under CC BY-SA 2.0.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.