diff --git a/DependencyInjection/Configuration/Configuration.php b/DependencyInjection/Configuration/Configuration.php index 0e9bfb57..4daa9098 100644 --- a/DependencyInjection/Configuration/Configuration.php +++ b/DependencyInjection/Configuration/Configuration.php @@ -66,6 +66,7 @@ public function getConfigTreeBuilder() $this->addDoctrineSection($rootNode); $this->addMonologSection($rootNode); $this->addSwiftMailerSection($rootNode); + $this->addProfilerStorageSection($rootNode); return $treeBuilder; } @@ -242,4 +243,23 @@ private function addSwiftMailerSection(ArrayNodeDefinition $rootNode) ->end() ->end(); } + + /** + * Adds the snc_redis.profiler_storage configuration + * + * @param ArrayNodeDefinition $rootNode + */ + private function addProfilerStorageSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('profiler_storage') + ->canBeUnset() + ->children() + ->scalarNode('client')->isRequired()->end() + ->scalarNode('ttl')->isRequired()->end() + ->end() + ->end() + ->end(); + } } diff --git a/DependencyInjection/SncRedisExtension.php b/DependencyInjection/SncRedisExtension.php index 9bcacc93..50d8c7ed 100644 --- a/DependencyInjection/SncRedisExtension.php +++ b/DependencyInjection/SncRedisExtension.php @@ -68,6 +68,10 @@ public function load(array $configs, ContainerBuilder $container) if (isset($config['swiftmailer'])) { $this->loadSwiftMailer($config, $container); } + + if (isset($config['profiler_storage'])) { + $this->loadProfilerStorage($config, $container, $loader); + } } /** @@ -385,6 +389,25 @@ protected function loadSwiftMailer(array $config, ContainerBuilder $container) $container->setAlias('swiftmailer.spool.redis', 'snc_redis.swiftmailer.spool'); } + /** + * Loads the profiler storage configuration. + * + * @param array $config A configuration array + * @param ContainerBuilder $container A ContainerBuilder instance + * @param XmlFileLoader $loader A XmlFileLoader instance + */ + protected function loadProfilerStorage(array $config, ContainerBuilder $container, XmlFileLoader $loader) + { + $loader->load('profiler_storage.xml'); + + $container->setParameter('snc_redis.profiler_storage.client', $config['profiler_storage']['client']); + $container->setParameter('snc_redis.profiler_storage.ttl', $config['profiler_storage']['ttl']); + + $client = $container->getParameter('snc_redis.profiler_storage.client'); + $client = sprintf('snc_redis.%s_client', $client); + $container->setAlias('snc_redis.profiler_storage.client', $client); + } + public function getConfiguration(array $config, ContainerBuilder $container) { return new Configuration($container->getParameter('kernel.debug')); diff --git a/Profiler/Storage/RedisProfilerStorage.php b/Profiler/Storage/RedisProfilerStorage.php new file mode 100644 index 00000000..515611a4 --- /dev/null +++ b/Profiler/Storage/RedisProfilerStorage.php @@ -0,0 +1,391 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Snc\RedisBundle\Profiler\Storage; + +use Symfony\Component\HttpKernel\Profiler\Profile; +use Symfony\Component\HttpKernel\Profiler\ProfilerStorageInterface; + +/** + * RedisProfilerStorage stores profiling information in Redis. + * + * This class is a reimplementation of + * the RedisProfilerStorage class from Symfony 2.8 + * + * @author Andrej Hudec + * @author Stephane PY + * @author Gijs van Lammeren + */ +class RedisProfilerStorage implements ProfilerStorageInterface +{ + /** + * Key prefix + * + * @var string + */ + const TOKEN_PREFIX = 'sf_prof_'; + + /** + * Index token name + * + * @var string + */ + const INDEX_NAME = 'index'; + + const REDIS_SERIALIZER_NONE = 0; + const REDIS_SERIALIZER_PHP = 1; + + /** + * The redis client. + * + * @var \Predis\Client|\Redis + */ + protected $redis; + + /** + * TTL for profiler data (in seconds). + * + * @var int + */ + protected $lifetime; + + /** + * Constructor. + * + * @param \Predis\Client|\Redis $redis Redis database connection + * @param int $lifetime The lifetime to use for the purge + */ + public function __construct($redis, $lifetime = 86400) + { + $this->redis = $redis; + $this->lifetime = (int) $lifetime; + } + + /** + * {@inheritdoc} + */ + public function find($ip, $url, $limit, $method, $start = null, $end = null) + { + $indexName = $this->getIndexName(); + + if (!$indexContent = $this->getValue($indexName, self::REDIS_SERIALIZER_NONE)) { + return array(); + } + + $profileList = array_reverse(explode("\n", $indexContent)); + $result = array(); + + foreach ($profileList as $item) { + if ($limit === 0) { + break; + } + + if ($item == '') { + continue; + } + + $values = explode("\t", $item, 7); + list($itemToken, $itemIp, $itemMethod, $itemUrl, $itemTime, $itemParent) = $values; + $statusCode = isset($values[6]) ? $values[6] : null; + + $itemTime = (int) $itemTime; + + if ($ip && false === strpos($itemIp, $ip) || $url && false === strpos($itemUrl, $url) || $method && false === strpos($itemMethod, $method)) { + continue; + } + + if (!empty($start) && $itemTime < $start) { + continue; + } + + if (!empty($end) && $itemTime > $end) { + continue; + } + + $result[] = array( + 'token' => $itemToken, + 'ip' => $itemIp, + 'method' => $itemMethod, + 'url' => $itemUrl, + 'time' => $itemTime, + 'parent' => $itemParent, + 'status_code' => $statusCode, + ); + --$limit; + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function purge() + { + // delete only items from index + $indexName = $this->getIndexName(); + + $indexContent = $this->getValue($indexName, self::REDIS_SERIALIZER_NONE); + + if (!$indexContent) { + return false; + } + + $profileList = explode("\n", $indexContent); + + $result = array(); + + foreach ($profileList as $item) { + if ($item == '') { + continue; + } + + if (false !== $pos = strpos($item, "\t")) { + $result[] = $this->getItemName(substr($item, 0, $pos)); + } + } + + $result[] = $indexName; + + return $this->delete($result); + } + + /** + * {@inheritdoc} + */ + public function read($token) + { + if (empty($token)) { + return false; + } + + $profile = $this->getValue($this->getItemName($token), self::REDIS_SERIALIZER_PHP); + + if ($profile) { + $profile = $this->createProfileFromData($token, $profile); + } + + return $profile; + } + + /** + * {@inheritdoc} + */ + public function write(Profile $profile) + { + $data = array( + 'token' => $profile->getToken(), + 'parent' => $profile->getParentToken(), + 'children' => array_map(function ($p) { return $p->getToken(); }, $profile->getChildren()), + 'data' => $profile->getCollectors(), + 'ip' => $profile->getIp(), + 'method' => $profile->getMethod(), + 'url' => $profile->getUrl(), + 'time' => $profile->getTime(), + ); + + $profileIndexed = $this->getValue($this->getItemName($profile->getToken())); + + if ($this->setValue($this->getItemName($profile->getToken()), $data, $this->lifetime, self::REDIS_SERIALIZER_PHP)) { + if (!$profileIndexed) { + // Add to index + $indexName = $this->getIndexName(); + + $indexRow = implode("\t", array( + $profile->getToken(), + $profile->getIp(), + $profile->getMethod(), + $profile->getUrl(), + $profile->getTime(), + $profile->getParentToken(), + $profile->getStatusCode(), + )) . "\n"; + + return $this->appendValue($indexName, $indexRow, $this->lifetime); + } + + return true; + } + + return false; + } + + /** + * Creates a Profile. + * + * @param string $token + * @param array $data + * @param Profile $parent + * @return Profile + */ + protected function createProfileFromData($token, $data, $parent = null) + { + $profile = new Profile($token); + $profile->setIp($data['ip']); + $profile->setMethod($data['method']); + $profile->setUrl($data['url']); + $profile->setTime($data['time']); + $profile->setCollectors($data['data']); + + if (!$parent && $data['parent']) { + $parent = $this->read($data['parent']); + } + + if ($parent) { + $profile->setParent($parent); + } + + foreach ($data['children'] as $token) { + if (!$token) { + continue; + } + + if (!$childProfileData = $this->getValue($this->getItemName($token), self::REDIS_SERIALIZER_PHP)) { + continue; + } + + $profile->addChild($this->createProfileFromData($token, $childProfileData, $profile)); + } + + return $profile; + } + + /** + * Gets the item name. + * + * @param string $token + * + * @return string + */ + protected function getItemName($token) + { + $name = $this->prefixKey($token); + + if ($this->isItemNameValid($name)) { + return $name; + } + + return false; + } + + /** + * Gets the name of the index. + * + * @return string + */ + protected function getIndexName() + { + $name = $this->prefixKey(self::INDEX_NAME); + + if ($this->isItemNameValid($name)) { + return $name; + } + + return false; + } + + /** + * Check if the item name is valid. + * + * @param string $name + * @throws \RuntimeException + * @return bool + */ + protected function isItemNameValid($name) + { + $length = strlen($name); + + if ($length > 2147483648) { + throw new \RuntimeException(sprintf('The Redis item key "%s" is too long (%s bytes). Allowed maximum size is 2^31 bytes.', $name, $length)); + } + + return true; + } + + /** + * Retrieves an item from the Redis server. + * + * @param string $key + * @param int $serializer + * + * @return mixed + */ + protected function getValue($key, $serializer = self::REDIS_SERIALIZER_NONE) + { + $value = $this->redis->get($key); + + if ($value && (self::REDIS_SERIALIZER_PHP === $serializer)) { + $value = unserialize($value); + } + + return $value; + } + + /** + * Stores an item on the Redis server under the specified key. + * + * @param string $key + * @param mixed $value + * @param int $expiration + * @param int $serializer + * + * @return bool + */ + protected function setValue($key, $value, $expiration = 0, $serializer = self::REDIS_SERIALIZER_NONE) + { + if (self::REDIS_SERIALIZER_PHP === $serializer) { + $value = serialize($value); + } + + return $this->redis->setex($key, $expiration, $value); + } + + /** + * Appends data to an existing item on the Redis server. + * + * @param string $key + * @param string $value + * @param int $expiration + * @return bool + */ + protected function appendValue($key, $value, $expiration = 0) + { + if ($this->redis->exists($key)) { + $this->redis->append($key, $value); + + return $this->redis->expire($key, $expiration); + } + + return $this->redis->setex($key, $expiration, $value); + } + + /** + * Removes the specified keys. + * + * @param array $keys + * @return bool + */ + protected function delete(array $keys) + { + return (bool) $this->redis->del($keys); + } + + /** + * Prefixes the key. + * + * @param string $key + * @return string + */ + protected function prefixKey($key) + { + return self::TOKEN_PREFIX . $key; + } +} diff --git a/Resources/config/profiler_storage.xml b/Resources/config/profiler_storage.xml new file mode 100644 index 00000000..40dc6e35 --- /dev/null +++ b/Resources/config/profiler_storage.xml @@ -0,0 +1,20 @@ + + + + + + Snc\RedisBundle\Profiler\Storage\RedisProfilerStorage + + + + + + %snc_redis.profiler_storage.ttl% + + + + diff --git a/Resources/doc/index.md b/Resources/doc/index.md index 2d2b0404..35d85e69 100644 --- a/Resources/doc/index.md +++ b/Resources/doc/index.md @@ -275,6 +275,20 @@ swiftmailer: type: redis ``` +### Profiler storage ### + +To store your profiler data in Redis for Symfony 3 add following to your config: + +``` yaml +snc_redis: + ... + profiler_storage: + client: profiler_storage + ttl: 3600 +``` + +This will overwrite the `profiler.storage` service. +Prior to [Symfony 3.0 support for Redis was built-in](http://symfony.com/doc/current/profiler/storage.html). ### Complete configuration example ### @@ -291,6 +305,11 @@ snc_redis: alias: cache dsn: redis://localhost/1 logging: true + profiler_storage: + type: predis + alias: profiler_storage + dsn: redis://localhost/2 + logging: false cluster: type: predis alias: cluster @@ -333,4 +352,7 @@ snc_redis: swiftmailer: client: default key: swiftmailer + profiler_storage: + client: profiler_storage + ttl: 3600 ``` diff --git a/Tests/Profiler/Storage/Mock/RedisMock.php b/Tests/Profiler/Storage/Mock/RedisMock.php new file mode 100644 index 00000000..6cac4ec3 --- /dev/null +++ b/Tests/Profiler/Storage/Mock/RedisMock.php @@ -0,0 +1,230 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Snc\RedisBundle\Tests\Profiler\Storage\Mock; + +/** + * RedisMock for simulating Redis extension in tests. + * + * @author Andrej Hudec + */ +class RedisMock +{ + private $connected = false; + private $storage = array(); + + /** + * Add a server to connection pool. + * + * @param string $host + * @param int $port + * @param float $timeout + * + * @return bool + */ + public function connect($host, $port = 6379, $timeout = 0) + { + if ('127.0.0.1' == $host && 6379 == $port) { + $this->connected = true; + + return true; + } + + return false; + } + + /** + * Verify if the specified key exists. + * + * @param string $key + * + * @return bool + */ + public function exists($key) + { + if (!$this->connected) { + return false; + } + + return isset($this->storage[$key]); + } + + /** + * Store data at the server with expiration time. + * + * @param string $key + * @param int $ttl + * @param mixed $value + * + * @return bool + */ + public function setex($key, $ttl, $value) + { + if (!$this->connected) { + return false; + } + + $this->storeData($key, $value); + + return true; + } + + /** + * Sets an expiration time on an item. + * + * @param string $key + * @param int $ttl + * + * @return bool + */ + public function expire($key, $ttl) + { + if (!$this->connected) { + return false; + } + + if (isset($this->storage[$key])) { + return true; + } + + return false; + } + + /** + * Retrieve item from the server. + * + * @param string $key + * + * @return bool + */ + public function get($key) + { + if (!$this->connected) { + return false; + } + + return $this->getData($key); + } + + /** + * Append data to an existing item. + * + * @param string $key + * @param string $value + * + * @return int Size of the value after the append + */ + public function append($key, $value) + { + if (!$this->connected) { + return false; + } + + if (isset($this->storage[$key])) { + $this->storeData($key, $this->getData($key).$value); + + return strlen($this->storage[$key]); + } + + return false; + } + + /** + * Remove specified keys. + * + * @param string|array $key + * + * @return int + */ + public function del($key) + { + if (!$this->connected) { + return false; + } + + if (is_array($key)) { + $result = 0; + foreach ($key as $k) { + if (isset($this->storage[$k])) { + unset($this->storage[$k]); + ++$result; + } + } + + return $result; + } + + if (isset($this->storage[$key])) { + unset($this->storage[$key]); + + return 1; + } + + return 0; + } + + /** + * Flush all existing items from all databases at the server. + * + * @return bool + */ + public function flushAll() + { + if (!$this->connected) { + return false; + } + + $this->storage = array(); + + return true; + } + + /** + * Close Redis server connection. + * + * @return bool + */ + public function close() + { + $this->connected = false; + + return true; + } + + private function getData($key) + { + if (isset($this->storage[$key])) { + return unserialize($this->storage[$key]); + } + + return false; + } + + private function storeData($key, $value) + { + $this->storage[$key] = serialize($value); + + return true; + } + + public function select($dbnum) + { + if (!$this->connected) { + return false; + } + + if (0 > $dbnum) { + return false; + } + + return true; + } +} diff --git a/Tests/Profiler/Storage/RedisProfilerStorageTest.php b/Tests/Profiler/Storage/RedisProfilerStorageTest.php new file mode 100644 index 00000000..61427fd6 --- /dev/null +++ b/Tests/Profiler/Storage/RedisProfilerStorageTest.php @@ -0,0 +1,298 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Profiler; + +use Snc\RedisBundle\Profiler\Storage\RedisProfilerStorage; +use Snc\RedisBundle\Tests\Profiler\Storage\Mock\RedisMock; +use Symfony\Component\HttpKernel\Profiler\Profile; + +class RedisProfilerStorageTest extends \PHPUnit_Framework_TestCase +{ + protected static $storage; + + protected function setUp() + { + $redisMock = new RedisMock(); + $redisMock->connect('127.0.0.1', 6379); + + self::$storage = new RedisProfilerStorage($redisMock, 86400); + + if (self::$storage) { + self::$storage->purge(); + } + } + + protected function tearDown() + { + if (self::$storage) { + self::$storage->purge(); + self::$storage = false; + } + } + + /** + * @return \Snc\RedisBundle\Profiler\Storage\RedisProfilerStorage + */ + protected function getStorage() + { + return self::$storage; + } + + public function testStore() + { + for ($i = 0; $i < 10; ++$i) { + $profile = new Profile('token_' . $i); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar'); + $profile->setMethod('GET'); + $this->getStorage()->write($profile); + } + + $this->assertCount(10, $this->getStorage()->find('127.0.0.1', 'http://foo.bar', 20, 'GET'), '->write() stores data in the storage'); + } + + public function testChildren() + { + $parentProfile = new Profile('token_parent'); + $parentProfile->setIp('127.0.0.1'); + $parentProfile->setUrl('http://foo.bar/parent'); + + $childProfile = new Profile('token_child'); + $childProfile->setIp('127.0.0.1'); + $childProfile->setUrl('http://foo.bar/child'); + + $parentProfile->addChild($childProfile); + + $this->getStorage()->write($parentProfile); + $this->getStorage()->write($childProfile); + + // Load them from storage + $parentProfile = $this->getStorage()->read('token_parent'); + $childProfile = $this->getStorage()->read('token_child'); + + // Check child has link to parent + $this->assertNotNull($childProfile->getParent()); + $this->assertEquals($parentProfile->getToken(), $childProfile->getParentToken()); + + // Check parent has child + $children = $parentProfile->getChildren(); + $this->assertCount(1, $children); + $this->assertEquals($childProfile->getToken(), $children[0]->getToken()); + } + + public function testStoreSpecialCharsInUrl() + { + // The storage accepts special characters in URLs (Even though URLs are not + // supposed to contain them) + $profile = new Profile('simple_quote'); + $profile->setUrl('http://foo.bar/\''); + $this->getStorage()->write($profile); + $this->assertTrue(false !== $this->getStorage()->read('simple_quote'), '->write() accepts single quotes in URL'); + + $profile = new Profile('double_quote'); + $profile->setUrl('http://foo.bar/"'); + $this->getStorage()->write($profile); + $this->assertTrue(false !== $this->getStorage()->read('double_quote'), '->write() accepts double quotes in URL'); + + $profile = new Profile('backslash'); + $profile->setUrl('http://foo.bar/\\'); + $this->getStorage()->write($profile); + $this->assertTrue(false !== $this->getStorage()->read('backslash'), '->write() accepts backslash in URL'); + + $profile = new Profile('comma'); + $profile->setUrl('http://foo.bar/,'); + $this->getStorage()->write($profile); + $this->assertTrue(false !== $this->getStorage()->read('comma'), '->write() accepts comma in URL'); + } + + public function testStoreDuplicateToken() + { + $profile = new Profile('token'); + $profile->setUrl('http://example.com/'); + + $this->assertTrue($this->getStorage()->write($profile), '->write() returns true when the token is unique'); + + $profile->setUrl('http://example.net/'); + + $this->assertTrue($this->getStorage()->write($profile), '->write() returns true when the token is already present in the storage'); + $this->assertEquals('http://example.net/', $this->getStorage()->read('token')->getUrl(), '->write() overwrites the current profile data'); + + $this->assertCount(1, $this->getStorage()->find('', '', 1000, ''), '->find() does not return the same profile twice'); + } + + public function testRetrieveByIp() + { + $profile = new Profile('token'); + $profile->setIp('127.0.0.1'); + $profile->setMethod('GET'); + $this->getStorage()->write($profile); + + $this->assertCount(1, $this->getStorage()->find('127.0.0.1', '', 10, 'GET'), '->find() retrieve a record by IP'); + $this->assertCount(0, $this->getStorage()->find('127.0.%.1', '', 10, 'GET'), '->find() does not interpret a "%" as a wildcard in the IP'); + $this->assertCount(0, $this->getStorage()->find('127.0._.1', '', 10, 'GET'), '->find() does not interpret a "_" as a wildcard in the IP'); + } + + public function testRetrieveByUrl() + { + $profile = new Profile('simple_quote'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/\''); + $profile->setMethod('GET'); + $this->getStorage()->write($profile); + + $profile = new Profile('double_quote'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/"'); + $profile->setMethod('GET'); + $this->getStorage()->write($profile); + + $profile = new Profile('backslash'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo\\bar/'); + $profile->setMethod('GET'); + $this->getStorage()->write($profile); + + $profile = new Profile('percent'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/%'); + $profile->setMethod('GET'); + $this->getStorage()->write($profile); + + $profile = new Profile('underscore'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/_'); + $profile->setMethod('GET'); + $this->getStorage()->write($profile); + + $profile = new Profile('semicolon'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar/;'); + $profile->setMethod('GET'); + $this->getStorage()->write($profile); + + $this->assertCount(1, $this->getStorage()->find('127.0.0.1', 'http://foo.bar/\'', 10, 'GET'), '->find() accepts single quotes in URLs'); + $this->assertCount(1, $this->getStorage()->find('127.0.0.1', 'http://foo.bar/"', 10, 'GET'), '->find() accepts double quotes in URLs'); + $this->assertCount(1, $this->getStorage()->find('127.0.0.1', 'http://foo\\bar/', 10, 'GET'), '->find() accepts backslash in URLs'); + $this->assertCount(1, $this->getStorage()->find('127.0.0.1', 'http://foo.bar/;', 10, 'GET'), '->find() accepts semicolon in URLs'); + $this->assertCount(1, $this->getStorage()->find('127.0.0.1', 'http://foo.bar/%', 10, 'GET'), '->find() does not interpret a "%" as a wildcard in the URL'); + $this->assertCount(1, $this->getStorage()->find('127.0.0.1', 'http://foo.bar/_', 10, 'GET'), '->find() does not interpret a "_" as a wildcard in the URL'); + } + + public function testStoreTime() + { + $dt = new \DateTime('now'); + $start = $dt->getTimestamp(); + + for ($i = 0; $i < 3; ++$i) { + $dt->modify('+1 minute'); + $profile = new Profile('time_' . $i); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://foo.bar'); + $profile->setTime($dt->getTimestamp()); + $profile->setMethod('GET'); + $this->getStorage()->write($profile); + } + + $records = $this->getStorage()->find('', '', 3, 'GET', $start, time() + 3 * 60); + $this->assertCount(3, $records, '->find() returns all previously added records'); + $this->assertEquals($records[0]['token'], 'time_2', '->find() returns records ordered by time in descendant order'); + $this->assertEquals($records[1]['token'], 'time_1', '->find() returns records ordered by time in descendant order'); + $this->assertEquals($records[2]['token'], 'time_0', '->find() returns records ordered by time in descendant order'); + + $records = $this->getStorage()->find('', '', 3, 'GET', $start, time() + 2 * 60); + $this->assertCount(2, $records, '->find() should return only first two of the previously added records'); + } + + public function testRetrieveByEmptyUrlAndIp() + { + for ($i = 0; $i < 5; ++$i) { + $profile = new Profile('token_' . $i); + $profile->setMethod('GET'); + $this->getStorage()->write($profile); + } + $this->assertCount(5, $this->getStorage()->find('', '', 10, 'GET'), '->find() returns all previously added records'); + $this->getStorage()->purge(); + } + + public function testRetrieveByMethodAndLimit() + { + foreach (array('POST', 'GET') as $method) { + for ($i = 0; $i < 5; ++$i) { + $profile = new Profile('token_' . $i . $method); + $profile->setMethod($method); + $this->getStorage()->write($profile); + } + } + + $this->assertCount(5, $this->getStorage()->find('', '', 5, 'POST')); + + $this->getStorage()->purge(); + } + + public function testPurge() + { + $profile = new Profile('token1'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://example.com/'); + $profile->setMethod('GET'); + $this->getStorage()->write($profile); + + $this->assertTrue(false !== $this->getStorage()->read('token1')); + $this->assertCount(1, $this->getStorage()->find('127.0.0.1', '', 10, 'GET')); + + $profile = new Profile('token2'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://example.net/'); + $profile->setMethod('GET'); + $this->getStorage()->write($profile); + + $this->assertTrue(false !== $this->getStorage()->read('token2')); + $this->assertCount(2, $this->getStorage()->find('127.0.0.1', '', 10, 'GET')); + + $this->getStorage()->purge(); + + $this->assertEmpty($this->getStorage()->read('token'), '->purge() removes all data stored by profiler'); + $this->assertCount(0, $this->getStorage()->find('127.0.0.1', '', 10, 'GET'), '->purge() removes all items from index'); + } + + public function testDuplicates() + { + for ($i = 1; $i <= 5; ++$i) { + $profile = new Profile('foo' . $i); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://example.net/'); + $profile->setMethod('GET'); + + ///three duplicates + $this->getStorage()->write($profile); + $this->getStorage()->write($profile); + $this->getStorage()->write($profile); + } + $this->assertCount(3, $this->getStorage()->find('127.0.0.1', 'http://example.net/', 3, 'GET'), '->find() method returns incorrect number of entries'); + } + + public function testStatusCode() + { + $profile = new Profile('token1'); + $profile->setStatusCode(200); + $this->getStorage()->write($profile); + + $profile = new Profile('token2'); + $profile->setStatusCode(404); + $this->getStorage()->write($profile); + + $tokens = $this->getStorage()->find('', '', 10, ''); + $this->assertCount(2, $tokens); + $this->assertContains($tokens[0]['status_code'], array(200, 404)); + $this->assertContains($tokens[1]['status_code'], array(200, 404)); + } +}