Generic functionality and configuration - The PHP Project Guide (2014)

The PHP Project Guide (2014)

10. Generic functionality and configuration

Generic functionality defined patterns that allow you to re-use code for a variety of purposes.

It sounds boring, but generic functionality and somewhere to define configuration within your website will save you hours, perhaps even days in the long run. Consider a website that outputs articles on several pages. Perhaps you have a main news section, a featured article section and for each author on your website you list a short list of their most recent articles on their profile page. Are you going to build the functionality to output articles for each instance? Certainly not!

10.1 Global configuration

Configuration within your website is extremely important, particular as it grows and may become unmaintainable. Global configuration, in short, means somewhere you can specify values that may be used once or more than once throughout your application. The reason for this configuration is that it makes it easier to change functionality from one place, normally a configuration file. Although, this can be from a database which may prove easier if you’ve set up an administration area, from which these values can be changed.

Let’s first take a look at flat file based configuration, how we may create a configuration file and how we make use of our values. One of the easiest ways to create configuration within a flat file is simply to create a new file, create an array within this file to store your configuration settings and then include this file where you need (almost always globally). The reason we use an array is because this allows us to easily create configuration groups which house different configuration options. Let’s take a look at an example, which may be defined within a file called config.php.

1 $GLOBALS['config'] = array(

2 'mysql' => array(

3 'host' => 'localhost',

4 'username' => 'billy',

5 'password' => 'N&G8>di4ab0x',

6 'db' => 'site'

7 ),

8 'articles' => array(

9 'initial_count' => '10'

10 )

11 );

Here you can see an associative array, with each inner array containing a group for configuration options. You’ll also have noticed the variable we’re applying this array to is $GLOBALS[‘config’]. This isn’t necessary, and may perform slower if you were to do it this way, particularly if you had a very large array, although the speed difference would usually be unnoticeable. If you’re declaring configuration within a class, you may want to make this global to a class so you can reference this only where your class is instantiated (and presumably then where you’d need these configuration options as part of your website functionality).

Now you’ve defined your configuration options, you’ll need a function to deal with returning the values based on what you pass to this function. This could be a standalone function, or be part of a class as a method, depending on how your configuration array is defined. An example of a function that deals with returning values may look something like this:

1 function conf($group, $option) {

2 return (isset($GLOBALS['config'][$group][$option])) ? return $GLOBALS['config'][\

3 $group][$option] : false;

4 }

So, this will return the value requested based on what’s passed to $group and $option. This is a very simple example, but what if you wanted to create arrays within your main groups? You could of course add another argument to your function, like:

1 function conf($group, $first, $second) {

But, this is still unmaintainable. Just think what would happen if you wanted an even deeper collection of arrays! For this, you may need to consider a better way to address this, and we can make use the func_get_args function to help us. Here’s an example of how you may implement a function to read any part of your global config array, any level deep:

1 function config() {

2 foreach(func_get_args() as $arg) {

3 $value = isset($value) ? $value[$arg] : $GLOBALS['config'][$arg];

4 }

5 return $value;

6 }

This function defines no arguments to be passed in, but we use func_get_args (returning an array of arguments passed through to the function) and loop through this. We then check if this value is set, and if so, move to the next value. We eventually return this value. This means we can make use of the function like this:

1 config('articles', 'homepage', 'initial_count');

2 config('articles', 'article_page', 'initial_count');

10.2 Database based configuration

If you don’t require as many groups of options, you can choose to store them in your database. This is also useful if you’re building a backend for administration and need to change website settings from here. Be aware that this option may also be slower, as reading options would require a query on every page where options are required to be read. These could be cached, but could still be slower and if you don’t have caching in place this would not be feasible. To set up a configuration table within your database, you simply need to consider key/value pairs. So, a table may contain the following fields:

· option_id

· option

· value

You could still group data here, perhaps using an underscore for groups. For example, if you had options for articles, you may have an option like articles_initial_count and assign a value to this. You can probably already see how this may start to get messy and would become unmaintainable with many configuration options. Unfortunately, this is just part of the table structure, and would otherwise mean you’d have to create many tables for different configuration options. This isn’t a good idea, would impact speed and would generally be a mess and hard to maintain. If you do choose this option, how do you go about pulling configuration values from your database? One query, either globally or defined within you class (perhaps within your __construct method in your core class) will then allow you to return an associative array of this result and then start to access settings. We won’t look at any specific examples of how to set this up, as this could take many forms depending on how everything has already been set up. Storing configuration data in a table may also be more secure as it’s not prone to being revealed as a flat file as easily. Just ensure that this method of getting settings is absolutely necessary, considering the negatives we’ve already looked at.

One last consideration is that you’d be unable to store MySQL connection information within your database table, simply because you need these values prior to connecting to your database! You should store these in a configuration file to then connect to your database. From here, if you’re using your database to store your configuration, you can then build the functionality to retrieve it.

10.3 Generic data output

As with the example above, you may have areas of your website that output the same thing, in the same format, only based on different settings each time. We’ll get straight into an example of this, continuing with the idea of outputting articles within the same website, but varying slightly each time. We’ll assume a design has already been built for the listing of articles. This could be a series of div elements that would be output for each loop of your records, outputting data for the current article.

The first thing you’ll need to assess is exactly what will vary throughout these lists of articles. Assuming what we’ve discussed above will happen, these will be:

1. Result limit (amount of article to appear)

2. The field to sort by

3. Order of sorted data (ascending or descending)

4. Ability to specify a particular field to filter in (e.g. a particular user).

Once we’ve established these settings, we build a method to take arguments, which we’ll later process within the method.

1 public function articles($filter, $sortBy = 'timestamp', $sortOrder = 'desc', $li\

2 mit = 25) {

3 // nothing here yet

4 }

At first glance this is very simple, and it’ll also save us a lot more time later on, as it’s more maintainable. Notice the default values for if these arguments aren’t passed.

So now we come to actually building some of the inner contents of the method. This isn’t a full scale solution so we’ll just build something very simple. We’re also assuming that there is database functionality to query and return results, so this example won’t work straight out of the box. Also note I’m using SELECT *, which should be avoided, and you should specify explicitly the fields you want to retrieve. I’ve done this simply to save space.

1 public function articles($filter = null, $sortBy = 'timestamp', $sortOrder = 'DES\

2 C', $limit = 25) {

3 $sql = "

4 SELECT * FROM `articles`

5 " . $this->escape($filter) . "

6 ORDER BY `". $this->db->escape($orderBy). "`

7 " . $this->db->escape(strtoupper($sortOrder)) . "

8 LIMIT " . (int)$limit;

9

10 $this->db->query($sql);

11 }

From this you’ll now need to output your data with your chosen database handler and within your written markup. Below is an example of how this may be done, very basically. You may have other details to include, but this generally shows looping through article results with the most basic of data.

1 // continued

2 $this->db->query($sql);

3

4 foreach($this->db->rows() as $article) {

5 echo <<<HTML

6 <div class="article-list">

7 <h2>{$article['headline']}</h2>

8 <p>{$article['teaser']}</p>

9 </div>

10 HTML;

11 }

Notice we’re also using the heredoc syntax to output the HTML, which we looked at earlier.

We’ve now created a method that allows us to output a list of articles with the chosen markup, with the ability to specify configuration options. You can add more configuration options to the argument list to suit your needs, and you will more than likely need to do this at some stage. Before adding anything else to the argument list, consider how you might reduce the amount of arguments by combining options into an array of data and then exploding or looping through it within the method. All this takes is careful planning beforehand, however if you do build in functionality that doesn’t make sense, or is difficult when you come to implement it, you’ll soon spot the difficulty and will be able to make changes. And as always, if you’re passing arguments directly into a query string, ensure you’re escaping and formatting any data that potentially lead to SQL injection or data inconsistency.

Also remember, if you notice that you’re repeating any code, you could probably avoid this!

10.4 Translations

At one point or another, you may need to offer your website content in other languages. There are many considerations when using translations, as you need to know how deep you’ll need to translate. Will this be site text only, or will you need to localise article content too? You may provide one article with the content separated into many records, all in different languages to be output based on the locale chosen. We won’t be talking about how you can provide your users with the option to switch language, but we’ll be looking at the options we have for the backend translation and implementing this, including a look at how you may translate non general site content like articles.

Let’s firstly take a look at how you may go about translating site text, so that a locale is provided by whatever means, which would then go and fetch site text based on this. We’re not going to be looking at many examples here, as there are many variations to the process of translating site text. We’re going to be looking specifically at gettext, PHP functionality that allows you to output text based on locale, but also with the added benefit of being able to generate translation files (with a .po extension) that contain your website translations, by scanning through all of the files that make use of your gettext output. This gives the benefit of being able to write your code as you normally would output everything, for example:

1 echo gettext('Hello') . ' {$username}';

The gettext function is what does the magic of translating this text, so you would have ‘Hello’ output in different languages when the locale is switched. Bear in mind you have to do the translations yourself, so don’t try to implement this where you haven’t generated language files and translated everything. You may find it tedious to write code using the gettext function for everything you need to translate, so PHP provides a shorthand function for this, a simple underscore. In this case, the above snippet of code would look like this:

1 echo _('Hello') . ' {$username}';

This is much better as I’m sure you’ll agree, and makes for more natural reading when scanning through your code.

So what now? You’ve written your code with the _ function, outputting all this text, but now you need to generate the files that will be read depending on which locale has been selected. This will then fetch the associated text and output. The steps to doing this are fairly simple as gettext provides utilities to sweep through your code and find any text wrapped within the function you’ve chosen to use (either _ or gettext). You’d then hand these files to a translator, or translate them yourself and place these files where they can be read by gettext. There are many resources that explain this process and exactly where they need to go, so we won’t waste space listing everything out here. However, you now know that you can use gettext to easily translate site text.

10.5 Translating other site content

You may have other site content like articles that will require translation, if not now, into the future. The ability to translate these will also mean the content will need to be written in the language it’s intended for, and then somehow marked with the locale for the language. For example, if you have a database table for articles, you may keep the ‘body’ of the articles in a separate table, and mark each record with the locale. Your main article table would contain a body_id field that references body content stored in the table articles_body, for example. This means that a field, locale, can be incorporated into the articles_body table. So, when querying the articles table for an article, you’d join this to the article_body table and pull in the content. This could then be based on the fact that a locale matches up. You could even fall back to the default language version if the specific locale’s body content isn’t found. This makes it really easy to then take the locale that is currently set and pull in localised content. For example, if you’re already using gettext to translate site language, you may have a variable declared that changes based on the locale selected:

1 $locale = (isset($_COOKIE['locale'])) ? $_COOKIE['locale'] : 'en_GB';

In this example, the locale is set based on a cookie. If this isn’t defined, it defaults to en_GB. This can then be used with gettext, but can also be incorporated into queries that return localised content. Remember, the above example uses content that can be set by the user, so remember to escape values like this before passing them to queries.

Implementing these methods early doesn’t mean you have to start translating your content to other languages straight away, but it means you’re all set up to provide content in other languages later on. Imagine building your website with vast amounts of text and content without considering this beforehand. If demand for your content in other languages increases, you’ll have a lot more work to do to turn this around if not initially implemented!