This is a DTO (data transfer object) that contains information on a drawing.

(double-click the code to select all)
<?php
namespace sa;

/**
 * Contains information on an image.
 * @author Michael Angstadt
 */
class Note{
	/**
	 * The relative path to the full resolution image.
	 * @var string
	 */
	public $image;

	/**
	 * The relative path to the thumbnail image.
	 * @var string
	 */
	public $thumbnail;
	
	/**
	 * The description of the image or empty string if there is no description
	 * @var string
	 */
	public $text;
	
	/**
	 * The string that is used as a GET parameter to display this image.
	 * @var string
	 */
	public $id;
	
	/**
	 * The date of the image
	 * @var integer (timestamp)
	 */
	public $date;
}

This data access object (DAO) is responsible for getting the data on each drawing. It takes the directory that the drawings are located in as a constructor argument, and contains a method that gets the information on all of the drawings in that directory.

The notes are sorted by date using the usort function. With this function, you provide your own comparison function, which allows you to sort the data however you wish.

(double-click the code to select all)
<?php
namespace sa;
use utils\FileUtils;

/**
 * Retrieves the image data.
 * @author Michael Angstadt
 */
class NotesDao {
	/**
	 * The directory where the notes are stored.
	 * @var string
	 */
	private $dir;
	
	/**
	 * @param string $dir the directory where the images are stored (web friendly)
	 */
	public function __construct($dir){
		$this->dir = $dir;
	}
	
	/**
	 * Gets all the notes.
	 * @return array(Note) the notes, sorted by date descending
	 */
	public function getAllNotes(){
		$notes = array();
	
		//get the list of all files in this format: "20070314.jpg"
		$fileNames = FileUtils::listFilesRecursive($this->dir, function($path){
			return is_dir($path) || preg_match('/^\\d{8}\\.jpg$/', basename($path));
		});

		foreach ($fileNames as $f){
			$id = basename($f, '.jpg'); //example: given "notes/20110102.jpg", returns "20110102"
			$date = strtotime($id);
			
			//ignore sketches that have dates in the future
			if ($date > time()){
				continue;
			}

			$note = new Note();
			
			$note->image = $f;
			$note->id = $id;
			$note->date = $date;
			
			$thumbnail = dirname($f) . '/t_' . $note->id . '.jpg';
			if (!file_exists($thumbnail)) {
				$thumbnail = "$this->dir/nothumb.jpg";
			}
			$note->thumbnail = $thumbnail;
			
			$descrFile = dirname($f) . '/' . $note->id . '.txt';
			if (file_exists($descrFile)){
				$note->text = file_get_contents($descrFile);
			} else {
				$note->text = '';
			}
			
			$notes[] = $note;
		}
		
		//sort by date descending
		usort($notes, function($a, $b){
			return $b->date - $a->date;
		});
	
		return $notes;
	}
}

The unit test for the NotesDao class. I create a number of fake files to simulate an actual image directory, and then test to make sure the DAO returns the correct data.

I don't need to use real images because NotesDao doesn't care about the image data, just the filenames of the images. This means that I can programmatically create empty files for the images, and then delete them when the test finishes. This is the kind of thing you should try to do whenever possible when unit testing a class that reads files off the filesystem. Create the files in your unit test code, and then delete them when the test finishes. Unit tests should be as self-contained as possible.

(double-click the code to select all)
<?php
namespace sa;
use utils\FileUtils;
use \DateTime;
use \DateInterval;

/**
 * Tests the NotesDao class.
 * @author Michael Angstadt
 */
class NotesDaoTest extends \PHPUnit_Framework_TestCase{
	/**
	 * The directory where the test files will be held.
	 * @var string
	 */
	private $dir = 'notes-test';
	
	public function setUp(){
		mkdir($this->dir);
	}
	
	public function tearDown(){
		FileUtils::rmdirRecursive($this->dir);
	}
	
	/**
	 * Tests the getAllNotes() method.
	 */
	public function testGetAllNotes(){
		//no thumb, no description
		touch("$this->dir/20050314.jpg");
		
		//no description
		touch("$this->dir/20051202.jpg");
		touch("$this->dir/t_20051202.jpg");
		
		//has everything
		touch("$this->dir/20050101.jpg");
		touch("$this->dir/t_20050101.jpg");
		file_put_contents("$this->dir/20050101.txt", 'The image description.');
		
		//has a date that's in the future, so it should be ignored
		$date = new DateTime();
		$date->add(new DateInterval('P1D'));
		touch("$this->dir/" . $date->format('Ymd') . ".jpg");
		
		//has today's date, so it should not be ignored
		$today = new DateTime();
		touch("$this->dir/" . $today->format('Ymd') . ".jpg");
		
		//it should ignore these file
		touch("$this->dir/some-file.txt");
		touch("$this->dir/another-file.jpg");
		
		//create a sub-directory with sketches in it
		$subDir = "$this->dir/subdir";
		mkdir($subDir);
		touch("$subDir/20050220.jpg");
		touch("$subDir/t_20050220.jpg");
		file_put_contents("$subDir/20050220.txt", 'Sub dir image description.');
		touch("$subDir/20060115.jpg");
		
		$dao = new NotesDao($this->dir);
		$notes = $dao->getAllNotes();
		$this->assertEquals(6, count($notes));
		
		$i = 0;
		
		$note = $notes[$i++];
		$this->assertEquals("$this->dir/" . $today->format('Ymd') . ".jpg", $note->image);
		$this->assertEquals($today->format('Ymd'), $note->id);
		$this->assertEquals(strtotime($today->format('Ymd')), $note->date);
		$this->assertEquals("$this->dir/nothumb.jpg", $note->thumbnail);
		$this->assertEquals('', $note->text);
		
		$note = $notes[$i++];
		$this->assertEquals("$subDir/20060115.jpg", $note->image);
		$this->assertEquals('20060115', $note->id);
		$this->assertEquals(strtotime('20060115'), $note->date);
		//the "nothumb.jpg" image is always in the root directory, even if the sketch is in a sub directory
		$this->assertEquals("$this->dir/nothumb.jpg", $note->thumbnail);
		$this->assertEquals('', $note->text);
		
		$note = $notes[$i++];
		$this->assertEquals("$this->dir/20051202.jpg", $note->image);
		$this->assertEquals('20051202', $note->id);
		$this->assertEquals(strtotime('20051202'), $note->date);
		$this->assertEquals("$this->dir/t_20051202.jpg", $note->thumbnail);
		$this->assertEquals('', $note->text);
		
		$note = $notes[$i++];
		$this->assertEquals("$this->dir/20050314.jpg", $note->image);
		$this->assertEquals('20050314', $note->id);
		$this->assertEquals(strtotime('20050314'), $note->date);
		$this->assertEquals("$this->dir/nothumb.jpg", $note->thumbnail);
		$this->assertEquals('', $note->text);
		
		$note = $notes[$i++];
		$this->assertEquals("$subDir/20050220.jpg", $note->image);
		$this->assertEquals('20050220', $note->id);
		$this->assertEquals(strtotime('20050220'), $note->date);
		$this->assertEquals("$subDir/t_20050220.jpg", $note->thumbnail);
		$this->assertEquals('Sub dir image description.', $note->text);
		
		$note = $notes[$i++];
		$this->assertEquals("$this->dir/20050101.jpg", $note->image);
		$this->assertEquals('20050101', $note->id);
		$this->assertEquals(strtotime('20050101'), $note->date);
		$this->assertEquals("$this->dir/t_20050101.jpg", $note->thumbnail);
		$this->assertEquals('The image description.', $note->text);
	}
}

A utility class that NotesDao and NotesDaoTest uses, which contains filesystem helper functions.

(double-click the code to select all)
<?php
namespace utils;

/**
 * A collection of filesystem-related utility methods.
 * @author Michael Angstadt
 */
class FileUtils {
	/**
	 * Recursively removes a directory that has files in it.
	 * @param string $dir the directory to delete
	 */
	public static function rmdirRecursive($dir) {
		//delete the files and get a list of the directories (breadth first)
		$stack[] = $dir;
		for ($i = 0; $i < count($stack); $i++) {
			$currentDir = $stack[$i];
			if ($dh = opendir($currentDir)) {
				while (($name = readdir($dh)) !== false) {
					if ($name != '.' && $name != '..'){
						$path = "{$currentDir}/{$name}";
						if (is_file($path)) {
							unlink($path);
						}
						else if (is_dir($path)) {
							$stack[] = $path;
						}
					}
				}
			}
		}
		
		//delete the directories
		for ($i = count($stack)-1; $i >= 0; $i--) {
			rmdir($stack[$i]);
		}
	}
	
	/**
	 * Gets all files in a directory, and recursively in all sub-directories.
	 * @param string $parentDir the directory
	 * @param callback $filterFunc (optional) a custom callback function for filtering the files that are returned and the directories that are traversed.
	 * Files are only returned and directories are only traversed if this function returns true.
	 * If no callback is specified, then all files/directories will be returned/traversed.<br>
	 * <b>Signature:</b> <code>boolean function($fileOrDirPath)</code>
	 * @return array(string) the paths to the files
	 */
	public static function listFilesRecursive($parentDir, $filterFunc = null) {
		$fileList = array();
		$stack[] = $parentDir;
		while ($stack) {
			$currentDir = array_pop($stack);
			if ($dh = opendir($currentDir)) {
				while (($name = readdir($dh)) !== false) {
					if ($name != '.' && $name != '..'){
						$path = "$currentDir/$name";
						if ($filterFunc == null || $filterFunc($path)){
							if (is_file($path)) {
								$fileList[] = $path;
							}
							else if (is_dir($path)) {
								$stack[] = $path;
							}
						}
					}
				}
			}
		}
		return $fileList;
	}
	
	/**
	 * Lists all files and directories in a given directory.
	 * @param string $parentDir the directory
	 * @param callback $filterFunc (optional) a custom callback function for filtering the files/directories that are returned.<br>
	 * Files/directories are only returned if this function returns true.  If no callback is specified, then all files/directories will be returned.<br>
	 * <b>Signature:</b> <code>boolean function($fileName)</code>
	 * @return array(string) the paths to the files and directories.  All the paths start with the parent directory.
	 */
	public static function listFiles($parentDir, $filterFunc = null) {
		$list = array();
		if ($dh = opendir($parentDir)) {
			while (($name = readdir($dh)) !== false) {
				if ($name != '.' && $name != '..'){
					$path = "$parentDir/$name";
					if ($filterFunc == null || $filterFunc($name)){
						$list[] = $path;
					}
				}
			}
		}
		return $list;
	}
}