Making a FileHandler more OOP-friendly using SOLID

Posted on

Problem

I have a class created for the sole purpose is to help handle files. The class holds a path (folder) that contains a single file at a time. Some extra properties to this class are: age of a file and max file size. I’m a very procedural style coder, and I’m trying to shift my work to a more OOP and eventually end up with something more SOLID.

The FileHandler receive a path and a file name. A majority of the functions described are basically a wrapper for system file manipulation. Now before you wonder why wrap things when you easily call them: I want to OOP this and on a bigger scale files are being manipulated for processing from another object. Another reason why I did this, up until recently file_get_content() was nice so far with files under 2MB but as it is growing now using file_get_content is no longer ideal, so on a larger application I will be changing commands.

Small goal:

A worker object process files by writing into files. If we set the max file to be MB, it needs to check for this, and create a new file, switch to it and continue writing into this. It should also check if the file it tries to write into is not idle for more than MAX_EXPIRE.

Another (independent) worker process files. It takes a single file, reads it, and moves it.

One of the more important concept that I got stuck on is that now I have this class that works but I need to refactor it into a Code with Intent (so that it follows SOLID). Is it already there? Can we do more? Should I break it into more classes to serve my purpose?

class FileHandler 
{
    protected $path;
    protected $file_name;
    protected $max_file_size;
    protected $max_age;
    protected $creation_time = false;


    public function __construct($file_path = "./", $file_name = '', $size = 5242880, $max_age = 900)
    {
        $this->path = $file_path;
        $this->file_name = $file_name;
        $this->max_file_size = $size;
        $this->max_age = $max_age;
    }

    public function fileExists()
    {
        return @file_exists($this->path.'/'.$this->file_name);
    }

    public function directoryExists()
    {
        return @file_exists($this->path);
    }

    public function read()
    {
        if ($this->isFile($this->path."/".$this->file_name)) {
            return @file_get_contents($this->path."/".$this->file_name);
        }

        throw new FileNotFoundException("File does not exist at path {$this->path}");
    }

    public function write(DataRepository $data)
    {
        if (!file_put_contents($this->path."/".$this->file_name, $data->getData(), FILE_APPEND)) {
            throw new FileCannotWriteException("Unable to write in the log file : ".$this->file_name);
        }

        return true;
    }

    public function create()
    {
        if (!$this->directoryExists()) {
            $this->makeDirectory($this->path);
        }
        $this->creation_time = time();
        return file_put_contents($this->path."/".$this->file_name, '');

    }

    public function dataSize()
    {
        return @filesize($this->path."/".$this->file_name);
    }


    public function delete()
    {
        return @unlink($this->path."/".$this->file_name);
    }


    public function changeFile($file_name)
    {
        $this->file_name = $file_name;
    }


    public function changeDirectory($path)
    {
        $this->path = $path;
        if (!$this->isWritable()) {
            $this->makeDirectory($this->path);
        }
    }


    public function writeLimit()
    {
        clearstatcache();

        return ($this->dataSize()<$this->max_file_size) ? true : false;
    }


    public function isWritable()
    {
        return is_writable($this->path."/".$this->file_name);
    }


    private static function _glob($pattern, $flags = 0)
    {
        return glob($pattern, $flags);
    }



    public static function files($directory)
    {
        $glob = self::_glob($directory.'/*');

        if ($glob === false) {
            return array();
        }

        return array_filter($glob, function ($file) {
            return filetype($file) == 'file';
        });
    }


    public function makeDirectory($path, $mode = 0777, $recursive = false, $force = false)
    {
        if ($force) {
            return @mkdir($path, $mode, $recursive);
        } else {
            return mkdir($path, $mode, $recursive);
        }
    }


    public function moveFile($target)
    {
        $file_name = $this->file_name;
        if (@rename($this->path."/".$file_name, $target."/".$file_name)) {
            $this->file_name = "";
            return true;
        }

        return false;
    }

    public function isFile($file)
    {
        return is_file($file);
    }

    public function getPath()
    {
        return $this->path;
    }

    public function lockFile()
    {
        if ($lock = @fopen($this->path."/".$this->file_name, "r+")) {
            if (@flock($lock, LOCK_EX|LOCK_NB)) {
                return $lock;
            }
        }

        return false;
    }

    public function unlockFile($lock)
    {
        if ($lock !== null) {
            if (flock($lock, LOCK_UN)) {
                $lock = null;
                return true;
            }
        }
        return false;
    }

    public function getLastModification()
    {
        return filemtime($this->path."/".$this->file_name);
    }

    public function getRelativeFilename()
    {
        return $this->file_name;
    }

    public function getAbsoluteFilename()
    {
        return $this->path."/".$this->file_name;
    }

    public function isMaxAgeReached()
    {
        return $this->creation_time !== false && time() - $this->creation_time >= $this->max_age;
    }

    public static function generateFileFolderByDate($file)
    {
        $timestamp = false;
        $name_type_1 = '#filelog_d+_(d+).log#';
        $name_type_2 = '#(d+)_filelog_d+.log#';
        if (preg_match($name_type_1, $file, $match) || preg_match($name_type_2, $file, $match)) {
            $timestamp = $match[1];
        }
        if ($timestamp === false) {
            return '';
        }

        return date('Ymd', $timestamp);
    }
}

Solution

The class in on itself seems fine to me, but the question you should be asking is what problem am I solving here?.

OOP design works well on the big picture. Let’s say you have a simple application where you want to CRUD a user’s contacts. And let’s say that you decided for some reason not to use a database but to use plain ‘ol files.

In that case, it makes sense for your application to work with User and Contact objects, where each User object contains a bunch of Contact objects. Have you noticed what the application doesn’t know? That’s right, the application has no idea where the data came from, and that’s part of SOLID.

In that sense, you would have UserMapper and ContactMapper to connect the User and Contact respectively to the files. Note that even the User or Contact objects don’t know where the data they hold came from. Example:

class User {
    public $id;
    public $name;
    public $contacts;
}

class UserMapper implements Mapper {
    public function fetch($id, User $user) {
        //Talk to files to get the information needed for the user, and map that information to the $user object.
        $user->name = $someQueryResult->getName();
        //etc...
    }
    public function save(User $user) {
        //Save user details back to files
    }
}

Note that this way the User has no knowledge of where the data in it came from, could have been files, could have been a session or even a REST api.


So you see, it’s nice to have a FileHandler helper to do some tasks, but it all comes down to how things work together in your general application sense.

Leave a Reply

Your email address will not be published. Required fields are marked *