Skip to content

Instantly share code, notes, and snippets.

@mermshaus
Created March 24, 2013 17:19
Show Gist options
  • Save mermshaus/5232706 to your computer and use it in GitHub Desktop.
Save mermshaus/5232706 to your computer and use it in GitHub Desktop.
A file-based hit counter implementation
<?php // File: org.ermshaus.Counter.bundle.php
namespace org\ermshaus;
use Exception;
/**
*
*/
class CounterException extends Exception
{
}
/**
*
*
* Usage example:
*
* require_once __DIR__ . '/org.ermshaus.Counter.bundle.php';
*
* $counter = new org\ermshaus\Counter(__DIR__ . '/counter.dat');
*
* $counter->increase();
*
* echo $counter->get(), ' / ', $counter->getTotal();
*/
class Counter
{
/**
* Path to storage file
*
* @var string
*/
protected $dataFile;
/**
*
* @param string|null $dataFile If null, __DIR__ . '/data/counter.dat' will
* be used
*/
public function __construct($dataFile = null)
{
if ($dataFile !== null) {
if (!is_string($dataFile)) {
throw new CounterException('Argument needs to be string');
}
$this->setDataFile($dataFile);
} else {
$this->setDataFile(__DIR__ . '/data/counter.dat');
}
}
/**
* Returns the PATH component of the current request
*
* @return string
*/
protected function getPath()
{
$requestUri = $_SERVER['REQUEST_URI'];
return parse_url($requestUri, PHP_URL_PATH);
}
/**
* Increases the counter value for an identifier
*
* @param string $identifier If null, request path will be used
*/
public function increase($identifier = null)
{
if ($identifier === null) {
$identifier = $this->getPath();
}
if (!is_string($identifier)) {
throw new CounterException('Argument needs to be string');
}
$h = fopen($this->dataFile, 'c+b');
if (flock($h, LOCK_EX)) {
$contents = '';
while (!feof($h)) {
$contents .= fread($h, 8192);
}
$counterData = array();
if ('' !== trim($contents)) {
$counterData = unserialize($contents);
}
if (!array_key_exists($identifier, $counterData)) {
$counterData[$identifier] = 0;
}
$counterData[$identifier]++;
ftruncate($h, 0);
rewind($h);
$serialized = serialize($counterData);
fwrite($h, $serialized, strlen($serialized));
flock($h, LOCK_UN);
} else {
throw new CounterException('Unable to obtain lock');
}
fclose($h);
}
/**
* Returns the current counter value for an identifier
*
* @param string|null $identifier If null, request path will be used
* @return int
*/
public function get($identifier = null)
{
if ($identifier === null) {
$identifier = $this->getPath();
}
if (!is_string($identifier)) {
throw new CounterException('Argument needs to be string');
}
$returnValue = 0;
$h = fopen($this->dataFile, 'rb');
if (flock($h, LOCK_SH)) {
$contents = '';
while (!feof($h)) {
$contents .= fread($h, 8192);
}
$counterData = array();
if ('' !== trim($contents)) {
$counterData = unserialize($contents);
}
if (array_key_exists($identifier, $counterData)) {
$returnValue = $counterData[$identifier];
}
flock($h, LOCK_UN);
} else {
throw new CounterException('Unable to obtain lock');
}
fclose($h);
return $returnValue;
}
/**
* Returns see sum of all current counter values
*
* @return int
*/
public function getTotal()
{
$returnValue = 0;
$h = fopen($this->dataFile, 'rb');
if (flock($h, LOCK_SH)) {
$contents = '';
while (!feof($h)) {
$contents .= fread($h, 8192);
}
$counterData = array();
if ('' !== trim($contents)) {
$counterData = unserialize($contents);
}
$returnValue = array_sum($counterData);
flock($h, LOCK_UN);
} else {
throw new CounterException('Unable to obtain lock');
}
fclose($h);
return $returnValue;
}
/**
* Returns all stored counter data
*
* @return array
*/
public function getAllData()
{
$returnValue = array();
$h = fopen($this->dataFile, 'rb');
if (flock($h, LOCK_SH)) {
$contents = '';
while (!feof($h)) {
$contents .= fread($h, 8192);
}
$counterData = array();
if ('' !== trim($contents)) {
$counterData = unserialize($contents);
}
$returnValue = $counterData;
flock($h, LOCK_UN);
} else {
throw new CounterException('Unable to obtain lock');
}
fclose($h);
return $returnValue;
}
/**
* Gets the file in which counter values are stored
*
* @return string
*/
public function getDataFile()
{
return $this->dataFile;
}
/**
* Sets the file in which counter values are stored
*
* @param string $dataFile
*/
public function setDataFile($dataFile)
{
if (!is_string($dataFile)) {
throw new CounterException('Argument needs to be string');
}
$dataFile_rp = realpath($dataFile);
if (false === $dataFile_rp) {
if (false === touch($dataFile)) {
throw new CounterException(sprintf(
'Unable to create data file %s',
$dataFile
));
}
$dataFile_rp = realpath($dataFile);
if (false === $dataFile_rp) {
throw new CounterException('Unknown error');
}
}
$this->dataFile = $dataFile_rp;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment