How to add or update values in a file using PHP

1k Views Asked by At

I am working on a small class that will allow me to write queue data to a file. Similar idea to PHP $_SESSION.

I thought the following steps will do the trick

  1. Open a file using fopen() in 'a+' mode
  2. Lock the file using flock() with LOCK_EX type to prevent another process from using the same file
  3. Read the file's existing content using fread(). Then put the data into array using json_decode(array, true)
  4. Now the data is in array. If the key exists in the array update its value, otherwise insert the key to the array.
  5. After create an array with all the data that need to go on the file, I truncate the file using ftruncate() and write the new data to the file using fwrite()
  6. Unlock the file using flock() with LOCK_UN type to allow another process to use it.
  7. Finally Close the file

I believe I wrote the code to satisfy the above step in my updateCache() method. But it does not seems to work properly. It does not keep track of the data.

Here is my class

<?php

namespace ICWS;

use \ICWS\showVar;
use \DirectoryIterator;
/**
 * CacheHandler
 *
 * @package ICWS
 */
class CacheHandler {

    /* Max time second allowed to keep a session File */
    private $maxTimeAllowed = 300;


    private $filename = '';
    private $handle;

    public function __construct( $filename )
    {

        $this->filename = $filename;

        // 5% chance to collect garbage when the class is initialized.
        if(mt_rand(1, 100) <= 5){
            $this->collectGarbage();
        }
    }

    private function print_me($a)
    {
        echo '<pre>';
        print_r($a);
        echo '</pre>';
    }

    /**
    * Add/Update the cached array in the session
    *
    * @param string $key
    * @param bigint $id
    * @param array $field
    * @return void
    */  
    public function updateCache($key, $id, array $field)
    {

        $currentVal = $field;
        $this->openFile();

        $this->lockFile();
        $storage = $this->readFile();

        //create a new if the $id does not exists in the cache
        if( isset($storage[$key]) && array_key_exists($id, $storage[$key]) ){
            $currentVal = $storage[$key][$id];

            foreach($field as $k => $v){
                $currentVal[$k] = $v; //update existing key or add a new one
            }

        }

        $storage[$key][$id] =  $currentVal; 

        $this->updateFile($storage);

        $this->unlockFile();

        $this->closeFile();

    }

    /**
    * gets the current cache/session for a giving $key
    *
    * @param string $key. If null is passed it will return everything in the cache
    * @return object or boolean
    */  
    public function getCache($key = null)
    {
        $value = false;
        $this->openFile();
        rewind($this->handle);
        $storage = $this->readFile();

        if(!is_null($key) && isset($storage[$key])){
            $value = $storage[$key];
        }


        if(is_null($key)){
            $value = $storage;
        }

        $this->closeFile();

        return $value;
    }

    /**
    * removes the $id from the cache/session
    *
    * @param string $key
    * @param bigint $id
    * @return boolean
    */      
    public function removeCache($key, $id)
    {

        $this->openFile();

        $this->lockFile();
        $storage = $this->readFile();

        if( !isset($storage[$key][$id])){
            $this->unlockFile();
            $this->closeFile();
            return false;
        }

        unset($storage[$key][$id]);
        $this->updateFile($storage);
        $this->unlockFile();
        $this->closeFile();
        return true;
    }

    /**
    * unset a session
    *
    * @param argument $keys
    * @return void
    */  
    public function truncateCache()
    {
        if(file_exists($this->filename)){
            unlink($this->filename);
        }
    }

    /**
    * Open a file in a giving mode
    *
    * @params string $mode 
    * @return void
    */
    private function openFile( $mode = "a+")
    {
        $this->handle = fopen($this->filename, $mode);

        if(!$this->handle){
            throw new exception('The File could not be opened!');
        }
    }

    /**
    * Close the file
    * @return void
    */
    private function closeFile()
    {
        fclose($this->handle);
        $this->handle = null;
    }

    /**
    * Update the file with array data
    *
    * @param array $data the array in that has the data
    * @return void
    */
    private function updateFile(array $data = array() )
    {
        $raw = json_encode($data);
        $this->truncateFile();
        fwrite($this->handle, $raw);
    }

    /**
    * Delete the file content;
    *
    * @return void
    */
    private function truncateFile( )
    {
        ftruncate($this->handle, 0);
        rewind($this->handle);
    }


    /**
    * Read the data from the opned file
    *
    * @params string $mode 
    * @return array of the data found in the file
    */
    private function readFile()
    {
        $length = filesize($this->filename);
        if($length > 0){
            rewind($this->handle);
            $raw = fread($this->handle, $length);
            return json_decode($raw, true);
        }

        return array();
    }


    /**
    * Lock the file
    *
    * @return void
    */  
    private function lockFile( $maxAttempts = 100, $lockType = LOCK_EX)
    {

        $i = 0;
        while($i <= $maxAttempts){

            // acquire an exclusive lock
            if( flock($this->handle, LOCK_EX) ){
                break;
            }

            ++$i;
        }
    }


    /**
    * Unlock the file
    *
    * @return void
    */  
    private function unlockFile()
    {

        fflush($this->handle);
        flock($this->handle, LOCK_UN);
    }

    /**
    * Remove add cache files
    *
    * @return void
    */  
    private function collectGarbage()
    {
        $mydir = dirname($this->filename);

        $dir = new DirectoryIterator( $mydir );
        $time = strtotime("now");
        foreach ($dir as $file) {

            if (  !$file->isDot()
                && $file->isFile()
                && ($time - $file->getATime()) > $this->maxTimeAllowed
                && $file->getFilename() != 'index.html'
            ) {
                unlink($file->getPathName());

            }

        }
    }

    function addSlashes($str)
    {
        return addslashes($str);
    }


}

This is how I use it

<?php
    require 'autoloader.php';
    try {
        $cache = new \ICWS\CacheHandler('cache/879');

        $field = array('name' => 'Mike A', 'Address' => '123 S Main', 'phone' => '2152456245', 'ext' => 123);
        $cache->updateCache('NewKey', 'first', $field);

        $cache->updateCache('NewKey', 'second', $field);

        $field = array('Address' => '987 S Main', 'phone' => '000000000000', 'ext' => 5555);
        $cache->updateCache('NewKey', 'first', $field);

    $field = array('locations' => array('S' => 'South', 'N' => 'North', 'E' => 'East'));
    $cache->updateCache('NewKey', 'first', $field);

        echo '<pre>';
        print_r($cache->getCache());
        echo '</pre>';  


    } catch (exception $e){

        echo $e->getMessage();
    }

?>

I am expecting the array to look like this

Array
(
    [first] => stdClass Object
        (
            [name] => Mike A
            [Address] => 987 S Main
            [phone] => 000000000000
            [ext] => 5555
            [locations] => Array
                (
                    [S] => South
                    [N] => North
                    [E] => East
                )

        )

    [second] => stdClass Object
        (
            [name] => Mike A
            [Address] => 123 S Main
            [phone] => 2152456245
            [ext] => 123
        )

)

But after executing the first time, I get blank array. Then after I execute the script again I get the following array.

Array
(
    [NewKey] => Array
        (
            [first] => Array
                (
                    [locations] => Array
                        (
                            [S] => South
                            [N] => North
                            [E] => East
                        )

                )

        )

)

What could be causing the data not to update correctly?

I wrote my updateCache() using $_SESSION which gave me the correct output but I need to do this without sessions

Here is my sessions bases method

public function updateCache($key, $id, array $field)
{

    $currentVal = (object) $field;

    //create a new if the $id does not exists in the cache
    if( isset($_SESSION[$key]) && array_key_exists($id, $_SESSION[$key]) ){

        $currentVal = (object) $_SESSION[$key][$id];

        foreach($field as $k => $v){
            $currentVal->$k = $v; //update existing key or add a new one
        }

    }

    $_SESSION[$key][$id] =  $currentVal;    
}
1

There are 1 best solutions below

0
On BEST ANSWER

The problem lies here:

private function readFile()
{
    $length = filesize($this->filename);
    if($length > 0){
        rewind($this->handle);
        $raw = fread($this->handle, $length);
        return json_decode($raw, true);
    }

    return array();
}

The documentation of filesize() states:

Note: The results of this function are cached. See clearstatcache() for more details.

Because the file is empty when you start, it would have cached that information and your branch is skipped entirely until the next script execution. This should fix it:

private function readFile()
{
    rewind($this->handle);
    $raw = stream_get_contents($this->handle);
    return json_decode($raw, true) ?: [];
}