I/O operations in the filesystem are often very slow, compared with CPU calculations. In an asynchronous PHP application this means that every time we access the filesystem even with a simple
fopen() call, the event loop is being blocked. All other operations cannot be executed while we are reading or writing on the disk. As a rule of thumb:
In an asynchronous PHP application, we cannot use native PHP functions to access the filesystem.
So, what is the solution? ReactPHP ecosystem already has a component that allows you to work asynchronously with a filesystem: reactphp/filesystem. This component provides a promise-based interface for the most commonly used operations within a filesystem.
Before we start working with files and directories we need to make some setup. First of all, like in any other ReactPHP application, we need an event loop. Next, we need to create an instance of the
It is a sort of factory for all other objects that we may need: files and directories. To get an object that represents a file we can use
file($filename) method, which returns a promise that fulfills with the contents of the file:
This method returns an instance of
React\Filesystem\Node\FileInterface, which provides various methods for working with files.
To asynchronously read the contents of the file call
And don’t forget to call
$loop->run() or nothing will happen. Behind the scenes this method opens a file in a reading mode, then starts reading this file and buffering its contents. Once, reading is done it fulfills its promise with this contents. It works like
file_get_contents() but in an asynchronous way and doesn’t block the loop. To prove this we can attach a timer to output a message every second. This timer represents some other performing task while we are reading a file. And then we start reading a huge file (in my case 40MB):
You can see that while we are reading the file the loop is not blocked and the timer works. It approximately takes 8 seconds to read the whole file:
In case you want to work with the underlying stream, that provides the contents, you can use method
open($flags). Consider it as an asynchronous analog for native PHP
fopen() function, it accepts the same flags. This method returns a promise which fulfills with an instance of a stream (readable or writable depending on the mode you specified):
This snippet does the same as the previous one, but instead of buffering we have access to every received chunk of data.
Creating a new file
Before writing the file, we should create one if it doesn’t exist. There are three ways to do it. The first one is to create a file object and then call method
create() on it. It returns a promise which fulfills once the file is being created. The promise rejects if a file with a specified name already exists:
create() under the hood calls method
touch() works as you expect: if there is no file with a specified name it creates this file and if such file exists - it does nothing. In this case, the returned promise fulfills if the file was created or it already exists:
The third approach to create a file is to use
open() method and provide
c (create) flag:
open() opens the file and returns a promise which fulfills with a stream that can be read from or written to. The next snippet opens a file in a writable mode (
w) and creates it (
c) if it doesn’t exist:
In case we have opened file in a readable mode (
r) the promise fulfills with an instance of
To write something to the file you should open it in a writable mode and then just use an opened writable stream and
write() data to it:
We open a file via
open() method and provide to flags:
c to create a file if it doesn’t exist and
w to open this file in a writable mode. Then when a file is opened in the onFulfilled handler we get access to the stream which represents our file. In this handler, we can start writing to this stream.
If you are not familiar with ReactPHP streams and don’t know how they work check this article.
Don’t forget to call
close()on the file, when you are done. Don’t leave opened file descriptors.
Also, there is a helper method
putContents(). Which under the hood does the same what we have already done:
One notice here: it implicitly calls
close() method on the file and closes it.
rename($toFilename) renames current file object to a specified name. Returns a promise that fulfills with an instance of a new file renamed file:
remove() removes current file object. Returns a promise that fulfills once the file is removed:
stat() returns a promise which fulfills with an associative array that contains information about the file. The array structure is the same as native PHP
stat() function returns:
time() returns a promise which fulfills with an associative array that consists of three
DateTime objects. Each object for the change time, access time, and modification time. Actually is a wrapper over the
stat() function and returns only a time part from
exists() returns a promise which fulfills if the current file exists otherwise it rejects:
size() returns a promise which fulfills with the size of the file in bytes:
chown($uid = -1, $gid = -1) changes the owner of the file. This method accepts owner id and optional group id. Returns a promise that fulfills once the owner has been changed:
501is my current uid. To get your uid run
id -uin your terminal.
chmod($mode) changes the mode of the file. Parameter
$mode is the same as native PHP
chmod() function has:
To create a directory object we use the same
FilesystemInterface and method
This code creates a variable
$dir which points to the current directory and is an instance of the
Then, to list all contents of the directory we can use method
ls(), which returns a promise that fulfills with an instance of
SplObjectStorage which represents a map of objects
React\Filesystem\Node\Nodeinterface objects (files and directories):
The snippet above outputs the contents of the directory. Or if you need a promise which fulfills with an array of paths:
ls() iterates only one level deep inside the directory. If you want to get the contents of all child directories recursively use
lsRecursive(). The signature is the same with
ls(): returns a promise which fulfills with an instance of
SplObjectStorage that contains instances of
The snippet above outputs paths of all the inner nodes of the directory. All instances of
React\Filesystem\Node\Nodeinterface implement magic
__toString() method, which returns a path to the current node.
Creating a new directory
create() creates a new directory. It returns a promise which fulfills once the directory is created or rejects if such directory already exists:
You can also create a set of embedded directories with
Actually, all directory-related methods have appropriate recursive pairs: use method name and suffix
To remove an empty directory you can use
remove() method. It returns a promise that fulfills once the directory is removed. The same promise rejects if the directory is not empty:
In case you need to remove the non-empty directory you can use
removeRecursive(), which removes the directory and all its contents.
size() can be useful in case you need to count contents of the directory. It returns a promise that fulfills with an associative array. This array contains the number of child directories, files and their total size in bytes:
size() goes only one level deep inside the directory. In case you need to get counters recursively use
Directory object also has
chown() methods, which behaves exactly as their
Also, all these methods have recursive implementations:
chownRecursive() that does the same job but with all inner files and directories.
To create a symbolic link from a specified path you should make to steps:
- Get access to the current filesystem adapter.
symlink($fromPath, $toPath)method on it.
symlink($fromPath, $toPath) creates a symbolic link for a specified
$fromPath and names this link after the value provided via
$toPath. This method returns a promise which fulfills once the link is created. In the snippet above we create a symbolic link
test_link.txt which points to file
To resolve actual file link points to you can use
readlink($path) of the filesystem adapter:
readlink($path) returns a promise which fulfills with a path the link is pointing at.
To remove the link use method
unlink() on filesystem adapter:
unlink()can also be applied to files, not only symbolic links.
This tutorial has introduced ReactPHP Filesystem Component which allows you to work asynchronously with a filesystem in ReactPHP ecosystem. This component contains classes and interfaces to work with files, directories, and symbolic links. Filesystem I\O is blocking, so when you deal with files in your asynchronous ReactPHP application you SHOULD use reactphp/filesystem.
You can find examples from this article on GitHub.
This article is a part of the ReactPHP Series.