In response to Steve's call for a shift toward civic-focused applications that are a better suited for adoption and maintenance by PHP-friendly IT departments, I've momentarily set aside OpenShift's shiny new nodejs-0.10
cartridge in order to show you how easy it is to write a basic single-page mapping application in PHP.
Those of you who are familiar with Steve's mapping quickstarts may recognize this application as a port of his own MongoDB-Python-Flask example which uses MongoDB's spatial features to perform a classic bounding-box query.
Project source: https://github.com/openshift-quickstart/silex-mongodb-parks
Whether your goals are civic-minded or otherwise, PHP can help you craft solutions that are every bit as simple and elegant as what you might expect to see when using modern Python, Ruby, or JavaScript frameworks. This particular example is intended to serve as a reusable template for folks who are interested in producing their own mapping applications - substituting in their own collection of map points or other spatial data.
The included sample data contains the names and locations of major National Parks and Historic Sites.
MongoDB provides basic support for spatial data and queries, so I've chosen to use it over MySQL in this example. Since this demo currently only includes a simple bounding box selection, it might be possible to write it with MySQL in mind. But, the lack of real support for geospatial extensions might eventually become a problem if mapping is a central theme in your application.
Our new postgresql-9.2
cartridge includes support for PostGIS 2.1, which provides the most advanced collection of geo-spatial features available on OpenShift. Ultimately, choosing the right tool for the job may involve finding solutions that help to bridge technology communities, requiring minimal long-term effort to clone, configure, host, and maintain.
MongoDB also provides support for flexible Schemaless data structures (NoSQL), making it an ideal candidate for early projects and evolving prototypes, or for applications with plenty of sparse data or unstructured information.
When importing data, MongoDB expects it's input files to list a single JSON document per line. Our project's parkcoord.json
file conforms to these conditions, providing an easy way to load our sample data:
{
"Name" : "Abraham Lincoln Birthplace National Historical Park",
"pos" : [-85.7302 , 37.5332 ]
}
If included in your project, the .openshift/action_hooks/deploy
script will be executed just before your application is started. A list of the available build scripts or action_hooks
can be found in our guide for deploying and building applications.
Our deploy
script is configured to automatically bootstrap our MongoDB database with the following command:
mongoimport --db $OPENSHIFT_APP_NAME --collection parks --host $OPENSHIFT_MONGODB_DB_HOST --username $OPENSHIFT_MONGODB_DB_USERNAME --password $OPENSHIFT_MONGODB_DB_PASSWORD --port $OPENSHIFT_MONGODB_DB_PORT --type json --file $OPENSHIFT_REPO_DIR/parkcoord.json
It will then make a call to php/bootstrap.php
, adding a spatial index to our data collection:
$parks->ensureIndex(array('pos'=>"2d"));
Now that our data has been imported and indexed, let's take a look at our Back-end API and set up our map's bounding-box query.
Silex is a clean and simple microFramework built from reusable Symfony2 components. It provides an interface reminiscent of Sinatra, Flask, or RESTify, and can be installed via PHP's Composer dependency manager.
The example project includes a composer.phar
file that can be used to help manage dependencies. Dependencies are recorded in composer.json
as well as in the associated composer.lock
file.
{
"require": {
"silex/silex": "~1.1"
}
}
Composer will install all it's dependencies into your project's vendor
folder.
Inside your project's php
folder, an .htaccess
file will redirect all web traffic requests to our main app.php
file.
The Silex controller code is simple to load:
require '../vendor/autoload.php';
$app = new \Silex\Application();
In app.php
we include a basic route for our index page:
$app->get('/', function () use ($app) {
return $app->sendFile('../static/index.html');
});
And, a route for the static assets in our css
folder:
$app->get('/css/{filename}', function ($filename) use ($app){
if (!file_exists('../static/css/' . $filename)) {
$app->abort(404);
}
return $app->sendFile('../static/css/' . $filename, 200, array('Content-Type' => 'text/css'));
});
Our Back-end API includes a /parks/within
endpoint, which contacts MongoDB to execute our bounding-box query, producing a JSON
data response that will be used to update our map view:
$app->get('/parks/within', function () use ($app) {
$db_name = getenv('OPENSHIFT_APP_NAME') ? getenv('OPENSHIFT_APP_NAME') : 'parks';
$db_connection = getenv('OPENSHIFT_MONGODB_DB_URL') ? getenv('OPENSHIFT_MONGODB_DB_URL') . $db_name : "mongodb://localhost:27017/" . $db_name;
$client = new MongoClient($db_connection);
$db = $client->selectDB($db_name);
$parks = new MongoCollection($db, 'parks');
$bounding_box = array( 'pos' =>
array( '$within' =>
array( '$box' =>
array( array( $lon1, $lat1),
array( $lon2, $lat2)))));
$result = $parks->find( $bounding_box );
$response = "[";
foreach ($result as $park){
$response .= json_encode($park);
if( $result->hasNext()){ $response .= ","; }
}
$response .= "]";
return $app->json(json_decode($response));
});
On OpenShift, your Mongodb connection details will be automatically provided to your app via system environment variables, allowing you to keep your code clean and free of passwords, keys, and other configuration strings. The above code checks to see if these variables are populated, providing sensible fallback values to help support local development environments.
The server-side code concludes with an $app->run();
statement.
All of the View-related code is written in Javascript and is run on the client-side.
Leaflet's mapping solution is well documented, looks great, and is really easy to setup and configure.
Include a link to Leaflet's css stylesheet in our index.html
file:
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.5.1/leaflet.css" />
We'll also need to initialize our map view, and include a few event hooks and callbacks for updating our map content whenever our viewport is being loaded or modified:
<script src="http://cdn.leafletjs.com/leaflet-0.5.1/leaflet.js"></script>
<script>
var map = L.map('map').setView([42.35, -71.06], 12);
var markerLayerGroup = L.layerGroup().addTo(map);
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'
}).addTo(map);
function getPins(e){
bounds = map.getBounds();
url = "parks/within?lat1=" + bounds.getSouthWest().lat + "&lon1=" + bounds.getSouthWest().lng + "&lat2=" + bounds.getNorthEast().lat + "&lon2=" + bounds.getNorthEast().lng;
$.get(url, pinTheMap, "json")
}
function pinTheMap(data){
//clear the current map pins
map.removeLayer(markerLayerGroup);
//add the new pins
var markerArray = new Array(data.length)
for (var i = 0; i < data.length; i++){
park = data[i];
markerArray[i] = L.marker([park.pos[1], park.pos[0]]).bindPopup(park.name);
}
markerLayerGroup = L.layerGroup(markerArray).addTo(map);
}
// update the map pins whenever the map is redrawn:
map.on('dragend', getPins);
map.on('zoomend', getPins);
map.whenReady(getPins)
</script>
The dragend
and zoomend
map events will now fire whenever the map viewport is adjusted.
I know I've used MongoDB for this demo, which may not be the easiest DB solution for older IT departments to adopt. However, OpenShift really helps to address that concern by providing an open platform that is readily available for use within any IT department - ensuring consistent, reliable, and supported access to newer technologies.
This means that you can build reusable applications that are completely portable across open cloud platforms, an ideal scenario for open civic-focused community solutions.
To deploy a clone of this application using the rhc command line tool:
rhc app create parks php-5.3 mongodb-2.2 --from-code=https://github.com/ryanj/silex-mongodb-parks.git
Or clone+deploy via our web-based workflow in a single click:
https://openshift.redhat.com/app/console/application_type/custom?cartridges%5B%5D=php-5.3&cartridges%5B%5D=mongodb-2.2&initial_git_url=https%3A%2F%2Fgithub.com%2Fryanj%2Fsilex-mongodb-parks.git
A live demo is available here: http://phparks-shifter.rhcloud.com/
Nice article, so what's next ?