2
\$\begingroup\$

I am designing a basic in-memory cache storage with a thin CRUD (actually CRD) interface. The design is inspired by backend solutions such as Parse and StackMob.

Main characteristics:

  • Cache consists of tables(schemas) storing hashes.

  • The keys(fields) in a table are fixed(enforced).

  • Maximum memory efficiency for storing large data.

Here is the design I came up with, which is tested and fully functional.

There are too many Exceptions for my taste, but somehow I need to enforce things that "should not happen". Another thought was to log errors silently and then ignore them. However, that means my code using this API will have to account for those errors, increasing the complexity.

The basic methods are create, readOne, read, and delete. CRUD usually includes update but I find it useless as it can be accessed by combining 'read', 'delete', 'create' and requires additional decision how to handle concurrency.

I'm curious to get feedback and thankful for any critiques or remarks regarding what can be improved.

/** Local In-Memory Cache: Tables referred by their string names
 * Storing hashes:
 *    all hashes must have 'id' as key
 *    all hashes must have keys other than 'id'
 *    and hashes in the same table must have the same key structure (enforced)
 * Basic CRUD interface + 'readOne' for the first entry + 'realDelete'
 * optimized to save memory: arrays packed into |-separated strings and gz(de/in)flated
 */
class LocStore {
   // ass. array 'table' => string of keys
   private $_keys = array();
   // assoc. array of strings indexed by table and id
   private $_values = array(); 

   /** create entry, enforce the same keys and new id
    * @param string $table
    * @param ass. array $hash, must have 'id' not null
    */
   public function create ($table, array $hash) {
      $id = $hash['id'];
      if (! $id) throw new Exception('Id is null');
      unset($hash['id']);
      if (! $hash) throw new Exception('Empty hash beside id');
      // sort by keys alphabetically 
      ksort($hash);
      $keyString = $this->_toString(array_keys($hash));
      if ( empty($this->_keys[$table]) ) {
         $this->_keys[$table] = $keyString;         
      } elseif ($this->_keys[$table] != $keyString) {
         throw new Exception('Array keys mismatch');
      }
      if ( isset($this->_values[$table]) && ! empty($this->_values[$table][$id]) ) {
         throw new Exception("Id '$id' already exists");
      }
      $this->_values[$table][$id] = $this->_toString(array_values($hash));
      // for chaining
      return $this;
   }

   // read one entry, empty array if nothing is there
   public function readOne ($table) {
      if ( empty($this->_values[$table]) ) return array();
      reset($this->_values[$table]);
      $id = key($this->_values[$table]);
      return $this->read($table, $id);
   }

   // read by id, empty array if not found
   public function read ($table, $id) {
      if ( empty($this->_values[$table]) || empty($this->_values[$table][$id]) ) return array();
      $keys = $this->_toArray($this->_keys[$table]);
      $values = $this->_toArray($this->_values[$table][$id]);
      $result = array_combine($keys, $values);
      $result['id'] = $id;
      return $result;
   }

   // delete by id, exception if not found
   public function delete ($table, $id) {
      if ( empty($this->_values[$table]) || empty($this->_values[$table][$id]) ) {
         throw new Exception('Deleting non-existant entry');
      }
      unset($this->_values[$table][$id]);
      return $this;
   }

   public function readDelete($table, $id) {
      $hash = $this->read($table, $id);
      if ($hash) $this->delete($table, $id);
      return $hash;
   }

   private function _toString (array $array) {
      $json = json_encode($array);
      return gzdeflate($json);
   }

   private function _toArray ($string) {
      $string = gzinflate($string);
      return json_decode($string);
   }

}
\$\endgroup\$
  • \$\begingroup\$ Out of interest why not just use memcached? \$\endgroup\$ – Dave Nov 28 '13 at 16:03
  • \$\begingroup\$ I've considered memcached or other in-memory databases. I actually could not see the reason to use any of them. I understand there is no compression out of the box, API is not suitable for tables, the code is long and hidden inside thousands of other files in Zend. I'd still need to rewrite and adapt, and I can do just that with much shorter code and full control of what it does. \$\endgroup\$ – Dmitri Zaitsev Nov 30 '13 at 14:22
  • \$\begingroup\$ @Dave Is there anything I miss or overlook that way? \$\endgroup\$ – Dmitri Zaitsev Nov 30 '13 at 14:29
  • \$\begingroup\$ The whole point of memcached is to cache sql query results so that instead of having to hit the mysqld every time you simply pull the result set from memcached instead saving disk IO and load on the mysqld doing it in php like this means that your php script memory usage will skyrocket you'll probably find you have to edit your php.ini to allow larger memory usage by script 64mb is default i think also on pages with large datasets then you'll be increasing load time massively due to your gzip part as it has to zip/unzip massive datasets AND page your mysqld. \$\endgroup\$ – Dave Nov 30 '13 at 17:06
  • \$\begingroup\$ not to mention you have to handle persistance of your cache object across page loads which is either storing it in a session or some how committing it to a text file or db anyway. While there's nothing massively wrong with your code (that I can see at least) there's also no benefit to your application by doing what you're doing only detractors \$\endgroup\$ – Dave Nov 30 '13 at 17:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.