Heya! I’m Dave. I’m a product designer, engineer, PM, and team lead who works at Automattic.

Custom PHP Error Log (w/ full code)

I built a custom error handling system, which I find extremely handy, so I’m sharing all of the code below. Feel free to use it however you’d like.

I can’t remember where I got the inspiration for this design. I’d love to give them credit if I can find it.

Over the past year, I’ve been slowly building my side project Crafd.com on weekends. As the sole developer, I’ve handled all of the design, front-end and back-end work.

I built Crafd using vanilla PHP and JavaScript, without any frameworks or libraries. It’s been enjoyable to pare back the bloat of modern web development and focus on raw code again.

Testing has never been my forte. That said, I recognize the value of having some sort of validation to quickly catch bugs after deploying changes. Rather than use an external error handling service, I decided to roll my own error handling system as a learning exercise.

I centralized all MySQL, PHP and JavaScript errors into one UI. Below I will share the code I used to create this error handling system.

Preview

Here’s a quick preview of the page in action:

Code

Here is a recap of the code that is used to run this page:

HTML

<html>
<head>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<meta http-equiv="content-type" content="text/html; charset=utf-8" />
	<title>Error Dashboard</title>
	<link rel="stylesheet" type="text/css" charset="utf-8"  media="screen, projection" href="/css/crafd-admin.css" />
</head>
<body>
	<div class="backdrop"></div>
	<div class="admin-container">
		<!-- LEFT COLUMN -->
		<div class="admin-left">
			<header>
				<a href="" class="menu">
					<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
						<path d="M3 12H21" stroke="#3E3E3E" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
						<path d="M3 6H21" stroke="#3E3E3E" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
						<path d="M3 18H21" stroke="#3E3E3E" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
					</svg>
				</a>
				<h2>Error Logs</h2>
			</header>
			<div class="log-files">
				<?php foreach ($errors as $key => $error) { ?>
				<div class="log-file" id="log-<?php echo safe($key); ?>" data-key="<?php echo safe($key); ?>">
					<span class="log-file-name"><?php echo safe($error['name']); ?></span>
					<span class="log-file-size"><?php echo safe($error['size']); ?></span>
					<a href="/admin/errors/delete/<?php echo str_replace('.log', '', safe($error['name'])); ?>" class="log-file-delete">
						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
					</a>
				</div>
				<?php } ?>
			</div>
		</div>
		<!-- TABLE -->
		<div class="admin-right">
			<div class="top-bar">
				<div class="top-bar-filters">
					<a href="" class="filter filter-mysql show" data-type="mysql">
						<svg height="15" viewBox="0 0 14 15" width="14" xmlns="http://www.w3.org/2000/svg"><g style="stroke:#9E9E9E;stroke-width:1.333333;fill:none;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round" transform="translate(1 1)"><ellipse cx="6" cy="2" rx="6" ry="2"/><path d="m12 6.66666667c0 1.10666666-2.66666667 2-6 2s-6-.89333334-6-2"/><path d="m0 2v9.3333333c0 1.1066667 2.66666667 2 6 2s6-.8933333 6-2v-9.3333333"/></g></svg>
						MySQL
					</a>
					<a href="" class="filter filter-php show" data-type="php">
						<svg height="10" viewBox="0 0 15 10" width="15" xmlns="http://www.w3.org/2000/svg"><g style="stroke:#9E9E9E;stroke-width:1.333333;fill:none;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round" transform="translate(1 1)"><path d="m9.33333333 8 3.99999997-4-3.99999997-4"/><path d="m4 0-4 4 4 4"/></g></svg>
						PHP
					</a>
					<a href="" class="filter filter-s3 show" data-type="js">
						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>
						S3
					</a>
					<a href="" class="filter filter-js show" data-type="js">
						<svg height="11" viewBox="0 0 14 11" width="14" xmlns="http://www.w3.org/2000/svg"><text fill="#9E9E9E" fill-rule="evenodd" font-family="SFProDisplay-Semibold, SF Pro Display" font-size="12" font-weight="500" letter-spacing=".0873409128"><tspan x="0" y="10">JS</tspan></text></svg>
						JavaScript
					</a>
				</div>
				<div class="top-bar-search">

				</div>
			</div>
			<!-- TABLE -->
			<table class="errors" cellpadding="0" cellspacing="0" border="0">
				<thead>
					<tr>
						<th></th>
						<th>Time</th>
						<th>Message</th>
						<th>File</th>
						<th>Line</th>
						<th>User</th>
						<th>URL</th>
					</tr>
				</thead>
				<tbody></tbody>
			</table>
		</div>
	</div>
	<!-- JAVASCRIPT -->
	<script>
		var errors = <?php echo json_encode(safe($errors)); ?>;
	</script>
	<script type="text/javascript" src="/js/crafd-utility.js"></script>
	<script type="text/javascript" src="/js/admin/error.js</script>
	<?php require_once(PUBPATH.'templates/error-row.php'); ?>
</body>
</html>

CSS (crafd-admin.css)

Here are the relevant CSS styles:

/* ------------------------------- */
/* FONTS */
/* ------------------------------- */
@import url('https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;600&display=swap');
/* ------------------------------- */
/* RESET */
/* ------------------------------- */
* { box-sizing: border-box; margin: 0; padding: 0; }
a { color: #2772A8; text-decoration: none; }
a:hover { text-decoration: underline; }
body { background: #F5F5F5; color: #3E3E3E; font-family: 'Figtree', sans-serif; font-size: 12px; text-align: left; }
h2 { font-family: 'Figtree', sans-serif; font-size: 24px; font-weight: 300; line-height: 24px; }
/* ------------------------------- */
/* ADMIN CONTAINER */
/* ------------------------------- */
.admin-container {
	align-items: flex-start;
	display: flex;
	gap: 48px;
	margin: 48px;
}
.admin-container .admin-left {
	width: 240px;
}
.admin-container .admin-right {
	flex: 1;
}
/* ------------------------------- */
/* ERRORS */
/* ------------------------------- */
.errors {
	width: 100%;
}
.errors td {
	background: #FFF;
	border-bottom: 1px solid #DADADA;
	padding: 8px 12px;
}
.errors td:first-child,
.errors th:first-child {
	padding-left: 24px;
}
.errors th {
	padding: 8px 12px;
	text-align: left;
}
.errors tr {
	display: none;
}
.errors tr.show {
	display: table-row;
}
.errors tr.details {
	display: none;
}
.errors tr.details.show {
	display: table-row;
}
.errors tr.details td {
	background: #F5F5F5;
}
.errors tr .details-toggle {
	color: #3E3E3E;
	display: -webkit-box;
	-webkit-line-clamp: 1;
	-webkit-box-orient: vertical;  
	overflow: hidden;
	width: 200px;
}
.errors tr.error-js svg text {
	fill: #21B557;
}
.errors tr.error-mysql svg g {
	stroke: #2290C4 !important;
}
.errors tr.error-php svg g {
	stroke: #9427B8 !important;
}
/* ------------------------------- */
/* HEADER */
/* ------------------------------- */
header {
	align-items: center;
	display: flex;
	height: 29px;
	gap: 16px;
	margin-bottom: 48px;
}
header h2 {
	flex: 1;
	text-align: left;
}
/* ------------------------------- */
/* LOG FILE */
/* ------------------------------- */
.log-file {
	align-items: center;
	background: #FFF;
	border:  1px solid #FFF;
	border-radius: 6px;
	cursor: pointer;
	display: flex;
	gap: 16px;
	margin-bottom: 8px;
	padding: 8px 16px;
}
.log-file:hover {
	border: 1px solid #DADADA;
}
.log-file.selected {
	background: #E8FEF4;
	border: 1px solid #7DCEB7;
}
.log-file-delete:hover svg {
	stroke: #3E3E3E;
}
.log-file-name {
	flex: 1;
	font-weight: bold;
	text-align: left;
}
.log-file-size {
	color: #9E9E9E;
	font-size: 11px;
	text-align: right;
}
/* ------------------------------- */
/* TOP BAR */
/* ------------------------------- */
.top-bar {
	align-items: center;
	display: flex;
	margin-bottom: 16px;
}
.top-bar .filter {
	align-items: center;
	border: 1px solid #DADADA;
	border-radius: 4px;
	box-shadow: inset -1px 0 2px rgba(0,0,0,0.11);
	color: #9E9E9E;
	display: flex;
	height: 29px;
	gap: 8px;
	padding: 6px 12px;
}
.top-bar .filter:hover {
	text-decoration: none;
}
.top-bar .filter-js.show {
	background: #21B557;
	border: 1px solid #21B557;
	color: #FFF;
}
.top-bar .filter-js.show svg text {
	fill: #FFF;
}
.top-bar .filter-mysql.show {
	background: #2290C4;
	border: 1px solid #2290C4;
	color: #FFF;
}
.top-bar .filter-mysql.show svg g {
	stroke: #FFF !important;
}
.top-bar .filter-php.show {
	background: #9427B8;
	border: 1px solid #9427B8;
	color: #FFF;
}
.top-bar .filter-php.show svg g {
	stroke: #FFF !important;
}
.top-bar .filter-s3.show {
	background: #B8278F;
	border: 1px solid #B8278F;
	color: #FFF;
}
.top-bar .filter-s3.show svg {
	stroke: #FFF !important;
}
.top-bar-filters {
	align-items: center;
	display: flex;
	gap: 16px;
}
.top-bar-search {
	flex: 1;
}

JS (error.js)

"use strict";

window.CRFD.Error = function () {
	// -------------------------------------------------------------
	// INIT
	// -------------------------------------------------------------
	function init() {
		// Init events
		initEvents();
		// Select first log file
		var firstLog = document.querySelector('.log-files .log-file:first-child');
		if (firstLog) {
			firstLog.click();
		}
	}
	function initEvents() {
		// Log file click
		var logFiles = document.querySelectorAll('.log-file');
		logFiles.each(function( logFile ) { 
			// Toggle log-file on/off
			_on(logFile, 'click', function() {
				// Unselect all other log files
				logFilesUnselectAll();
				// Add selected class
				_classAdd(logFile, 'selected');
				// Grab data-key attribute
				var key = _attrGet(this, 'data-key');
				// Load log file rows into table
				logDataPopulateTable(key);
			});
			// Remove button
			var removeButton = logFile.querySelector('.log-file-delete');
			_on(removeButton, 'click', function(e) {
				// Ensure that click doesn't propogate to logFile
				e.stopPropagation();
			});
		});
		// Filter clicks
		var filters = document.querySelectorAll('.filter');
		filters.each(function( filter ) { 
			_on(filter, 'click', function(e) {
				e.preventDefault();
				// Grab data-type attribute
				var type = _attrGet(this, 'data-type');
				// Toggle show class
				_classToggle(filter, 'show');
				// Toggle show class on table rows
				var rows = document.querySelectorAll('table tr');
				rows.each(function( row ) { 
					if (_classHas(row, 'error-' + type)) {
						_classToggle(row, 'show');
					}
				});
			});
		});
	}
	// -------------------------------------------------------------
	// LOG DATA
	// -------------------------------------------------------------
	function logDataPopulateTable(key) {
		var rows = '';
		// Iterate through and add populated table rows
		for (var i = 0; i < errors[key]['lines'].length; i++) {
			errors[key]['lines'][i]['key'] = i;
			rows += _tmpl('errorRow', errors[key]['lines'][i]);
		}
		// Add to table
		_$('table tbody').innerHTML = rows;
		// Add click event to toggle details
		var rows = document.querySelectorAll('table tr.error .details-toggle');
		rows.each(function( row ) { 
			_on(row, 'click', function(e) {
				e.preventDefault();
				// Grab key attribute
				var key = _attrGet(this, 'data-key');
				// Toggle show class on details table row
				_classToggle(_$('table tr.details-' + key), 'show');
			});
		});
	}
	// -------------------------------------------------------------
	// LOG FILE
	// -------------------------------------------------------------
	function logFilesUnselectAll() {
		var logFiles = document.querySelectorAll('.log-file');
		logFiles.each(function( logFile ) {
			_classRemove(logFile, 'selected');
		});
	}
	// -------------------------------------------------------------
	// PUBLIC FACING METHODS
	// -------------------------------------------------------------
	return {
		init: function() {
			if (document.readyState === 'complete') {
				init();
			} else {
				window.addEventListener('load', init, false);
			}
		}
	};
} ();
CRFD.Error.init();

JS (crafd-utility.js)

Here are the utility functions that I use in error.js:

//console.log(_$('#Logo'));
function _$(query) { if (!query) { return console.log('_$ Crafd error'); } var results = document.querySelectorAll(query); if (results.length === 1) { return document.querySelector(query); } else if (results.length === 0) { return false; } else { return results; } }
//console.log(_attrGet(document.getElementById('Logo'), 'id'));
function _attrGet(el, value) { if (!el || !value) { return console.log('_attrGet Crafd error'); } return el.getAttribute(value); }
//_classAdd(document.getElementById('Logo'), 'fun');
function _classAdd(el, cl) { if (!el || !cl) { return console.log('_classAdd Crafd error'); } if (el) { if (el.classList) { el.classList.add(cl); } else { el.className += ' ' + cl; } } }
//console.log(_classHas(document.getElementById('Logo'), 'fun'));
function _classHas(el, className) { if (!el || !className) { return console.log('_classHas Crafd error'); } if (!el.classList) { return false; } return el.classList.contains(className); }
//_classRemove(document.getElementById('Logo'), 'fun');
function _classRemove(el, cl) { if (!el || !cl) { return console.log('_classRemove Crafd error'); } if (el.classList) { return el.classList.remove(cl); } return el.className = el.className.replace(new RegExp('(^|\\b)' + cl.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); }
// Toggle class on/off
function _classToggle(el, cl) { return el.classList.toggle(cl); }
//_on(_$('.head'), 'click', function(e) {});
function _on(el, event, fn) { if (!el || !event || !fn) { console.log(el); return console.log('_on Crafd error'); } return el.addEventListener(event, fn); }
// Simple JavaScript Templating
// John Resig - http://ejohn.org/blog/javascript-micro-templating/ - MIT Licensed
//_tmpl('staging', stagingData);
function _tmpl(str, data) { var tmplCache = []; var fn = !/\W/.test(str) ? tmplCache[str] = tmplCache[str] || _tmpl(document.getElementById(str).innerHTML) : new Function("obj", "var p=[],print=function(){p.push.apply(p,arguments);};" + "with(obj){p.push('" + str.replace(/[\r\t\n]/g, " ").split("<%").join("\t").replace(/((^|%>)[^\t]*)'/g, "$1\r").replace(/\t=(.*?)%>/g, "',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'") + "');}return p.join('');"); return data ? fn( data ) : fn; }
// Error handling
window.onerror = function(message, file, line, column, error) { console.error(error); if (error && error.stack) { message = message + ' - ' + error.stack; } _ajax('POST', '/api/error/', 'message=' + encodeURIComponent( message ) + '&file=' + encodeURIComponent( file ) + '&line=' + encodeURIComponent( line ) + '&url=' + window.location.href, false); };

JS Template (error-row.php)

<script type="text/html" id="errorRow">
	<tr class="error error-<%=type%> show">
		<td>
			<% if (type == 'mysql') { %>
			<svg height="15" viewBox="0 0 14 15" width="14" xmlns="http://www.w3.org/2000/svg"><g style="stroke:#9E9E9E;stroke-width:1.333333;fill:none;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round" transform="translate(1 1)"><ellipse cx="6" cy="2" rx="6" ry="2"/><path d="m12 6.66666667c0 1.10666666-2.66666667 2-6 2s-6-.89333334-6-2"/><path d="m0 2v9.3333333c0 1.1066667 2.66666667 2 6 2s6-.8933333 6-2v-9.3333333"/></g></svg>
			<% } else if (type == 'php') { %>
			<svg height="10" viewBox="0 0 15 10" width="15" xmlns="http://www.w3.org/2000/svg"><g style="stroke:#9E9E9E;stroke-width:1.333333;fill:none;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round" transform="translate(1 1)"><path d="m9.33333333 8 3.99999997-4-3.99999997-4"/><path d="m4 0-4 4 4 4"/></g></svg>
			<% } else if (type == 's3') { %>
			<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>
			<% } else { %>
			<svg height="11" viewBox="0 0 14 11" width="14" xmlns="http://www.w3.org/2000/svg"><text fill="#9E9E9E" fill-rule="evenodd" font-family="SFProDisplay-Semibold, SF Pro Display" font-size="12" font-weight="500" letter-spacing=".0873409128"><tspan x="0" y="10">JS</tspan></text></svg>
			<% } %>
		</td>
		<td><%=time%></td>
		<td><a href="#" class="details-toggle" data-key="<%=key%>"><%=message%></a></td>
		<td><%=file%></td>
		<td><a href="https://github.com/davemart-in/crafd/blob/main/<%=file%>#L<%=line%>" target="_blank"><%=line%></a></td>
		<td><%=username%></td>
		<td><a href="<?php echo BASEURL; ?><%=url%>"><%=url%></a></td>
	</tr>
	<tr class="details details-<%=key%>">
		<td></td>
		<td colspan="5">
			<%=message%>
		</td>
		<td></td>
	</tr>
</script>

PHP (/api/error/)

The following code processed JS errors from window.onerror:

<?php if (!defined('COREPATH')) exit('No direct script access allowed');

// Prep post variables
$type = 'js';
$message = (isset($_POST['message'])) ? $_POST['message'] : '';
$file = (isset($_POST['file'])) ? $_POST['file'] : '';
$line = (isset($_POST['line'])) ? $_POST['line'] : '';
$url = (isset($_POST['url'])) ? $_POST['url'] : '';

// Pass to error handler
return error($type, $message, $file, $line, $url);

/* CLOSE DB & SESSIONS ------------------------------------------- */
closeDbAndSessions();

PHP (Controller)

<?php if (!defined('APPPATH')) exit('No direct script access allowed');

/* ADMIN CHECK ------------------------------------------- */
// Code here to prevent unauthorized access

/* CHECK IF DELETE ------------------------------------------- */
if (isset($glob['route'][2]) && $glob['route'][2] == 'delete') {
	$file = $glob['route'][3] . '.log';
	$path = APPPATH . 'errors/';
	if (file_exists($path.$file)) {
		unlink($path.$file);
	}
	return redirect('admin/errors');
}

/* VARS ------------------------------------------- */
$errors = [];

/* LOAD FILESYSTEM FUNCTION ------------------------------------------- */
require_once(COREPATH.'libraries/Filesystem.php');

/* PREP LOG FILES ------------------------------------------- */
$path = APPPATH.'errors/';
$csv_files = filesystem_directory_map($path);

/* ITERATE OVER LOG FILES ------------------------------------------- */
for ($i = 0; $i < count($csv_files); $i++) {
	// Get file name
	$file_name = $csv_files[$i];
	$log_file = $path . $file_name;
	$errors[$i]['name'] = $file_name;
	// Get file contents
	$file_contents = file_get_contents($log_file);
	// Get file size
	$file_size = filesystem_filesize($log_file);
	$errors[$i]['size'] = $file_size;
	// Loop through each line
	$lines = explode("\n", $file_contents);
	// Loop through each line
	$errors[$i]['lines'] = [];
	foreach ($lines as &$line) {
		// If line is empty, skip it
		if (empty($line)) {
			continue;
		}
		// Repopulate line breaks
		$line = str_replace('~~', "<br>", $line);
		$parts = explode('|', $line);
		// If parts is not 7 parts, skip it
		if (count($parts) != 7) {
			continue;
		}
		$errors[$i]['lines'][] = [
			'type' => $parts[0],
			'time' => $parts[1],
			'message' => $parts[2],
			'file' => $parts[3],
			'line' => $parts[4],
			'username' => $parts[5],
			'url' => $parts[6]
		];
	}
}
// Sort by date
asort($errors);

/* LOAD VIEW ------------------------------------------- */
if (isset($glob['view'])) {
	require_once($glob['view']);
}

/* CLOSE DB & SESSIONS ------------------------------------------- */
closeDbAndSessions();

PHP (Filesystem.php)

<?php if (!defined('COREPATH')) exit('No direct script access allowed');

function filesystem_directory_map($sourceDir, $directoryDepth = 0) {
	try {
		
		if (!is_dir($sourceDir)) {
			return false;
		}

		$fp = opendir($sourceDir);

		$fileData  = [];
		$newDepth  = $directoryDepth - 1;
		$sourceDir = rtrim($sourceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;

		while (false !== ($file = readdir($fp))) {
			// Remove '.', '..'
			if ($file === '.' || $file === '..') {
				continue;
			}

			if (is_dir($sourceDir . $file)) {
				$file .= DIRECTORY_SEPARATOR;
			}

			if (($directoryDepth < 1 || $newDepth > 0) && is_dir($sourceDir . $file)) {
				$fileData[$file] = directory_map($sourceDir . $file, $newDepth);
			} else {
				$fileData[] = $file;
			}
		}

		closedir($fp);

		return $fileData;
	} catch (Throwable $e) {
		return [];
	}
}

function filesystem_filesize($log_file) {
	$size_in_bytes = filesize($log_file);
	$size_in_kb    = $size_in_bytes / 1024;
	$size_in_megabytes = $size_in_bytes / 1024 / 1024;
	// If less than 1kb, return in bytes
	if ($size_in_kb < 1) {
		return $size_in_bytes . ' B';
	}
	// If less than 1mb, return in kb
	if ($size_in_megabytes < 1) {
		return round($size_in_kb, 2) . ' KB';
	}
	// Else return in mb
	return round($size_in_megabytes, 2) . ' MB';
}

PHP (Bootstrap.php)

This code runs in my bootstrap file to kickstart the app on every page.

/* ERROR TRACKING ------------------------------------------- */
require_once(COREPATH.'libraries/Error.php');
// Set default error function for PHP
set_error_handler('php_error_handler');
// Catch fatal PHP errors
register_shutdown_function('php_fatal_handler');

PHP (Error.php)

<?php if (!defined('APPPATH')) exit('No direct script access allowed');

function error($type, $message, $file='', $line='', $url='') {
	$year = date("y");
	$month = date("m");
	$day = date("d");
	$path = APPPATH.'errors/'.$year.'-'.$month.'-'.$day.'.log';
	// Prep file path
	if ($file === null) {
		$file = ''; // Or some other appropriate default value
	}
	$file = str_replace(ROOTPATH, '', $file);
	$file = str_replace(BASEURL, 'public/', $file);
	// Prep URL
	$url = str_replace(BASEURL, '', $url);
	$server_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
	if (isset($server_uri) && substr($server_uri, 0, 1) == '/') {
		$server_uri = substr($server_uri, 1);
	}	
	/* Prep data --------------------------------- */
	$error_data = [
		'type' => $type,
		'time' => date('H:i:s'),
		'message' => $message,
		'file' => $file,
		'line' => $line,
		'username' => (!empty($_SESSION['username'])) ? $_SESSION['username'] : '',
		'url' => (!empty($url)) ? $url : $server_uri
	];
	// Ensure that no values in $error_data contain a |
	$error_data = array_map(function($value) {
		return is_null($value) ? $value : str_replace('|', '', $value);
	}, $error_data);
	// format log message into single string concatenated by |
	$error_log = implode('|', $error_data);
	// Remove all new lines
	$error_log = trim(preg_replace('/\s\s+/', '~~', $error_log));
	// Replace new lines with space
	$error_log = str_replace("\r", '~~', $error_log);
	$error_log = str_replace("\n", '~~', $error_log);
	// Add new line to the end of the log message
	$error_log = $error_log . PHP_EOL;
	/* Log the error --------------------------------- */
	file_write($path, $error_log);
}

function php_error_handler($errno, $errstr, $errfile, $errline) {
	$message = $errno . ' - ' . $errstr;
	return error('php', $message, $errfile, $errline);
}

function php_fatal_handler() {
	$errfile = '';
	$errstr  = 'Fatal error';
	$errno   = E_CORE_ERROR;
	$errline = 0;

	$error = error_get_last();

	if($error !== NULL) {
		$errno   = $error["type"];
		$errfile = $error["file"];
		$errline = $error["line"];
		$errstr  = $error["message"];
		$message = $errno . ' - ' . $errstr;

		error('php', $message, $errfile, $errline);
	}
}

PHP (MySQL Error Handling)

Finally, this code sits in my MySQL library catching any errors that take place:

private function ExceptionLog($message , $sql='') {

	$exception  = 'Unhandled Exception. <br />';
	$exception .= $message;
	$exception .= "<br /> You can find the error back in the log.";

	if(!empty($sql)) {
		# Add the Raw SQL to the Log
		$message .= "\r\nRaw SQL : "  . $sql;
	}
		# Log error to dashboard
		error('mysql', $message);

		# Write into log
		$this->log->write($message);
		
		# Email error
		//mail($this->settings['error_email'], 'Crafd DB Error', $exception, 'designpro@gmail.com');

	return $exception;
}	

And I’ve got a try/catch on connection (Note: I’ve redacted the rest of the function):

private function Connect($db_config) {
	try {
		# Code to attempt here
	}
	catch (PDOException $e) {
		# Write into log
		echo $this->ExceptionLog($e->getMessage());
		die();
	}
}

Lastly, another try/catch on execution:

private function Execute($query,$parameters = "") {
	try {
		# Code to attempt here
	}
	catch (PDOException $e) {
		# Write into log and display Exception
		echo $this->ExceptionLog($e->getMessage(), $query );
		die();
	}
}

2 responses to “Custom PHP Error Log (w/ full code)”

  1. alexmigf Avatar
    alexmigf

    Do you have a repository of this?

    Liked by 1 person

    1. Dave Martin Avatar

      Not open. The project is a private repo.

      Like

Leave a reply to Dave Martin Cancel reply