Google Maps Geocoding with Bootstrap and Zend Framework 2


As part of several of our apps, we have links to pop up a modal with a Google Map that shows where an address is located, with an option to get directions that opens up in the full Google Maps site.  So I thought I’d share the code to do that here, since I didn’t really find many decent examples that deal with doing this with Bootstrap 3.x, PHP, and/or Zend Framework.

First, the view – for sanity purposes, this code is simplified to focus just on the mapping elements. Also, I’m presuming you know how to do layouts and stuff, so I won’t replicate all that here.

<?php
    namespace Business\Businesss;

    use Application\BaseController;
    use Application\Service\GeocodingService;
    use Zend\View\Model\JsonModel;
    use Zend\View\Model\ViewModel;

    class BusinesssController extends BaseController
    {
        public function businessDetailsAction()
        {
            $business_id = (int) $this->getEvent()->getRouteMatch()->getParam('business_id', 0);
            $business = $this->getGateway('Businesss')->getBusiness($business_id);

            $geocoding_service = new GeocodingService();
            $geocoding = $geocoding_service->getGeocodes($business);

            $view_model = new ViewModel(array(
                'business' => $business,
                'geocoding' => $geocoding,
            ));

            return $view_model;
        }
    }

Again, I figure you know how to do entities and all. Suffice to say, the address does need to be in a usable form, generally broken down into address_1, address_2, city, state_abbreviation, zip_code. It is helpful to store them in these parts, either in individual fields or as we do with this app, as a serialized array in a single field.

All that said, on to the GeocodingService.php which handles the heavy lifting. Again, for ease of use, the Google credentials are stored in the app configuration. To improve performance, we also have a temporary table that stores the latitude and longitude values for any given address, significantly reducing the calls to the Google API. This could also be done using a Zend Cache, though that would mean they would be cleared on any server starts or on a regular schedule, versus just when non-existent, if manually cleared, or by your own age schedule.

<?php
    namespace Application\Service;

    use Application\BaseService;
    use Application\StaticGateway;
    use stdClass;
    use Zend\Db\Sql;
    use Zend\Stdlib\Hydrator\ObjectProperty;

    class GeocodingService extends BaseService
    {
        private static $geocoding_api_url = 'https://maps.google.com/maps/api/geocode/json?address=%1$s';

        public function getGeocodes($entity)
        {
            $geocoding = new stdClass();
            $geocoding->is_mappable = false;
            $geocoding->has_address_parts = false;

            if (
                !empty($entity->building_name)
                || !empty($entity->address)
                || !empty($entity->city)
                || !empty($entity->fk_state_id)
                || !empty($entity->postal_code)
            ) {
                $geocoding->has_address_parts = true;
            }

            if (
                !empty($entity->address)
                && !empty($entity->city)
                && !empty($entity->fk_state_id)
                && !empty($entity->postal_code)
            ) {
                $static_gateway = new StaticGateway();
                $state = $static_gateway->getState($entity->fk_state_id);

                $address_to_geocode = $entity->address
                                        . ','
                                        . $entity->city
                                        . ', '
                                        . $state->state_abbreviation
                                        . ' '
                                        . substr($entity->postal_code, 0, 5);

                $address_to_geocode = urlencode(trim($address_to_geocode));

                $select_query = new Sql\Select();
                $select_query
                    ->from('temp_geocodes')
                    ->columns(array(
                        'latitude',
                        'longitude'
                    ))
                    ->where(array('address' => $address_to_geocode));

                $prepared_db_statement = self::$sql->prepareStatementForSqlObject($select_query);
                $query_results = $prepared_db_statement->execute()->current();

                if (!empty($query_results)) {
                    $hydrator = new ObjectProperty();
                    $hydrator->hydrate($query_results, $geocoding);
                    $geocoding->is_mappable = true;
                    $geocoding->formatted_address = $entity->address
                                        . '<br />'
                                        . $entity->city
                                        . ', '
                                        . $state->state_abbreviation
                                        . ' '
                                        . $entity->postal_code;
                } else {
                    $geocoding_api_url = sprintf(self::$geocoding_api_url, $address_to_geocode);
                    $curl = curl_init();
                    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
                    curl_setopt($curl, CURLOPT_URL, $geocoding_api_url);
                    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

                    $curl_response = curl_exec($curl);
                    $info = curl_getinfo($curl);
                    $geocoding_results = json_decode($curl_response, true);
                    curl_close($curl);

                    if(
                        $curl_response !== false
                        && $geocoding_results !== false
                        && $geocoding_results['status'] == 'OK'
                    ) {
                        $geometry = $geocoding_results['results'][0]['geometry']['location'];

                        if(!empty($geometry['lat']) && !empty($geometry['lng']) && !empty($geocoding_results['results'][0]['formatted_address'])) {
                            $geocoding->directions_address = $address_to_geocode;
                            $geocoding->latitude = $geometry['lat'];
                            $geocoding->longitude = $geometry['lng'];
                            $geocoding->formatted_address = $geocoding_results['results'][0]['formatted_address'];
                            $geocoding->is_mappable = true;

                            $action_query = new Sql\Insert();
                            $action_query
                                ->into('temp_geocodes')
                                ->values(array(
                                    'address' => $address_to_geocode,
                                    'latitude' => $geocoding->latitude,
                                    'longitude' => $geocoding->longitude,
                                    'modified_date' => new Sql\Expression('NOW()')
                                ));
                            $prepared_db_statement = self::$sql->prepareStatementForSqlObject($action_query);
                            $prepared_db_statement->execute();
                        }
                    } else {
                        $hydrator = new ObjectProperty();
                        $hydrator->hydrate($geocoding_results['results'][0], $geocoding);
                    }
                }
            }

            return $geocoding;
        }
    }

With that, it’s view time!

<?php 
    /** OTHER CONTENT STUFF */
    if($geocoding->is_mappable) {
        echo $this->partial('partial-views/google-map');
    }
    /** OTHER CONTENT STUFF */

Wait, what? That’s so short!?! 😀

The core google code is actually in a partial view (under the application view folder). Using the partial enables me to reuse the code for different address needs (for example, both businesses and employees). Said partial handles the JavaScript side of using Google Maps.

<?php $this->layout()->needs_google_map = true; ?>

<div class="modal fade" id="googleMap" tabindex="-1" role="dialog" aria-labelledby="googleMapLabel">
    <div class="modal-dialog modal-full-screen" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                <h4 class="modal-title" id="googleMapLabel"></h4>
            </div>
            <div class="modal-body">
                <div id="map_canvas_container"></div>
                <div id="mapCanvas"></div>
            </div>
        </div>
    </div>
</div>

<?php $this->inlineScript()->captureStart(); ?>
    myGoogleMap = '';

    function loadGoogleMap(latitude, longitude, marker_address, marker_label) {
        var contentString = '<div class="map-marker">' + marker_address + '</div>';

        var myLocation = new google.maps.LatLng(latitude, longitude);

        var my_options = {
            zoom: 18,
            center: myLocation,
            mapTypeId: google.maps.MapTypeId.ROADMAP
        }

        myGoogleMap = new google.maps.Map(document.getElementById('mapCanvas'), my_options);

        infowindow = new google.maps.InfoWindow({
            content: contentString
        });

        var marker = new google.maps.Marker({
            map: myGoogleMap,
            position: myLocation
        });

        google.maps.event.addListener(marker, "click", function () {
            infowindow.open(myGoogleMap, marker);
        });

        infowindow.open(myGoogleMap, marker);
    }

    $(function(){
        $('#googleMap').on('shown.bs.modal', function(event) {
            var button = $(event.relatedTarget);

            latitude = button.data('latitude');
            longitude = button.data('longitude');
            marker_address = button.data('marker_address');
            marker_label = button.data('marker_label');
            linked_address = button.data('linked_address');

            $("#googleMapLabel").html('Map to ' + marker_label + ' - <a id="directionLink" href="http://maps.google.com/maps?saddr=&daddr=' + linked_address + '" target="_blank">Click for Directions</a>');

            loadGoogleMap(latitude, longitude, marker_address, marker_label);

            google.maps.event.trigger(myGoogleMap, 'resize');
            myGoogleMap.setCenter(new google.maps.LatLng(latitude, longitude));
        });
    });
<?php $this->inlineScript()->captureEnd(); ?>

And that’s it. The final result is a full-size modal pop up that contains the map, a marker with the target address, and a link to go to Google for full directions.