<?php

/**
 * レンダリングされたページを指定した期間データベース上にキャッシュする機構を提供します。
 * 更新タイミングはInnoDBの行ロックに依存します。すべてのメソッドをトランザクションの内部で呼び出しください。
 */
class DocumentCache {
  private static $instance;

  var $name;
  var $lifetime;
  private $_content = null;
  private $_updated = false;
  private $_expire = 0;

  function __construct($name, $lifetime = 0) {
    $this->name = $name;
    $this->lifetime = $lifetime;
  }

  function __get($name) {
    switch ($name) {
      case 'content':
      case 'updated':
      case 'expire':
        $realname = '_'.$name;
        return $this->$realname;
    }
    return null;
  }

  function __set($name, $value) {
    switch ($name) {
      case 'content':
        if ($this->_content != $value) {
          $this->_updated = true;
          $this->_content = $value;
        }
        break;
    }
  }

  /**
   * 指定されたキーのキャッシュとの同期処理を開始します。
   * このメソッドは指定したキーに対応した有効なキャッシュが存在しないとき、同じキーの行をロックします。
   */
  function BeginSync() {
    $params = array($this->name);
    DB::Transaction();
    DB::Prepare('SELECT content, expire FROM document_cache WHERE name = ?', $params);
    $row = DB::FetchAssoc(true);
    if (isset($row)) {
      extract($row, EXTR_PREFIX_ALL, 'cache');
    }
    else {
      $cache_expire = 0;
    }
    if ($cache_expire < time()) {
      //更新回数の抑制のため、有効期間が更新されたかどうかをロックして判定する。
      DB::Prepare('SELECT expire FROM document_cache WHERE name = ? FOR UPDATE', $params);
      $current_expire = DB::FetchResult();
      if (($current_expire === false) || ($current_expire == $cache_expire)) {
        //expireの値が変化している場合、既に誰かが更新を実施している。
        $cache_content = null;
      }
    }
    $this->_content = $cache_content;
    $this->_expire = $cache_expire;
    $this->_updated = false;
  }

  /**
   * 指定されたキーのキャッシュとの同期処理を完了します。
   */
  function EndSync() {
    if ($this->updated) {
      $query = <<<SQL
INSERT INTO document_cache (name, content, expire) VALUES (?, ?, ?)
  ON DUPLICATE KEY UPDATE content = ?, expire = ?
SQL;
      $expire = time() + $this->lifetime;
      if (ServerConfig::DEBUG_MODE) {
        echo 'Updated : ',date('Y-m-d H:i:s'),'<br>';
        echo 'Next Update : ',date('Y-m-d H:i:s', $expire);
      }
      DB::Prepare($query, array($this->name, $this->content, $expire, $this->content, $expire));
      DB::Execute();
    }
  }

  static function Load($name, $lifetime, $autoexit = false) {
    $self = new DocumentCache($name, $lifetime);
    $self->BeginSync();
    $content = $self->content;
    if (!empty($content)) {
      echo $content;
      if ($autoexit) {
        ob_flush();
        global $debug;
        if (ServerConfig::DEBUG_MODE)
          echo 'Next Update : ',date('Y-m-d H:i:s', $self->expire);
        exit();
      }
    }
    self::$instance[$name] = $self;
  }

  static function Save($name, $content = null, $lifetime = null) {
    if (isset(self::$instance[$name])) {
      $self = self::$instance[$name];
      if (isset($lifetime) && is_int($lifetime)) {
        $self->lifetime = $lifetime;
      }
      $self->content = isset($content) ? $content : ob_get_contents();
      $self->EndSync();
    }
  }

  static function Clean($exceeded = 172800) {
    $min_expire = time() - $exceeded;
    DB::Prepare('DELETE FROM document_cache WHERE expire < ?', array($min_expire));
    DB::Execute();
  }
}
