Ready to watch this entire course?
Become a member and get unlimited access to the entire skills library of over 4,900 courses, including more Developer and personalized recommendations.Start Your Free Trial Now
- View Offline
- Creating your first module
- Interacting with hooks
- Working with permissions and roles
- Controlling access
- Adding a menu item to an admin interface
- Using the Form API (FAPI) to quickly create a form
- Creating custom form validation
- Manually creating a custom content type
- Validating user input
- Importing content using feeds
- Creating a block
- Understanding best practices and coding standards
Skill Level Advanced
A Drupal block is an area which displays content whose position is controlled by the theme and administrative block settings. I'm going to programmatically create a block that shows a map of wind farms that are within 100 miles of the center specified. To help us some of the computation I'm going to leverage a library that is within the location module. There is no need to actually enable the location module to do this, but it does need to be there. Open windfarms.module and scroll to the end. Two functions are needed to generate a block and I will use a third to keep the code tidy.
The first function that's needed is an implementation of hook_block_info. By implementing this hook, I can declare to Drupal that a block with a particular machine name and human readable administrative name exists. Start with a docblock, Implements hook_block_info. Hook_block_info requires no parameters and returns a nested array, function windfarms_block_info, blocks = array.
Hook_block_info requires no parameters and returns a nested array. The array is keyed by the delta value of the block which is a fancy way of saying the machine name. Block names are unique to the module, so you don't have to worry about collisions. I'll create a block with the delta of gmap, blocks gmap is equal to an array. For each delta, define an array containing one key named info. This will contain a translated string of the administrative name for the block.
This is shown on the block admin interface and nowhere else. I'm setting the info to Wind Farm Map. Finally, return the blocks array. Next, I'm going to implement hook_block_view which returns the rendered contents of a block as an array with the two elements: the subject which is the localized title and the content. The content can either be a string or a renderable array which is similar to the farms, start with the docblock.
Implements hook_block_view, hook_block_view takes one parameter, function windfarms_block_view takes one parameter, the delta of the block in question defaulting to an empty string. As it returns an array, create a variable named block, initialized with an empty array and then switch on the delta, switch ($delta) and then a case for gmap.
The localized subject is displayed at the top of the block and can be overridden using the Drupal admin interface. If no title is needed, the subject is still required but it can be set to null. This case I'm going to set the $block 'subject' to the translated string Wind Farm Map. To keep things neat, I will generate the content of the block by calling a custom function that just returns renderable content and doesn't implement any hook, $block 'content' = windfarm_block_contents, and the ($delta).
After the end of the switch block return the block. The final function remains, windfarm_block_contents. I'll create the function first, function windfarm_block _contents, that takes one parameter the ($delta) and then I'll add the docblock, Wind Farm Block contents for the description, a parameter string the $delta, which is The block ID.
Then return a string containing the HTML output. Initialize the output, then switch on the ($delta) and add a case for gmap. As mentioned before, I will be using a library within the location module to handle some of the computations required. While I could theoretically copy and paste the functions, this causes several problems.
First, copying and pasting makes it look like I wrote the functions which I didn't. Second, the library can be updated and the enhancements would not be reflected in my code, and third, by mixing context the code structure can get concluded leading to what is referred to as Spaghetti code, where the logic is a big sloppy mess. I'm going to use module_load_include again, specifying include in the location module, in particular the earth file.
The function I'll be using operates in meters, so I'll convert the distance 100 miles into meters. 100 miles, $distance_meters = 100 * 1609.34. Next, I'll determine the range of the latitude and longitude from the default center. This is a simplified version of a proximity search which looks in a square, rather than a radius around a point. Why simplify? For two reasons, first it's fast and it has enough precision for an intention grabber.
And second, this course is about programming modules not geo-positioning calculations. Determine the range of lat and long from the default center. I'll use the default center latitude and longitude from the variables set from the admin interface, $lat = variable_get ( 'windfarms_default_center_lat'). I'll copy and paste and set that to long, and long.
Next I'll calculate the range using the Earth Library. So the $range_lat = earth_latitude_range, takes the long and lat and the distance will be actually distance_meters. I will do the same for the longitude with earth_longitude_range. Now that I know where I'll be looking it's time to build the query. While the last time I needed nodes I used DB query, but this time DB query isn't appropriate.
Drupal 7 introduced the EntityFieldQuery API which allows for queries of entities such as nodes to be built efficiently, avoiding the need to know the complete internals of every generated table and column. This API is used in an object oriented manner rather than procedural. To start, create a new entity field query object. Build_query then $query = new EntityFieldQuery.
Context is needed and to filter the query I will use what are known as conditions. In the end, these conditions will result in where statements with joints as necessary. The method entity condition adds a condition on entity generic metadata such as the entity type, bundle, revision ID or entity ID. Entity conditions take two required parameters, the name of what you want to filter and the value that you are filtering on. A third parameter, the operators such as equals, less than, and so forth, is optional and defaults to equals.
I want to only return nodes, which is in the entity type, so Only_show_nodes. So $query takes entityCondition, the name is entity_type and the value is node. Next, I will filter further and only show the bundle windfarm. Only_show_windfarms, $query-> entityCondition, the name is bundle, and the value is windfarm.
Now that I've filtered down to the node of content type windfarm, it's time to find only wind farms within the latitude and longitude range that I calculated earlier. To do so I will use the method fieldCondition. Similar to entityCondition, the first parameter is the name of what I want to filter on, which in this case is the field's windfarm latitude. The second parameter is the column that I want to filter on. Similar to when I was displaying the map the value of the latitude is stored in latitude.
Following the column is the value where the comparison will be made too. I will be checking to see if the latitude is within a range, so I will pass it the array containing that range. If I was filtering on a single value then I could just pass that value rather than putting it in array. Finally the optional operator, as I'm checking a range I will use the keyword between, Only show latitude between range, query fieldCondition, which takes the field windfarm latitude, then that value $range_lat and then the keyword between.
The longitude fieldCondition is constructed in a very similar manner, so I'll copy and paste, Only show longitude between the range and the windfarm_longitude, it will take the $range_long. Now that the query has been built, I can execute the query by calling the method execute. Execute query, so $result = $query->execute.
Following execution an array of associative arrays of stub entities will be returned, keyed by the entity type on the outside and relevant entity ID on the inside. Check to see if there are any nodes, if not, return an empty string. No results, if isset result keyed by the entity type, which in this case is node, then return the empty string. If there are nodes then load them all.
To do that, I will use node load multiple, which takes an array of node IDs and returns an array of node objects, $nodes = node_load_multiple. I'll use the array_keys of result node. Now that I have all the nodes for the windfarms that match, I can build the Google map using gmap 3D tools, same as the theme I will use module_ load_include to load the include file gmap3_tools.
Then create an empty array of markers, $markers = empty array, foreach nodes as node get the latitude and longitude, so the $lat = $node->windfarm_ latitude, LANGUAGE_NONE, 0 and then the key of 'value' and the long which is windfarm_longitude.
If either the latitude or longitude are missing, skip the entry. While it's highly unlikely, zero latitude and zero longitude is a legitimate coordinate. Cannot render map without both. If $lat == empty string or $long == empty string, continue.
If it passed validation get the facility name which is the node title $facility_name = check_plain($node_title). Then get the description, $description = check_plain($node-> body, LANGUAGE_NONE, index 0, and the 'value'. I'm going to add a link to the node. While it may be tempting to write a straight anchor tag in HTML, Drupal provides a clean mechanism for inserting links that integrates with a menu routing system.
The function is just L for link. This means that you can just pass a raw path for a node, and if you have an alias path defined for the node, the L function will convert it transparently. L takes three parameters, but only the first two are required. First, the text of the link for the anchor tag, then the internal path being linked to. Add a link to the node. $description concatenate equals a space followed by the L function, the first part is the label or info followed by the path node which is node/ and then the node ID.
Finally, create the marker and add it to the array. Add to markers, $markers, I'm going to append gmap3_tools_create_marker with a latitude, the longitude, the title is the $facility_ name and the description is the $description. When the markers are created, I will call gmap3_tools add map again, but this time the options will be a little different. Create map with all the markers, gmap3_tools_add_map with an array.
Instead of a dynamic mapID just specify gmap-canvas-block. The satellite view will be just fine for the map options, mapOptions set to an array and the mapTypeID set to GMAP3_ TOOLS_MAP_TYPE_ ID_SATELLITE. Instead of defining the marker in line, just pass the markers array.
So markers set to $markers. I will use a different option for the default markers position which will automatically zoom to encompass all the markers on the map. So gmap3ToolsOptions set to array, defaultMarkersPosition which is set to GMAP3_TOOLS_DEFAULT_MARKERS_POSITION_CENTER_ZOOM.
Finally, the only HTML that will actually be returned by the block function is the HTML container for the map. Set the ID to the same thing as the map ID above, so $output = 'div id="gmap-canvas-block". The style is the width 500 pixels and a height of 400, and close the div. Then end the switch statement, then at the end return the $output.
Save then return to the browser. Now that the block has been completely defined enable it in the admin interface. Go to Structure then Blocks. At the bottom, in disabled, Wind Farm Map is now shown. For the REGION set it to Content. Then arrange the content so Wind Farm Map is shown first, click Save blocks. The block will be shown on every page above the content which is a little disconcerting if you're looking at a wind farm or another node.
Therefore set it only show on the front page. Click configure, only on the listed pages and <front>, click Save block, go to the homepage. The Wind Farm Map is now displayed on the front page using a block populated by the database using entity field query. In this chapter, I've created a feed importer that creates wind farms from a CSV, added a Google map by integrating with another module and theming, then created a block that contains a map filled with the results of the geographic search of the custom content.
At this point, the base functionality of the module is complete. The next question, where to go from here?