/**
* Add caching to any class
*
* By including CachableTraitDoc when defining classes,
* you can use PhpDoc tag to enable method-level caching.
*/
trait CachableTraitDoc 
{
   protected $cacheTtlDefault = 5;
   protected $methodTtl = array();

   /**
    * Initialize caching code
    *
    * This method parses the class's PhpDoc block for a 
    * default caching time for any methods to be cached and 
    * stores it in the $cacheTtlDefault property.
    *
    * @return void
    */
   public function init() 
   {
      // instantiate a reflection class ...
      $reflectionClass = new ReflectionClass(get_class());
         
      // then get the class's docBlock ...
      $docBlock = $reflectionClass->getDocComment();

      // ... and use it to instantiate a docBlock parser
      $phpDoc =
         new \phpDocumentor\Reflection\DocBlock($docBlock);

      // get all 'cache-ttl-default' tags at the class level
      $cacheTtlTagDefaults =
         $phpDoc->getTagsByName('cache-ttl-default');

      // make sure we found at least one tag
      if (isset($cacheTtlTagDefaults[0])) {
         // get the tags value
         $cacheTtlDefault =
            $cacheTtlTagDefaults[0]->getContent();

         // make sure the value is a non-negative number
         if (is_numeric($cacheTtlDefault)
            && $cacheTtlDefault >= 0) {
            // accept it as default
            $this->cacheTtlDefault = $cacheTtlDefault;
         }
      }
   }

   /**
    * Default method handler that implements caching
    *
    * Default handler for all method that parses the called
    * method's PhpDoc block for the presence of the @cached
    * and @cache-ttl custom tags. It caches method calls if
    * the tags are found and simply passes the method's
    * return value through otherwise.
    *
    * @param String $name method name
    * @param Array $arguments method arguments
    * @return mixed
    */
   public function __call($name, $arguments) 
   {
      // method exists
      if (method_exists($this, $name)) {

         // should the method be cached
         // if $ttl is false or zero we don't cache
         if ($ttl = $this->getMethodCacheInfo($name)) {

            // key for cache is method name append
            // by hash of arguments
            $key = $name
                 . hash('sha256', implode('', $arguments));
 
            // does the cache have the value for the key?
            if (Cache::has($key)) {
               // get the data from the cache
               return Cache::get($key);
            }
            
            // value not in cache
            // get the value to store by executing the
            // function
            $value = call_user_func_array(
               array($this, $name),
               $arguments
            );

            // store key-value in cache
            Cache::add($key, $value, $ttl);

            // return value
            return $value;            

         // method should not be cached
         } else {
            return call_user_func_array(
               array($this, $name),
               $arguments
            );
         }

      // method does NOT exist -> throw exception
      } else {
          throw new Exception("Method not found: $name");
      }
   }

    /**
    * Method PhpDoc block parser for custom caching
    * annotations
    *
    * Parses a given method's PhpDoc block and looks for the
    * custom caching tags, @cached and @cache-ttl. If 
    * neither is found, FALSE is returned. If only @cached 
    * is found, the default ttl is return. If the cache-ttl 
    * is found as well, its value is returned.
    *
    * @param String $methodName method name
    * @return mixed
    */
   public function getMethodCacheInfo($methodName) {
      // no need to parse phpDoc more than once
      if (array_key_exists($methodName, $this->methodTtl)) {
         return $this->methodTtl[$methodName];
      }

      // instantiate a reflection class ...
      $reflectionClass = new ReflectionClass(get_class());
         
      // ... and use it to get the method object we want
      $reflectionMethod =
         $reflectionClass->getMethod($methodName);

      // then get the method's docBlock ...
      $docBlock = $reflectionMethod->getDocComment();

      // ... and use it to instantiate a docBlock parser
      $phpDoc =
         new \phpDocumentor\Reflection\DocBlock($docBlock);

      // method was not annotated to be cached
      if (!$phpDoc->hasTag('cached')) {
         return false;
      } 

      $cacheTtlTags = $phpDoc->getTagsByName('cache-ttl');

      // make sure we found at least one tag
      if (isset($cacheTtlTags[0])) {

         // get the tags value
         $cacheTtl = $cacheTtlTags[0]->getContent();

         // make sure the value is a non-negative number
         if (is_numeric($cacheTtl) && $cacheTtl >= 0) {

             // store ttl for the method so we don't 
             // have to parse phpDoc again later in
             // this same request
             $this->methodTtl[$methodName] = $cacheTtl;

             // return method's cache ttl
             return $cacheTtl;
            
         // return default cache ttl
         } else {
            return $this->cacheTtlDefault;
         }

      // return default cache ttl
      } else {
          return $this->cacheTtlDefault;
      }
   }
}