Finally, file streams, and deferred execution in PHP.
Cleaning up after yourself can be a tedious task. For example, closing file handlers after using them needs to be done. A programmer's life isn't all about the happy path. When things go pear-shaped you might end up duplicating cleanup code throughout your code. This is horrible, let's explore an alternative.
A typical scenario which requires cleanup is the use of file resources. When operating on files in high-traffic environments, or when dealing with large files, you'll want to steer clear of loading an entire file into memory. Functions like file_put_contents
and file_get_contents
are great, but not suited for every occasion. PHP provides an alternate way of dealing with files, using resources.
The fopen
function allows you to obtain a file pointer resource. The returned value is more commonly referred to as "file handle" or "stream". A file resource can be seen as a reference through which you can interact with the given file. As the name suggests, an opened file handle needs to be closed. For this the fclose
function is used. Typical file operations look like this:
$handle = fopen($location, 'r+');
// Do important stuff
fclose($handle);
// return a result
If we were to convert that into a step by step plan it would look something like this:
- Obtain a handle.
- Operate on the handle.
- Close the handle.
- Return a result.
However, development isn't only about the happy-path. What happens when things go wrong? Or things get more complicated? Well, then cleanup can become messy. Let's look at one of those cases.
We'll want to read and convert the first line of a file to uppercase after which we return it:
$handle = fopen($location, 'r+');
if ($handle === false) {
return false;
}
$firstLine = fgets($handle, 1024);
if ( ! $firstLine) {
fclose($handle);
return false;
}
fclose($handle);
return $firstLine;
Here we already have two places where cleanup needs to be happen. This bugs the living hell out of me, and I want it to be nicer. Other languages, like Go, have special language features to deal with this. Whenever cleanup needs to be done regardless of the outcome of a particular piece of code, the defer
keyword can help us out. A typical defer
statement in Go looks like this:
file, _ := os.Open("file.go") // For read access.
defer file.Close()
// Continue operating on the file.
A defer in Go places the executing of the statement at the end of the call-stack for a given function. In order to illustrate what's going on, let's look at a couple of examples.
Examples
As an example exercise we want to:
- Read the first line of a file.
- Convert the line to uppercase.
- Replace the line in the file.
- And return the uppercased line.
In PHP, using streams, this would look something like this:
function flupper($location) {
$handle = fopen($location, 'r+');
$firstLine = fgets($handle, 1024);
if ($firstLine === false) {
fclose($handle);
return false;
}
rewind($handle);
$uppedFirstLine = strtoupper($firstLine);
if (false === fwrite($handle, $uppedFirstLine)) {
fclose($handle);
return false;
}
fclose($handle);
return $uppedFirstLine;
}
Notice how we have all those fclose
calls duplicated all over that function. This example in Go would look something like this:
func flupper () (string, error) {
file, _ := os.OpenFile("file.txt", os.O_RDWR, 0444)
defer file.Close()
r := bufio.NewReader(file)
line, _, err := r.ReadLine()
if err != nil {
return "", fmt.Errorf("Could not read first line.")
}
file.Seek(0, os.SEEK_SET)
uline := strings.ToUpper(string(line))
_, err = file.Write([]byte(uline))
if err != nil {
return "", fmt.Errorf("Could not write to the file")
}
return uline, nil
}
Notice that there's only one file.Close
call, preceded by a defer
statement. During runtime, when this function has executed all its instructions, it'll execute all the deferred statements. This means regardless of what else
happens, or when we return
,the file handle will always be closed. Unfortunately we don't have anything like that in PHP. But we do have a way to deal with things we always want to happen, by using the finally
block.
Finally in PHP
In PHP we can handle exceptions using try/catch
blocks. In PHP 5.5 the finally
block was added to this group. This group is executed regardless of which catch block handles a given exception.
try {
do_something();
} catch (DatabaseException $e) {
// only executed with a database error
} catch (DomainException $e) {
// only executed with a domain error
} finally {
// always executed.
}
It doesn't matter how do_something
fails, the finally
block will always be executed. The block is also executed when the operation is successful, and no exception is thrown. Apart from that, a try is perfectly valid without a catch
block. A try/finally
combination, without catch any exceptions, is perfectly valid. Even when return
'ing in the try block, the finally part is executed. This gives an interesting opportunity for handling cleanups elegantly.
Let's see how we can convert our flupper
function to PHP using this technique.
BEFORE:
function flupper($location) {
$handle = fopen($location, 'r+');
$firstLine = fgets($handle, 1024);
if ($firstLine === false) {
fclose($firstLine);
return false;
}
rewind($handle);
$uppedFirstLine = strtoupper($firstLine);
if (false === fwrite($handle, $uppedFirstLine)) {
fclose($firstLine);
return false;
}
fclose($firstLine);
return $uppedFirstLine;
}
AFTER:
function flupper($location) {
try {
$handle = fopen($location, 'r+');
$firstLine = fgets($handle, 1024);
if ($firstLine === false) {
return false;
}
rewind($handle);
$uppedFirstLine = strtoupper($firstLine);
if (false === fwrite($handle, $uppedFirstLine)) {
return false;
}
return $uppedFirstLine;
} finally {
fclose($handle);
}
}
As you can see, in the try block there's no need to do any cleanup. No matter which return statement is used, the resource will always be closed. Aside from the extra level of indentation (which I've grown to hate thanks to @rdohms), the code in the try
block is super lean.
This technique also works great with external services in case of an exception. It's possibly even a nicer improvement. Instead of:
public function doSomething($payload) {
try {
$response = $this->service->handle($payload);
} catch (\Exception $e) {
cleanup($payload);
throw $e;
}
cleanup($payload);
return $response;
}
You could have been writing:
public function doSomething($payload) {
try {
return $this->service->handle($payload);
} finally {
cleanup($payload);
}
}
These two functions have exactly the same behavior. They allow exceptions to pass through, but take care of their own cleanup, preventing possible memory leaks or leave behind open handles.
In cases where cleanup is important, perhaps give this approach a try and see if it cleans up your code.
Hope you enjoy using this technique!