ytake Hatena

Web Application Developer

HHVM/Hack マイクロフレームワークにCache追加

ytake.hateblo.jp

以前から作ってたフレームワークで、
都度Cache組み込むのが面倒なのと、HackでPSRに準拠する必要もないだろうということで、
JavaEhcacheっぽい名前(名前だけ) のものを作り、
マイクロフレームワークに組み込みました。

github.com

HHVM/Hackにはmemcachedとredisのエクステンションが含まれているので、
なにもしなくても使えるのが楽で良いです。

後日 Mcrouter 対応ドライバも追加してリリースする予定です。

簡単な使い方

使いたいキャッシュドライバーをCacheManagerで指定すると、
そのキャッシュドライバが利用できるようにしています。

が、ドライバー単体でも利用できます

<?hh
use Nazg\HCache\Element;
use Nazg\HCache\CacheManager;

$manager = new CacheManager();
$cache = $manager->createCache('memcached');
$mc = new \Memcached('mc');
$mc->addServers([['127.0.0.1', 11211]]);
$cache->setMemcached($mc);
$cache->createCache('map')?->save('cache', new Element('testing', 0));

独自ドライバ追加方法

Nazg\HCache\CacheProvider クラスを継承していればなんでも使えます
ドライバ実装は下記の通りです。

<?hh // strict

use Nazg\HCache\CacheProvider;

class NullCache extends CacheProvider {
  <<__Override>>
  public function fetch(string $id): mixed {
    return;
  }
  <<__Override>>
  public function contains(string $id): bool {
    return false;
  }
  <<__Override>>
  public function save(string $id, Element $element): bool {
    return true;
  }
  <<__Override>>
  public function delete(string $id): bool {
    return true;
  }
  <<__Override>>
  public function flushAll(): bool {
    return true;
  }
}

上記の登録後は、以下の通りに記述するだけで利用できます。
簡単

<?hh

use Nazg\HCache\Element;
use Nazg\HCache\CacheManager;

$manager = new CacheManager();
$manager->addCache('null', () ==> new NullCache());
$manager->createCache('null'); // return NullCache

Hackで動的メソッドコール実装するヒント

Hackでは基本的に動的メソッドはtype checkerでエラーになりますが、
type checkerが理解できるように記述すればOKです。

CacheManagerは下記の実装になっています

<?hh

  protected Map<string, classname<CacheProvider>> $cache = Map {
    'apc' => \Nazg\HCache\Driver\ApcCache::class,
    'void' => \Nazg\HCache\Driver\VoidCache::class,
    'map' => \Nazg\HCache\Driver\MapCache::class,
    'file' => \Nazg\HCache\Driver\FileSystemCache::class,
    'memcached' => \Nazg\HCache\Driver\MemcachedCache::class,
    'redis' => \Nazg\HCache\Driver\RedisCache::class,
  };

  protected Map<string, (function():CacheProvider)> $userCache = Map{};

  public function createCache(string $namedCache): ?CacheProvider {
    if($this->cache->contains($namedCache)) {
      $cache = $this->cache->at($namedCache);
      return new $cache();
    }
    if($this->userCache->contains($namedCache)) {
      $cache = $this->userCache->at($namedCache);
      return $cache();
    }
    return null;
  }

CacheProviderでは実は下記のようになっており、継承に制約をつけています。

<?hh // strict

namespace Nazg\HCache;

use Nazg\HCache\{Cacheable, FlushableCache};

<<__ConsistentConstruct>>
abstract class CacheProvider implements Cacheable, FlushableCache {

}

コンストラクタに何かを付け加えることはできませんので、
インスタンス生成方法が統一されていますので、type checkerが 通ってよし と判断しています。

フレームワークへの組み込み

Containerにインスタンス生成方法等を登録します。

設定値はクラスでは関与せず、外から与えるものとしてshapeで縛っています。

<?hh

abstract class CacheServiceModule extends ServiceModule {
  protected Driver $defaultDriver = Driver::File;
  <<__Override>>
  public function provide(FactoryContainer $container): void {
    $container->set(
      CacheManager::class,
      $container ==> new CacheManager(),
      \Ytake\HHContainer\Scope::SINGLETON,
    );
    $container->set(
      CacheProvider::class,
      $container ==> {
        $manager = $container->get(CacheManager::class);
        if($manager instanceof CacheManager) {
          return $this->detectCacheProvider(
            $manager->createCache($this->defaultDriver),
            $this->cacheConfigure($container)
          );
        }
        throw new \RuntimeException("Failed to resolve " . CacheProvider::class);
      },
      \Ytake\HHContainer\Scope::SINGLETON,
    );
  }

  abstract protected function cacheConfigure(
    FactoryContainer $container
  ): CacheConfiguration;

  protected function detectCacheProvider(
    ?CacheProvider $provider, 
    CacheConfiguration $cacheConfigure
  ): CacheProvider {
    invariant($provider instanceof CacheProvider, "provider type error");
    if($this->defaultDriver === Driver::File) {
      if($provider instanceof FileSystemCache) {
        $dir = $cacheConfigure->getFileSystemDir();
        if(!is_null($dir)) {
          $provider->setDirectory($dir);
        }
      }
    }
    if($this->defaultDriver === Driver::Memcached) {
      if($provider instanceof MemcachedCache) {
        $m = $cacheConfigure->getMemcached();
        if(!is_null($m)) {
          $provider->setMemcached($m);
        }
      }
    }
    if($this->defaultDriver === Driver::Redis) {
      if($provider instanceof RedisCache) {
        $r = $cacheConfigure->getRedis();
        if(!is_null($r)) {
          $provider->setRedis($r);
        }
      }
    }
    return $provider;
  }
}

このクラスを継承し、利用者がどこから設定値を持ってくるかを指定すれば適切に起動します。
設定値のshapeは下記の通りです。

<?hh // strict

namespace Nazg\Cache;

type MemcachedServer = shape(
  'host' => string,
  'port' => int,
  ?'weight' => int,
);
type FileSystemConfig = shape(
  'cacheStoreDir' => string
);
type MemcachedConfig = shape(
  'servers' => ImmVector<MemcachedServer>,
  ?'persistentId' => string,
);
type RedisConfig = shape(
  'host' => string,
  ?'port' => int,
  ?'password' => string,
  ?'prefix' => string,
  ?'readTimeout' => float,
  ?'persistent' => bool,
  ?'database' => int
);

Skeletonへの組み込み

フレームワークのSkeletonを使えばこの辺りはデフォルトで対応しています。

github.com

設定値は下記で用意しています。(cache.global.php)

<?hh

use Nazg\Cache\Driver;
return [
  \Nazg\Foundation\Service::CACHE => [
    /*
     * Supported "apc", "map", "file", "memcached", "redis", "void"
     * Driver::Apc, Driver::Map, Driver::File, Driver::Memcached, Driver::Redis, Driver::void
     */
    'driver' => Driver::Memcached,
    'drivers' => [
      Driver::File => shape(
        'cacheStoreDir' => __DIR__ . '/../storages/cache/',
      ),
      Driver::Memcached => shape(
        'servers' => ImmVector{
          shape(
            'host' => '127.0.0.1',
            'port' => 11211,
            'weight' => 100,
          ),
        },
        // 'persistentId' => 'rename',
      ),
      Driver::Redis => shape(
        'host' => '127.0.0.1',
        'port' => 6379,
        'database' => 0,
        // 'password' => '', // optional
        // 'prefix' => '', // optional
        // 'readTimeout' => 1, // optional
        // 'persistent' => false, // optional
      ),
    ],
  ],
];