New and improved error handling class!

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

User avatar
daedalus__
DevNet Resident
Posts: 1925
Joined: Thu Feb 09, 2006 4:52 pm

New and improved error handling class!

Post by daedalus__ »

EDIT: Keep going down if you want to see the observer examples
EDIT2: This thread will be named back to observer examples later


I spent all morning writing this class, almost 8 straight hours. There is probably alot of work to do on it still but I'm glad to finally have a basic idea.

I tested outputting errors to the browser, and to a log file, both work great!

I would appreciate some feedback on this, anything really, try to keep it constructive and on-topic.

thanks and sorry in advance if its garbage

Code: Select all

<?php

class ErrorHandler
{
	private $errors;

	private $developer_mode;
	private $output_mode;	// 0 = View, 1 = File, 2 = Db | if ($developer_mode == 1) { $this = 0 }

	private $log_file_path;
	private $log_file_name;
	private $log_file;
	private $log_string = '';
	private $log_array;
	
	private $log_table;

	// Constructor
	function __construct($developer_mode, $output_mode, $log_file_path, $log_file_name, $log_table)
	{
		$this->SetDeveloperMode($developer_mode);
		$this->SetOutputMode($output_mode);
		$this->SetLogFilePath($log_file_path);
		$this->SetLogFileName($log_file_name);
		$this->SetLogTable($log_table);
		set_error_handler(array($this, 'LogError'));
	}

	// Setters
	function SetDeveloperMode($int)
	{
		// if $int is an integer and less than 2
		if ((is_integer($int) != FALSE) && ($int < 2))
		{
			$this->developer_mode = $int;
		}
		// if anything else, kill the script we cannot continue
		else
		{
			die('<b>Fatal Error</b>: Could not construct object, could not set Developer Mode,  arguement must be an integer!');
		}
	}

	function SetOutputMode($int) // This always need to be set AFTER $developer_mode
	{
		// if $int is an integer and less than 2
		if ((is_integer($int) != FALSE) && ($int < 2))
		{
			if (1 == $this->developer_mode)
			{
				$this->output_mode = 1; // File
			}
			else
			{
				$this->output_mode = $int;
			}
		}
		// if anything else, kill the script we cannot continue
		else
		{
			die('<b>Fatal Error</b>: Could not construct object, could not set Output Mode arguement must be an integer!');
		}
	}

	function SetLogFilePath($path)
	{
		if (is_string($path) != FALSE)
		{
			if (preg_match('/[^a-zA-Z0-9_.\/]/', $path) == FALSE)
			{
				$this->log_file_path = $path;
				if (substr($path, strlen($path)-1) != '/')
				{
					$this->log_file_path .= '/';
				}
			}
			else
			{
				die('<b>Fatal Error</b>: Could not construct object, could not set Log file path, arguement cannot contain any characters that are not foward slash, a-z, A-Z, 0-9, underscore, or period."!');
			}
		}
		else
		{
			die('<b>Fatal Error</b>: Could not construct object, could not set Log file path, arguement must be a string!');
		}
	}

	function SetLogFileName($filename)
	{
		if (is_string($filename) != FALSE)
		{
			if (preg_match('/[^a-zA-Z0-9_.]/', $filename) == FALSE)
			{
				$this->log_file_name = $filename;
			}
			else
			{
				die('<b>Fatal Error</b>: Could not construct object, could not set Log file name, arguement cannot contain any characters that are not a-z, A-Z, 0-9, underscore, or period."!');
			}
		}
		else
		{
			die('<b>Fatal Error</b>: Could not construct object, could not set Log file name, arguement must be a string!');
		}
	}

	function SetLogTable()
	{
		// I'm freaking tired I'll do this later, it's just going to check if the table exists, and if not, it will create it.
	}

	// Getters
	function GetDeveloperMode()
	{
		return $this->developer_mode;
	}

	function GetOutputMode()
	{
		return $this->output_mode;
	}

	function GetLogFilePath()
	{
		return $this->log_file_path;
	}
	
	function GetLogFileName()
	{
		return $this->log_file_name;
	}

	function GetLogTable()
	{
		return $this->log_table;
	}

	// Error Logging
	function LogError($level, $message, $file, $line)
	{
		$this->StoreError($level, $message, $file, $line);
		if (1 == $level)
		{
			$this->OutputErrorsToView();
			die();
		}
	}

	function OutputErrorsToFile()
	{
		if (file_exists($this->GetLogFilePath()) == FALSE)
		{
			mkdir($this->GetLogFilePath());
		}
		$this->log_file = fopen($this->GetLogFilePath().$this->GetLogFileName(), 'a+') or die('could not open file');
		foreach ($this->errors as &$layer1)
		{
			$this->log_string .= implode(',', $layer1);
			$this->log_string .= "\r\n";
		}
		fwrite($this->log_file, $this->log_string);
		fclose($this->log_file);
	}

	function OutputErrorsToDb()
	{
		foreach ($this->errors as &$layer1)
		{
			$sql = "INSERT INTO ".$this->GetLogTable()." (id, level, message, file, timestamp, line, class, function)
					VALUES (NULL, '%d', '%s', '%s', current_timestamp(), '%d')";
			$sql = vprintf($sql, array($layer1[0], 
										$layer1[1], 
										$layer1[2], 
										$layer1[3]));

			$link = mysql_connect('localhost', 'root', '****');
			mysql_query($sql);
			mysql_close($link);
		} 
	}

	// Error Viewing
	function OutputErrorsFromDb($where = 0, $order = 0)
	{
		$sql = 'SELECT id, level, message, file, UNIX_TIMESTAMP(timestamp), line, class, function
				FROM '.$this->GetLogTable().'
				ORDER BY '.$order;
		if ((0 != $where) && (is_array($where) != FALSE))
		{
			$sql .= 'WHERE '.$where[0].' = '.$where[1];
		}
		if ((0 != $order) && (is_string($order) != FALSE))
		{
			$sql .= 'ORDER BY '.$order;
		}
		$link	= mysql_connect('localhost', 'root', 'vfr43edc');
		$res	= mysql_query($sql);
		mysql_close($link);
		
		if (mysql_num_rows($result) != FALSE)
		{
			print "<table>";
			while ($row = mysql_fetch_assoc($res))
			{
				print "<tr>
						<td>$row[0]</td>
						<td>$row[1]</td>
						<td>$row[2]</td>
						<td>$row[3]</td>
						<td>$row[4]</td>
						</tr>";
			}
			print "</table>";
		}
		else
		{
			print "There are no errors logged in the database";
		}
	}
	
	function OutputErrorsFromFile()
	{
		$this->log_file		= fopen($this->GetLogFilePath().$this->GetLogFileName(), 'r');
		$this->log_string	= fread($this->log_file, 8192);
		fclose($this->log_file);
		
		$this->log_string = explode("
		", $this->log_string);
		foreach ($this->log_string as &$layer1)
		{
			$this->log_array[] = explode(', ', $layer1);
		}
		
		print "<table>";
		foreach ($this->log_array as &$layer1)
		{
			print "<tr>";
			foreach ($layer1 as &$layer2)
			{
				print "<td>$layer2</td>";
			}
			print "</tr>";
		}
		print "</table>";
	}

	function OutputErrorsToView()
	{
		$i = 1;
		foreach ($this->errors as &$layer1)
		{
			print "<pre>
Error #$i:
	Level:		$layer1[0]
	File:		$layer1[1]
	Message:	$layer1[2]
	Line:		$layer1[3]
	Time:		".date('U', $layer[4])."
</pre>";
			$i++;
		}
	}
	
	// Error Handling
	function StoreError($level, $file, $message, $line)
	{
		$this->errors[count($this->errors)+1] = array($level, $file, $message, $line, date('U'));
	}
}
?>
Last edited by daedalus__ on Wed Oct 24, 2007 1:45 pm, edited 3 times in total.
User avatar
feyd
Neighborhood Spidermoddy
Posts: 31559
Joined: Mon Mar 29, 2004 3:24 pm
Location: Bothell, Washington, USA

Post by feyd »

I think a bit of separation is in order. Specifically, a logging class doesn't need to know where the data is being output. It could use a passed class to handle the specific final output. In this way, you would use the same interface whether the output was to a file, database or screen or a combination thereof. It also allows for other output types that you haven't implemented or thought of to be integrated with overall ease. For instance, if I wanted to output to a debugging session on some external IP, it would require modifying this classes internals, possibly breaking it in the process.
User avatar
daedalus__
DevNet Resident
Posts: 1925
Joined: Thu Feb 09, 2006 4:52 pm

Post by daedalus__ »

so.. output_type extends errorhandler ?
User avatar
feyd
Neighborhood Spidermoddy
Posts: 31559
Joined: Mon Mar 29, 2004 3:24 pm
Location: Bothell, Washington, USA

Post by feyd »

Daedalus- wrote:so.. output_type extends errorhandler ?
generally, no. It would be a separated branch that just understands being told to log this and that. The implementation held within would know where to send the entries.
User avatar
daedalus__
DevNet Resident
Posts: 1925
Joined: Thu Feb 09, 2006 4:52 pm

Post by daedalus__ »

k, thanks ^^
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Have a look into the observer pattern if you want a clean way to seperate your output/logging stuff ;)
User avatar
daedalus__
DevNet Resident
Posts: 1925
Joined: Thu Feb 09, 2006 4:52 pm

Post by daedalus__ »

I'm looking but I don't really understand..?

lol
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Daedalus- wrote:I'm looking but I don't really understand..?

lol
OK the observer is a pattern which allows two objects to interact with each other without really understanding what either do. One object (the oberserver) sits inside of another object (the obbservable) and the observable passes message to the oberserver. That's the esscence of it.

If you're looking at online examples they all seem to include too much fluff to see what's actually part of the pattern and what's not so here's the basics:

Code: Select all

/**
 * This is the observable and contains instances
 * of all the obervers
 */
class myObservable
{
    /**
     * This can hold any number of objects
     * @var array observer collection
     */
    private $observers = array();
    
    public function __construct()
    {
        //
    }
    /**
      * Load in an observer object
      * @param object observer
      * @return void
      */
    public function loadObserver($object)
    {
        $this->observers[] =& $object;
    }
    /**
     * Passes message to the observers
     * The object doesn't care what the oberservers do with the message
     * @param string message
     * @return void
     */
    private function callObservers($message)
    {
         for ($i = 0; $i < count($this->observers); $i++)
         {
             $this->observers[$i]->takeMessage($message);
         }
    }
    /**
     * This is just my basic example of what might trigger
     * the observable to call the observers.  Usually this would
     * be something like an error generator or a message to log.
     * @param string user input
     * @return string output
     */
     public function makeBold($string)
     {
         $this->callObservers($string);
         return '<strong>'.$string.'</strong>';
     }
}

/**
 * This is a basic observer class and will be loaded into
 * the observable.  It accepts messages via it's "takeMessage()"
 * method.
 */
class myObserver
{
    /**
     * Just something I'm using as an example.  We'll write messages
     * to a file
     * @var string file path
     */
    private $file;
    
    public function __construct($file)
    {
        $this->file = $file;
    }
    /**
     * We accept messages from our observable here
     * What we do with it is completely up to us.
     * @param string message
     * @return void
     */
     public function takeMessage($string)
     {
         $handle = fopen($this->file, 'a+');
         fwrite($handle, $string);
         fclose($handle);
     }
}

//Create an instance of the observable
$observable = new myObservable;

//Pass it an observer to talk to (they're chatty bunch these observables!)
$observable->loadObserver(new myObserver('/tmp/log.txt'));

//Just use it as normal and marvel at how the observer responds to what you do,
// without caring what it is you're doing 
echo $observable->makeBold('Hello world!'); //Outputs a bold string and logs to a file
User avatar
daedalus__
DevNet Resident
Posts: 1925
Joined: Thu Feb 09, 2006 4:52 pm

Post by daedalus__ »

I've been staring at this example for about 3 hours but can't decide how to use it...

?
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

Maybe a use case example will make it clearer for you. The idea is that Logging is one thing and writing the log is a separate thing. So when using the Logger it might look like this:

Code: Select all

$logger = new Logger(new Logger_Filewriter('/this/is/my.log'));
$logger->logError('Yikes!');
// or
$logger = new Logger(new Logger_Dbwriter('mydb', 'mytable', 'myuser', 'mypassword'));
$logger->logError('Holy Moly!');
That way the Logger only need to know how to store the message and the polymorphic interface to the writers.
(#10850)
User avatar
daedalus__
DevNet Resident
Posts: 1925
Joined: Thu Feb 09, 2006 4:52 pm

Post by daedalus__ »

...that's alot simpler than people make it look? lol
Yossarian
Forum Contributor
Posts: 101
Joined: Fri Jun 30, 2006 4:43 am

Post by Yossarian »

Jason Sweats PHP Patterns book uses error logging as the example when explaining the Observer pattern.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Like this sort of thing:

Code: Select all

<?php

class ErrorHandler
{
	private $handlers;
	
	public function __construct($handlers=array())
	{
		$this->handlers =& $handlers;
	
		set_error_handler(array($this, 'logError'));
	}
	
	public function logError($level, $message, $file, $line)
	{
		for ($i = 0; $i < count($this->handlers); $i++)
		{
			$this->handlers[$i]->notifyOfError($level, $message, $file, $line);
		}
	}
	
	//All your other get and set stuff is ok to go here I guess
}

class ErrorHandler_DbWriter
{
	private $table;
	private $conn;
	
	public function __construct($host, $user, $pass, $db, $table)
	{
		$this->conn = mysql_connect($host, $user, $pass);
		mysql_select_db($db, $this->conn);
		
		$this->table = $table;
	}

	public function notifyOfError($level, $message, $file, $line)
	{
		$sql = "
		INSERT INTO
			".$this->table." (
			id, level, message, file, timestamp, line
		)
		VALUES (
			NULL, '$level', '$message', '$file', current_timestamp(), '$line'
		)";
		mysql_query($sql, $this->conn);
	}
	
	public function __destruct()
	{
		mysql_close($this->conn);
	}
}

$logger = new ErrorHandler(array(
	new ErrorHandler_DbWriter('localhost', 'user', 'pass', 'my_db', 'errors_table')
));

?>
You can then pull all that SQL stuff out of the main class and keep it clean. Same for the ViewOutput and FileWriter. So you then choose how you want to log like this:

Code: Select all

$logger = new ErrorHandler(array(
	new ErrorHandler_DbWriter('localhost', 'user', 'pass', 'my_db', 'errors_table'),
	new ErrorHandler_FileWriter('logfile.txt'),
	new ErrorHandler_HTMLOutput()
));
User avatar
daedalus__
DevNet Resident
Posts: 1925
Joined: Thu Feb 09, 2006 4:52 pm

Post by daedalus__ »

You guys succeeded in doing something that hundreds of tutorials and web pages could not do:

You made me understand a design pattern. :p

I want to rename this thread so people who search for observer pattern will find it easy, should i?
User avatar
feyd
Neighborhood Spidermoddy
Posts: 31559
Joined: Mon Mar 29, 2004 3:24 pm
Location: Bothell, Washington, USA

Post by feyd »

people searching for such a pattern will already be able to find it. Whether seeing the name of the thread gets their attention or not is the question.. it's up to you.
Post Reply