Today we’ll continue off where we left with our PHP website running on Windows Azure Websites. At this point we’ve got a PHP website deployed which will allow us to specify shortened URLs (slugs) and their full URL. The only real visual interface is that we can list out those shortened URLs. Our real goal in all of this is to connect to the website (and the web service methods) via an Android and iOS app. However, before we get knee deep in native code, let’s go over the PHP code we deployed.
One thing to note is that for this site, I used the Silex PHP micro-framework. It’s not really important that you understand how Silex works to follow along or even to extend what the site already does. I’ll try to point out where things are “Silex specific” just in case. Again, I want to thank @Khepin for his URL Shortener built using Silex as this site is built off of that.
If you haven’t already done so, pull the code for the site down from here. Let’s start by looking at the index.php file found in the root directory:
<?php $app = require __DIR__.'/src/app.php'; $app['debug'] = true; $app->run();
This is including the app.php file which returns a Silex/Application and then runs it. The other two files in the root directory are a .htaccess file and a web.config. These files accomplish the same thing, but .htaccess is for use with Apache and the web.config is for IIS. Since you have both files, you can run this site locally on Apache (if you’re developing on OS X or Linux) and then move it up to Windows Azure and use the web.config. The .htaccess is as follows:
<IfModule mod_rewrite.c> Options -MultiViews RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php [QSA,L] </IfModule>
And the web.config is:
<?xml version="1.0"?> <configuration> <system.webServer> <rewrite> <rules> <rule name="Main Rule" stopProcessing="true"> <match url=".*" /> <conditions logicalGrouping="MatchAll"> <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> </conditions> <action type="Rewrite" url="index.php" /> </rule> </rules> </rewrite> </system.webServer> </configuration>
Both files are redirecting all requests to the index.php file. So all requests will end up being routed through app.php. Now, open up the src folder and open up app.php. There’s quite a bit going on here so we’ll break it up. Let’s start at the top:
<?php use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\ParameterBag; /** Bootstraping */ require_once __DIR__.'/../vendor/Silex/silex.phar'; $app = new Silex\Application(); $app['autoloader']->registerNamespaces(array('Khepin' => __DIR__,)); $app->register(new Khepin\ShortenerExtension(), array('url_file_name' => __DIR__.'/../resources/urls.ini')); $app->register(new Silex\Provider\TwigServiceProvider(), array( 'twig.path' => __DIR__.'/templates', 'twig.class_path' => __DIR__.'/../vendor/twig/lib' //Uncomment these lines to turn caching on (just make sure the directory is writeable) //, //'twig.options' => array('cache' => __DIR__.'/../cache'), )); $app['key'] = 'my_key';
The first two lines here are pulling in some components (Request and ParameterBag from Symfony (which is what Silex is based on)). The “require_once” line is pulling in the main silex library (silex.phar). Then we’re creating our Silex\Application and doing some registering and loading of classes. Twig is used here for templating the web page side of things (so you can have a decent web experience and not just native mobile). The last line just creates a key variable that we can check against when someone tries to add a new shortened URL. Next up:
/** Decodes JSON Requests */ $app->before(function (Request $request) { if (0 === strpos($request->headers->get('Content-Type'), 'application/json')) { $data = json_decode($request->getContent(), true); $request->request = new ParameterBag(is_array($data) ? $data : array()); } });
This “app->before” means that this code will be called on any requests coming into the system. Inside of this, we’re checking to see if the content type is “application/json” and if it is, we’re decoding the json. Now we can look at the real application code. There are some methods present in the app.php file that we won’t go over because they aren’t relevant to what we’re talking about. Though, everything is commented so you should be able to easily figure out what the other methods do.
/** Shows the home page */ $app->get('/', function() use ($app){ return $app['twig']->render('index.html.twig'); });
This says that if a blank request comes in (so if you go to “http://urlshortener.azurewebsites.net/”) then use twig to render the index.html twig template (located in src/templates).
/** Echos out the full URL for a SLUG */ $app->get('/{url_slug}',function($url_slug) use($app){ //NOTE: switch the commenting on these lines and instead of printing //out the URL, users will get redirected echo $app['shortener']->get($url_slug); //return $app->redirect($app['url_service']->get($url_slug)); });
This is what (would) handles redirecting a user from the shortened URL to the full URL. As you can see in the code, instead of actually redirecting them, we’re just echoing out what the full URL that corresponds to the slug sent in is. To actually do a redirection, you would just need to uncomment the app->redirect line.
/** Shows a view of all the URLs and their Slugs */ $app->get('/view/list', function() use($app){ return $app['twig']->render('list.html.twig', array('list' => $app['shortener']->getAll())); });
Here we’re using twig again and rendering the list.html template and we’re passing in the data retrieved from shortener->getAll() to the rendering engine. So, if you go to http://urlshortener.azurewebsites.net/view/list you’ll see a list of all of the URL slugs next to their full URLs.
/** Adds a URL via query string parameters alone */ $app->get('/add/{key}/{url_slug}', function($url_slug, $key) use ($app){ //Check that the key sent over is valid if($app['key'] != $key){ throw new Exception('Invalid key'); } $app['shortener']->add($url_slug, $app['request']->get('url')); return $app['twig']->render('add.html.twig', array( 'url_slug' => $url_slug, 'url' => $app['request']->get('url'))); });
This is the method that will handle adding a new URL (from a query string, not what we’ll use for our mobile clients). It expects the key that we defined up above to be sent in, along with the URL slug and the full URL. If the key doesn’t match, we throw an exception. We then call shortener->add(…) to add the shortened URL and finally we use twig to render the add.html template with the information for the URL passed into the renderer. Now, as I said above, this isn’t the method we’ll use for the mobile clients. For those, we have API specific methods that take in, and return, json data. Let’s have a look at these:
/** API Method to fetch all URLs */ $app->match('/api-getall', function () use ($app){ $response = array(); $response['Urls'] = $app['shortener']->getAll(); $response['Status'] = "SUCCESS"; return $app->json($response, 200); });
This is similar to the getAll method above except now we’re putting the URL data into an array and serializing it to json.
/** Gets the details for a single URL */ $app->match('/api-get', function (Request $request) use ($app){ $url_slug = $request->get('url_slug'); if ($app['shortener']->exists($url_slug)) { $response = array('Status' => "SUCCESS", 'Url_Slug' => $url_slug, 'Url' => $app['shortener']->get($url_slug)); } else { $response = array('Status' => "Does not exist"); } return $app->json($response, 201); });
Here, we’re checking to see if we have a shortened URL for the slug that is sent in. If we do, we add the data on that slug to the response array, if not, we set the Status to “Does not exist”. Again, we then serialize that to json and return it.
/** API method to add a new URL */ $app->match('/api-add', function (Request $request) use ($app){ $key = $request->get('key'); $url = $request->get('url'); $url_slug = $request->get('url_slug'); if($app['key'] != $key){ throw new Exception('Invalid key'); } if ($app['shortener']->exists($url_slug)) { $response = array('Status' => "Already Exists"); } else { try { $app['shortener']->add($url_slug, $url); $response = array('Status' => "SUCCESS"); } catch (Exception $e) { $response = array('Status' => "FAILURE"); } } return $app->json($response, 201); });
And finally, here is the api method to add a new URL. In this case, we’re pulling the data out of the request, checking to see if the URL slug already exists, and then adding it (provided we can). The response is serialized to json and that’s it. The last bit of code from app.php that I’ll highlight is the very last line:
return $app;
If you recall from index.php, we were pulling in app.php and then calling run on that. This is why it’s necessary to return the $app here. Quickly, let’s look at the src/Khepin/ShortenerExtension.php class:
class ShortenerExtension implements ServiceProviderInterface { public function register(Application $app){ $app['shortener'] = $app->share(function() use($app){ return new UrlShortener($app['url_file_name']); }); } }
This is what enables us to use $app[‘shortener’] inside of app.php. Since we register ShortenerExtension at the top of app.php we are free to call $app[‘shortener’] whenever we want to call a method on UrlShortener. Speaking of which, let’s look at that class next. Near the top you’ll see the connection string variables, which if you’ve pushed this to your own server, you’ve changed:
private $db_server = 'localhost'; private $db_user = 'phptestuser'; private $db_password = 'phptestuser'; private $db_name = 'shorty';
Beneath that is a regex we use to make sure URL’s are valid. Then we have our constructor which loads all of the data from the database:
/** Loads the URLs from the DB */ public function __construct($url_file_name) { $this->url_file = $url_file_name; $db_url_list = array(); $con = mysql_connect($this->db_server,$this->db_user,$this->db_password); if (!$con) { die('Could not connect: ' . mysql_error()); } mysql_select_db($this->db_name, $con); $result = mysql_query("SELECT Name, Url FROM Url"); while($row = mysql_fetch_array($result)) { $this->url_list[$row['Name']] = $row['Url']; } mysql_close($con); return $this->url_list; }
There isn’t anything complex here. We’re connecting to the database, doing a SELECT query and then filling an array (url_list).
/** Gets a sepcfici URL slug */ public function get($url_slug) { return $this->url_list[$url_slug]; } /** Checks to see if a specific slug exists */ public function exists($url_slug) { return isset($this->url_list[$url_slug]); }
These two methods nearly accomplish the same thing. One of them will return the URL info for a slug. The other one will return a boolean for if a slug exists.
/** Returns all URLs */ public function getAll(){ return $this->url_list; }
Another exceedingly straightforward method. This just returns the whole URL array.
/** Adds a new SLUG to the DB (and file) */ public function add($url_slug, $url) { if (!\preg_match(self::url_regex, $url)) { throw new \Exception('Invalid url'); } if (isset($this->url_list[$url_slug])) { throw new \Exception('Url short name already exists'); } $this->url_list[$url_slug] = $url; //comment this out to remove file writing $this->dump(); //Add to DB $con = mysql_connect($this->db_server,$this->db_user,$this->db_password); if (!$con) { die('Could not connect: ' . mysql_error()); } mysql_select_db($this->db_name, $con); $sqlInsert = "INSERT into URL (Name, Url) values ('$url_slug', '$url')"; if (!mysql_query($sqlInsert,$con)) { die('Error: ' . mysql_error()); } mysql_close($con); }
This method is a little bit more busy. First, we use the regex to make sure the URL is valid. Then we check to make sure the URL slug hasn’t already been used. Then we add the slug. We dump the URL array to a local file (optional) and finally, we insert to the database.
That’s all the code this website and service will use. There are a few more files like the templates in src/templates but I won’t go through those right now. You can dig into those on your own and let me know if you have questions. Next up, we’ll actually start making our mobile clients to connect to these services.
For a free trial for Windows Azure Websites, sign up here.