Basic PHP tag filtering tutorial/files

Back to projects

Here, we'll be learning how to make a list of things using arrays, and then we're going to implement a tagging system that lets us filter the list by tag. I'll be making a list of albums, but you can and should make up your own system and follow along with that.

While the intended audience of this tutorial is beginner programmers, I'm still assuming you know basic PHP syntax and some core programming concepts like variables. Here's a list of the stuff we're going to use in this tutorial, in order of appearance. If you haven't heard of any of these things before, I suggest reading about them before you keep going so you have an idea of what they do and how they're implemented.

Syllabus

Extra credit

Also, another disclaimer: This is a basic programming tutorial. I'm not a professional programmer, so all I can guarantee here is that you will get a product that works and not that it'll necessarily hold up to any professional standards or best practices. If you're here, you probably don't care about that anyway, though!

With all the boring administrative stuff out of the way, let's get started!

Table of contents

A comment on comments

You will quickly notice that none of the code in here is commented. The reasoning for this is twofold: first, I'm explaining everything to you as we work through this code together. Second, I think excessive comments make code harder to read and would much rather just have something concise and descriptive that eliminates the need for them. (See the "good" and "bad" examples in the preface of Simple Programming Problems.)

That being said, you might still want to add comments (particularly if you're really new to PHP or programming). Add them! Your code is for you to read, so annotate it in a way that works for you.

Now we're actually going to get started.

Compiling the array

The first thing we need to do is make our array. As I mentioned earlier, I want to make a list of some albums that I have. An album has multiple pieces of metadata, so each album will be represented as an associative array. The keys are going to be whatever metadata I've chosen to use.

The metadata I want to include for each album are the title, artist, a link to the album cover, and tags, so I'm going to build my first album array like this. (There's no limit to the number of keys you can put into an array, so don't feel like you need to stick to the same number of fields that I have.)

array(
    "title" => "Alice's Restaurant",
    "artist" => "Arlo Guthrie",
    "cover" => "/stuff/albums/alicesrestaurant.jpg",
    "tags" => "folk americana cd 1960s"
)

Our array keys and metadata are all strings, so everything needs to be enclosed in quotes. We also need a separator character (also called a delimiter) to distinguish my tags with. I'm going to use a space; commas (,) and pipes (|) are also common choices. I'd have "folk,americana,cd,1960s" if I used commas instead of spaces.

You can use any character as a delimiter as long as you're consistent, and that character can't show up within a tag. You'll see later that I've used a dash to separate multi-word tags. In any case, your tags should be concise because we'll be accessing them from a URL later.

I want a list of albums and not just one album, though, so now I need to take the array I just made and put it into another array. I've decided to call the variable the array is stored in $albums, but you can and should name your variable according to its contents.

$albums = array(
    array(
        "title" => "Alice's Restaurant",
        "artist" => "Arlo Guthrie",
        "cover" => "/stuff/albums/alicesrestaurant.jpg",
        "tags" => "folk americana cd 1960s"
    )
);

Now that our album array has been created, we can continue adding albums using the same keys we used for the first one. Here's $albums with a few more items added to it:

$albums = array(
    array(
        "title" => "Alice's Restaurant",
        "artist" => "Arlo Guthrie",
        "cover" => "/stuff/albums/alicesrestaurant.jpg",
        "tags" => "folk americana cd 1960s"
    ),
    array(
        "title" => "Speaking in Tongues",
        "artist" => "Talking Heads",
        "cover" => "/stuff/albums/tongues.jpg",
        "tags" => "rock new-wave vinyl 1980s"
    ),
    array(
        "title" => "To The Faithful Departed",
        "artist" => "The Cranberries",
        "cover" => "/stuff/albums/faithfuldeparted.jpg",
        "tags" => "rock alternative cd 1990s"
    ),
    array(
        "title" => "good kid, m.A.A.d city",
        "artist" => "Kendrick Lamar",
        "cover" => "/stuff/albums/maad.jpg",
        "tags" => "hip-hop cd 2010s"
    )
);

Keep adding items to your array until you're happy. Once that's done, we can move on to getting this onto a page!

Displaying array entries

Right now, the array we made is floating around in the ether. We need to print its contents in HTML so they show up on our webpage.

...But before we do that, we need to handle the tags. The list of tags for each entry is stored as a single string of text, but we need to treat each tag as its own entity. We can do that by splitting our string into an array with the explode() function.

First, we're going to make a function. This will be a one-liner, but it will tell us concisely what we're doing every time we call it. I'm naming my function splitTags(), but use whatever name makes sense to you. This function takes one argument, $tags, which is a string containing our tags.

function splitTags($tags) {
}

The explode() function chops up a string and puts it into an array. It knows where to chop by looking for a delimiter character, which we've given it as an argument. I'm using a space for my delimiter, as I mentioned in the last section, so I'm going to pass a string with a space in it " " in as the first argument. We'll also put in the variable $tags as the second argument because it's the string to be... exploded.

We're going to store the resulting array in a variable called $tagArray, and then finally we'll have our function return $tagArray when it's called.

function splitTags($tags) {
    $tagArray = explode(" ", $tags);
    return $tagArray;
}

Now, we'll make another function that we can use to print an array of tags. I'm going to call it printTags(), and it'll take an array of tags (called $tagArray) as its argument.

Here, we're going to pass through each element in the tag array and create some HTML output for it. We can do this easily with a foreach loop. Inside the loop, we need to use echo to print each tag, which I'm referring to as $tag to keep things descriptive. Mind the quotation marks: echo only works with strings!

function printTags($tagArray) {
    foreach ($tagArray as $tag) {
        echo "#$tag ";
    }
}

Finally, we can proceed with displaying the code for each album using a function I'll call printAlbums(). (Or print[Whatever], depending on what you're doing.) It takes an array, which I've called $albums, and runs it through another foreach loop. Now, for each album, I need to access its metadata, which I can do by calling each key with $array['key']. I'm going to store each of those in a variable to make the rest of my code a little cleaner.

function printAlbums($albums) {
    foreach ($albums as $album) {
        $title = $album['title'];
        $artist = $album['artist'];
        $cover = $album['cover'];
        $tags = splitTags($album['tags']);
    }
}

The template HTML we need to include is a fairly substantial block of code, so let's put it into its own file for ease of use. I'll make a new file called albumtemplate.php in the same folder. When I want to use each variable, I'll print it out using echo. I'll also list the tags using the printTags() function with the $tags array as its argument (no echo necessary). Here's my completed template:

<div class="item">
<img src="<?php echo $cover; ?>" alt="" /><br/>
<p><strong>Title</strong>: <?php echo $title; ?><br/>
<strong>Artist</strong>: <?php echo $artist; ?></p>
<p>Tags: <?php printTags($tags); ?></p>
        $title = $album['title'];
</div>

Hop back over to your file with all your functions in it. We need our function to use the template we just made, which we'll do by adding an include statement as the last thing in our loop, and we're done!

function printAlbums($albums) {
    foreach ($albums as $album) {
        $artist = $album['artist'];
        $cover = $album['cover'];
        $tags = splitTags($album['tags']); 
        include "albumtemplate.php";
    }
}

Time to assemble a webpage! Paste in your HTML code underneath all the PHP work we just did. (This should be a whole, complete page with <html>, <head>, and <body> tags.) In the section where you want your list items to show up, you'll only need to add one line of code:

<?php printAlbums($albums); ?>

At this point, all of your list items should pop up on your page in some form.

Superglobals, $_GET, and you

Before we go wild with filtering, we're going to take a quick detour and introduce the thing that makes it possible: the superglobal $_GET variable. $_GET allows us to set parameters from the URL and alter the page behavior based on those parameters, which means we can generate new pages without leaving our file.

We can set parameters in our URL with the following syntax appended to the end: ?var=value. var is the name of the parameter, while value is its value. In our case, our URLs are going to look something like this when we filter them: ?tag=rock. Since we're filtering by tag, it only makes sense to name our URL parameter accordingly.

If I want to retrieve the value I set in the URL, I need to use $_GET like this in my code:

$tag = $_GET['tag'];

Let's go see how $_GET will be used in our filtering system.

Let's get filtering

This is where this starts getting fun! This section will introduce quite a bit more than the ones before it, so grab some coffee, buckle up, and let's get into it.

The filter function

Let's get the filtering function taken care of. I will call it filterList(), and it will take an array of albums $albums and the tag I want to filter by, $tag. This will involve our first use of conditional statements in this project, so let's take a second to think about what we want to happen here.

When we filter by a tag, the results should be only the entities (albums, in this case) that we've given that tag to. We need to include albums with our desired tag and omit the rest. To put that in a way a computer would understand: If an album has a tag, it should be included in the results. Else, it shouldn't be included. (We'll see in a second that the "else" portion of our statement is redundant, but it's still good to think about.)

Okay, so how are we going to do this? We don't want to modify our original array, so we need to make a new one that will hold all our filtered results. I will call that array $filteredAlbums.

Next, we need to go through our $albums array, which can be done with the foreach loop. When we look at each album, we need to convert its tags into an array so we can search through them. We already wrote a function that does that earlier, splitTags(). So, we'll use that and assign it to a new variable, which I'm calling $albumTags.

So far, we have this:

function filterAlbums($albums, $tag) {
    $filteredAlbums = array();
    foreach ($albums as $album) {
        $albumTags = splitTags($album['tags']);
    }
    return $filteredAlbums;
}

The last thing we need to do is tell the computer when it should add things to the filtered array. As we discussed before, this occurs when an album has the tag we want. This is a surprisingly easy task because our tags are already in an array, and PHP has a built-in function called in_array(). This function does exactly what it sounds like it does: it looks for a value in an array. If it can find it, it will return a Boolean value of true. Otherwise, it returns false. This means we can use the in_array() function as a condition for our conditional statement.

All that's left to do is tell the computer to add an album to $filteredAlbums. We'll do that by pushing that album onto $filteredAlbums. "Push" is programming-speak for appending an item onto the end of an array. PHP has a built-in array_push() function, but we're going to use the shorthand syntax of $array[] = $value; because it executes faster.

Our finished filtering function is this:

function filterAlbums($albums, $tag) {
    $filteredAlbums = array();
    foreach ($albums as $album) {
        $albumTags = splitTags($album['tags']);
        if (in_array($tag, $albumTags)) {
            $filteredAlbums[] = $album;
        }
    }
    return $filteredAlbums;
}

You may notice we didn't include an else statement after the if statement. This is because our else action is to simply do nothing, and we don't need to include a whole code block just for doing nothing.

That was a lot of thinking for such a short function! The good thing is that thinking like a computer gets easier with practice, so stuff like this won't take nearly as long.

As a bonus, those who are more familiar with programming (or anyone who's just curious) may be interested in reading an alternate solution(?) to this problem. It's shorter, but it has a pretty big caveat attached to it...

An alternate solution?

Technically, I should be able to look for tag matches without doing all this array conversion, right? PHP has the strpos() function, which lets us look for substring matches inside of a string. I could have done this just using strings, right?

function filterAlbums($albums, $tag) {
    $filteredAlbums = array();
    foreach ($albums as $album) {
        if (strpos($album['tags'], $tag) {
            $filteredAlbums[] = $album;
        }
    }
    return $filteredAlbums;
}

And the answer is yes... but this works only until you have a tag that's a substring of another tag, like "rocks" and "rock" or something like that. On the other hand, in_array() has to look for exact array element matches, in which case "rocks" and "rock" would be treated as completely separate entities.

Printing filtered arrays

Now that we have a function in hand for filtering, we need to get our filtered array to actually show up on the page. This requires even more conditional statements. Good thing we already got some practice in!

Go to the printAlbums() line that you put in in section 2 and delete it - we're going to replace it with some more complicated code using our new filter function. And now $_GET will make an appearance.

We can use an if statement to check whether our URL has filter criteria in it. Specifically, we want to use the built-in isset() function, which returns true if a variable is set and false if it isn't. If we do have filter criteria, we should store that tag in a new variable and use it in our filterAlbums() function that we just made. The filtered array will also be stored in a new variable, which we can then pass through our printAlbums() function that we made in section 2 (make sure to use the right variable).

Here's what we have so far:

if (isset($_GET['tag'])) {
    $tag = $_GET['tag'];
    $taggedAlbums = filterAlbums($albums, $tag);
    printAlbums($taggedAlbums);
}

But what if the URL isn't asking us to filter anything? We don't want to just do nothing if our condition isn't met. If no tag filter is set, we should just print out our full list of albums. So we'll use an else statement with the printAlbums() function again, but with the full array that we defined at the beginning (...also known as the code we put in in section 2).

if (isset($_GET['tag'])) {
    $tag = $_GET['tag'];
    $taggedAlbums = filterAlbums($albums, $tag);
    printAlbums($taggedAlbums);
} else {
    printAlbums($albums);
}

Making a tag masterlist

We're well on our way to a fully-functional tagging system! We just need to add links to all our tags so our website users can take advantage of this cool thing we've made. First, let's modify the printTags() function a little bit so clicking on a tag will take us to its filter page:

function printTags($tagArray) {
    foreach ($tagArray as $tag) {
        echo "<a href=\"?tag=$tag\">#$tag</a> ";
    }
}

Now, let's make a full list of tags so our users can see all their options. This function uses a bunch of stuff that we've learned about already, so hopefully it looks familiar.

We need a list of each distinct tag, so we don't want to indiscriminately throw all our tags into a bucket. The plan is to go through the tag array of each album and see if each tag is already in our tag masterlist. If it isn't, that tag gets added to the masterlist, and we continue on. Hopefully, by now you're thinking that we can do this with the in_array() function.

This time, our condition for our if statement is that a tag isn't already in the list. This calls for the not operator, which is represented by an exclamation point (!). The not operator reverses the Boolean value of the thing it's modifying. We're going to use !in_array, which will return true if an item isn't in the array and false if an item is. It's the opposite of how the in_array function usually behaves.

Let's see if you can look at the following function and pick apart what's going on.

function getAllTags($array) {
    $tagList = array();
    
    foreach ($array as $entry) {
        $entryTags = splitTags($entry['tags']);
        foreach ($entryTags as $tag) {
            if (!in_array($tag, $tagList)) {
                $tagList[] = $tag;
            }
        }
    }
    
    return $tagList;
}

If you're stuck or want to check your answer, here's an explanation:

What's going on

First, we're initializing a new array to put all our tags in, $tagList. We'll pass through each album/array entry with a foreach loop. We'll split its tags into an array called $entryTags, then use another foreach loop to pass through each tag. As we discussed above, we're using !in_array() to check whether a tag isn't in the masterlist. If not, we'll push that tag onto the end of $tagList.

If you want your tags to be in alphabetical order, you can use the natsort() function before you return the array, like this. natsort() is a sorting function that sorts things in the order that a human would. The main difference usually happens with numbers: A normal computer sorting algorithm might put 10 after 1, but natsort will put 10 after 9 instead.

function getAllTags($array) {
    $tagList = array();
    
    foreach ($array as $entry) {
        $entryTags = splitTags($entry['tags']);
        foreach ($entryTags as $tag) {
            if (!in_array($tag, $tagList)) {
                $tagList[] = $tag;
            }
        }
    }
    
    natsort($tagList);
    return $tagList;
}

Now I need to get this list to show up on the website. I already have a function that prints tags for me, so I just need to add a line of code where I want my list of tags to be printed:

<?php
    $tagList = getAllTags($albums);
    printTags($tagList);
?>

Now, users can click on a tag link (either in the masterlist or the tags displayed on individual albums) and bring up all albums with that tag.

The fun part: debugging

If you're following along and writing everything from scratch (hopefully you are), it's highly likely that you're going to hit a snag that prevents all or part of your page from rendering. This means something went wrong, and now you have to hunt through your code to fix it, a process known as debugging. PHP is especially tricky to debug because your code gets read before your page renders.

The first thing to do when you're debugging is enable error reporting, which can help guide you in the right direction of a mistake. Add error_reporting(E_ALL); to the very first line of PHP on your page. (You should disable error reporting once you've finished.)

Here are some common errors that can completely break a script:

  • Missing semicolon (;) at the end of a line
  • Too many or too few brackets, braces, or parentheses (check if your editor can highlight bracket pairs)
  • Missing quotes in strings
  • Incorrect variable names (typos, forgot a dollar sign)
  • Incorrect function arguments (names or data types)
  • Missing <?php or ?> tags

Something you can do to make the debugging process less annoying: don't wait until you've written an entire script to upload and test it. Once you finish a task, upload and test it. For this project, it's a good idea to set checkpoints in the following places:

  1. Printing all albums (no filtering)
  2. Filtering albums, then printing the filtered albums
  3. Making and printing the tag list

Extra credit

These are some extra bells and whistles that you might find useful, but aren't necessary for the program to be functional. If you're happy with your program as-is, jump to the conclusion.

Counting tagged items

We can modify our code to display the number of items in each tag in our big tag list. This task is a bit of a doozy because there are quite a few things that need to be modified for this to work - but you're already here, so let's get into doozy-ing.

First, we'll modify our getAllTags() function. This time, I'll show you the completed function first, then explain what's going on.

function getAllTags($array) {
    $tagList = array();
    
    foreach ($array as $entry) {
        $entryTags = splitTags($entry['tags']);
        
        foreach ($entryTags as $tag) {
            $key = array_search($tag, array_column($tagList, 'name'));
            if ($key == false) {
                $tagList[] = array(
                    "name" => $tag,
                    "count" => 1
                );
            } else {
                $tagList[$key]['count'] += 1;
            }
        }
        
    }
    
    array_multisort(array_column($tagList,'name'), SORT_ASC, SORT_NATURAL, $tagList);
    return $tagList;
}

The big ticket change here is that we're turning our big tag array $tagList into a multidimensional array, where each tag is an array with two keys: name and count. This will let us store our tag name and how many times it appears together in one place.

Because of this, we're going to switch from in_array() to array_search(). in_array() tells us if a value is in an array, while array_search() tells us where a value is in an array (if it is at all). It does this by looking for a specific value (the first argument, in this case $tag from the entry tag array) in an array (the second argument, in this case the name field of our $tagList array). To only access a specific key of a multidimensional array, we can use array_column() to extract just those key values in an array.

array_search() will return an array index or key if it finds a match and false otherwise. We can take advantage of this to rephrase our conditional statement with $key == false as its condition. Functionally, this means the same thing as !in_array(). But we need that key value if it does exist for a new else statement: now, if we do find a particular tag in $tagList, we should add 1 to its associated count.

Finally, we can't use natsort() on a multidimensional array. For that, we need the array_multisort() function, which lets us sort our arrays using the values of one of the keys. In this case, we would like to sort by name. The first argument of array_multisort() are the values we'd like to sort by. In this case, we'll grab the names of the tags using array_column() again. The second argument is the order we'd like to sort our arrays, which is SORT_ASC (ascending) for alphabetical order. The third argument specifies the algorithm to use; I will use SORT_NATURAL so it uses the same algorithm as natsort(). The last argument is the array we're trying to sort, which is $tagList. And now we're sorted!

Now that we want to print out counts along with our list of tags, we can't reuse printTags(), so we'll make a new function called printAllTags(). This is almost identical to printTags(), but we need to call both the name and count values for each tag.

function printAllTags($tagArray) {
    foreach ($tagArray as $tag) {
        $name = $tag['name'];
        $count = $tag['count'];
        echo "<a href=\"?tag=$name\">#$name ($count)</a> ";
    }
}

I need to change my tag masterlist printing code in the body of my page accordingly:

<?php
    $tagList = getAllTags($albums);
    printAllTags($tagList);
?>

And now my list of tags will tell me how many entries are sorted under each tag!

Error handling

When you're grabbing parameters from superglobals (or anything that can be modified directly by the user), it's a good idea to check if a parameter is actually valid or not. In this case, a user could type in whatever they wanted for a tag; if it doesn't exist, they'll end up with a blank page. That's not the worst thing in the world, but it's polite to tell people why they're getting a blank page.

We can handle nonexistent tags by modifying our display code to include an extra conditional statement. This statement will use in_array() once again to check if the tag in the URL matches a tag in our list. If it doesn't, the page will print an error informing the viewer that there aren't any albums with that tag.

Our new code with error handling looks like this:

if (isset($_GET['tag'])) {
    $tag = $_GET['tag'];
    if (!in_array($tag, $tagList)) {
        echo "<p>There are no albums with this tag.</p>";
    } else {
        $taggedAlbums = filterAlbums($albums, $tag);
        printAlbums($taggedAlbums);
    }
} else {
    printAlbums($albums);
}

If you modified your code to include counts in the previous section, we'll use a slightly different code here because $tagList is no longer a 1-dimensional array. Instead of searching directly in $tagList, we need to use array_column() to search just the name values. Otherwise, everything is the same:

if (isset($_GET['tag'])) {
    $tag = $_GET['tag'];
    if (!in_array($tag, array_column($tagList, 'name'))) {
        echo "<p>There are no albums with this tag.</p>";
    } else {
        $taggedAlbums = filterAlbums($albums, $tag);
        printAlbums($taggedAlbums);
    }
} else {
    printAlbums($albums);
}

Conclusion

If you made it this far, congratulations on finishing a whole functional PHP project! Hopefully you feel more comfortable with some programming basics now. I also hope that you're feeling motivated to learn more now that you've done something cool.

You can find downloads of the demo files below. I can't stop you from downloading the demo and modifying it instead of reading and following along with this tutorial, but if you're really wanting to learn to program, it would benefit you more to take the time to learn. Have fun!

Download Download (extra credit)