Ever wanted to change where your PHP sends the output to a file instead of the browser?
I have, and it involved changing every echo and other printing statemenet to fwrite. But
there is a better solution to that…
Tell me more
You probably have heard, or even used, PHP's output buffering functions. When you normally do something like echo 'hello'; in PHP, it will be immediately sent to the browser. Sometimes you may want to send headers, perhaps a redirection, to the user after the script has sent something else to the browser. This will normally cause an error, because the HTTP protocol says that you must send headers first and content after them. Output buffering functions can be used to buffer any data you output, and including other things, will let you send headers after other data has already been sent.
ob_start() and ob_end_flush() might already be familiar to you. In addition to their typical uses, the output buffering functions can be used to do various neat tricks, such as writing all output to a file.
The function ob_start() takes an optional parameter for a callback function. The callback function receives all output from PHP, so this is what we can use for our purprose.
Onwards to the code
Now that we have the basics covered, let's look at the code a bit.
Let's first look at a few functions to see how this will work, and then we will refactor it into a nice class. Let's start!
We need to open a file where the output will be written to
$ob_file = fopen('test.txt','w');
Very basic stuff. Next, let's create the callback function which will perform the actual writing to the file:
function ob_file_callback($buffer)
{
global $ob_file;
fwrite($ob_file,$buffer);
}
Look! There's a global, aren't they evil?
Don't worry about it, when we get to the next chapter, we will get rid of it in the class we will create.
Now we have all that we need together:
$ob_file = fopen('test.txt','w');
ob_start('ob_file_callback');
//Anything we output now will go to test.txt
echo 'Hi everyone!';
echo 'I bet you can\'t see this in your browser';
ob_end_flush();
Provided your script can write to test.txt, the above code will write the echo's to the file and not to the browser. Simple and easy.
This code we just made isn't very easy to re-use though: We used a global variable and we have to manually open the file and things like that. To make our code a little nicer, let's make it into a class!
Classified
Making this into a nice, easy to use class is pretty simple. I don't want to have to manually open a file, so I want the class to be able to open the target file for writing. It also obviously needs to handle the callback and such on its own so after I have written the class, I won't have to bother with the details anymore.
Let's just call the class OB_FileWriter. Note that I'm using PHP5 syntax, so if you're running PHP4, you need to change all variable declarations to var $variablename, change __construct into OB_FileWriter, remove __destruct and remove “public” from all function declarations.
class OB_FileWriter
{
private $_filename;
private $_fp = null;
public function __construct($filename)
{
$this->setFilename($filename);
}
public function __destruct()
{
if($this->_fp)
$this->end()
}
public function setFilename($filename)
{
$this->_filename = $filename;
}
public function getFilename()
{
return $this->_filename;
}
public function start()
{
}
public function end()
{
}
public function outputHandler($buffer)
{
}
}
So there's the basic skeleton for the class. Now let's add the code to the empty functions.
public function start()
{
$this->_fp = @fopen($this->_filename,'w');
if(!$this->_fp)
throw new Exception('Cannot open '.$this->_filename.' for writing!');
ob_start(array($this,'outputHandler'));
}
This function will be used for starting output buffering. It opens the file, checks if it was succesfull and starts buffering. Note that we used the @ symbol in front of the fopen command to suppress PHP error messages and instead just check if the $_fp variable was set and throw an exception if it failed.
ob_start takes the callback as the first parameter, but why is it an array? In PHP if you want to pass class' functions as callbacks, you need to pass an array containing the owner class and the class function name.
Next, the end function..
public function end()
{
@ob_end_flush();
if($this->_fp)
fclose($this->_fp);
$this->_fp = null;
}
First calling the ob_end_flush to end the buffering and flush the data to the file. Then if the file is still open as it should be, close it and set the variable to null to signify that the file is no longer open.
Finally, the outputHandler function
public function outputHandler($buffer)
{
fwrite($this->_fp,$buffer);
}
Simplest of them all! Just dump the contents of the buffer to the file.
Now we have a class for sending output to a file. Compare using this class to the approach in the earlier example:
$obfw = new OB_FileWriter('test.txt');
$obfw->start();
echo 'Hi everyone!';
echo 'I bet you can\'t see this in your browser';
$obfw->end();
Nice and easy!
Final improvements
The class can be used now, but there's one small thing to consider:
If we start the buffering and the code we run after that gives an error or throws an uncaught exception, what happens?
The message from the error will go straight to the file, which isn't necessarily what you want. At least I like to see my errors when they happen so that I can do something about it, so let's make it so that our class can optionally try to detect errors and exceptions and halt the buffering so that we can see them.
We can do this by registering our own error and exception handlers inside our class which will then stop the buffering and display the errors.
The class needs some new functions and a new variable for this, so let's add the variable first…
private $_errorHandlersRegistered = false;
and the functions…
private function _stopBuffering()
{
}
public function setHaltOnError($value)
{
}
public function exceptionHandler($exception)
{
}
public function errorHandler($errno, $errstr, $errfile, $errline)
{
}
The function names should be pretty self explanatory, so let's continue with adding the code in them:
public function setHaltOnError($value)
{
//Do nothing if the value is the same as old
if($value === $this->_errorHandlersRegistered)
return;
if($value === true)
{
set_exception_handler(array($this, 'exceptionHandler'));
set_error_handler(array($this, 'errorHandler'));
$this->_errorHandlersRegistered = true;
}
else
{
restore_exception_handler();
restore_error_handler();
$this->_errorHandlersRegistered = false;
}
}
So depending on the value you pass to this function, it will either register the class' error and exception handlers or restore the old handlers for them.
Now, let's add the code for the handlers:
public function exceptionHandler($exception)
{
$this->_stopBuffering();
echo 'Fatal error: Uncaught ', $exception;
}
Quite simple. Stop buffering and print a message with the exception details.
public function errorHandler($errno, $errstr, $errfile, $errline)
{
$this->_stopBuffering();
$errorNumber = E_USER_ERROR;
switch($errno)
{
case E_NOTICE:
$errorNumber = E_USER_NOTICE;
break;
case E_WARNING:
$errorNumber = E_USER_WARNING;
break;
}
trigger_error($errstr.', File: '.$errfile.' line '.$errline, $errorNumber);
}
We can re-trigger errors and they will be handled by the default error handler, so that's what we do here.
So what about _stopBuffering()? Let's move the code which stops the buffering from end() to this function and modify end a bit too…
public function end()
{
$this->_stopBuffering();
$this->setHaltOnError(false);
}
private function _stopBuffering()
{
@ob_end_flush();
if($this->_fp)
fclose($fp);
$this->_fp = null;
}
We do this because we want to be able to stop the buffering without removing the error handlers. Also, when we end(), we want to return back to using the normal error handlers so we must add that to end()
In closing
So redirecting PHP output to a file is pretty simple to do. Just some things to remember, such as that error/exception thing.
You might wonder why not just simply use ob_start(), ob_get_clean() and file_put_contents… With the class I described, everything outputted will be sent to the file when it's being written and if something else fails afterwards, the stuff which was sent earlier is still in the file. Writing the contents of the buffer to the file in the end of the script would mean that nothing is written if there's an error, so which way is better just depends on what your goal is.
Complete source code for OB_FileWriter