Limit the number of PHP API calls [duplicate] - php

I use a variety of 3rd party web APIs, and many of them enforce rate limiting. It would be very useful to have a fairly generic PHP library that I could rate limit my calls with. I can think of a few ways to do it, perhaps by putting calls into a queue with a timestamp of when the call can be made, but I was hoping to avoid reinventing the wheel if someone else has already done this well.

You can do rate limiting with the token bucket algorithm. I implemented that for you in PHP: bandwidth-throttle/token-bucket
:
use bandwidthThrottle\tokenBucket\Rate;
use bandwidthThrottle\tokenBucket\TokenBucket;
use bandwidthThrottle\tokenBucket\storage\FileStorage;
$storage = new FileStorage(__DIR__ . "/api.bucket");
$rate = new Rate(10, Rate::SECOND);
$bucket = new TokenBucket(10, $rate, $storage);
$bucket->bootstrap(10);
if (!$bucket->consume(1, $seconds)) {
http_response_code(429);
header(sprintf("Retry-After: %d", floor($seconds)));
exit();
}

I realize this is an old thread but thought I'd post my solution since it was based on something else I found on SE. I looked for a while for an answer myself but had trouble finding something good. It's based on the Python solution discussed here, but I've added support for variable-sized requests and turned it into a function generator using PHP closures.
function ratelimiter($rate = 5, $per = 8) {
$last_check = microtime(True);
$allowance = $rate;
return function ($consumed = 1) use (
&$last_check,
&$allowance,
$rate,
$per
) {
$current = microtime(True);
$time_passed = $current - $last_check;
$last_check = $current;
$allowance += $time_passed * ($rate / $per);
if ($allowance > $rate)
$allowance = $rate;
if ($allowance < $consumed) {
$duration = ($consumed - $allowance) * ($per / $rate);
$last_check += $duration;
usleep($duration * 1000000);
$allowance = 0;
}
else
$allowance -= $consumed;
return;
};
}
It can be used to limit just about anything. Here's a stupid example that limits a simple statement at the default five "requests" per eight seconds:
$ratelimit = ratelimiter();
while (True) {
$ratelimit();
echo "foo".PHP_EOL;
}
Here's how I'm using it to limit batched requests against the Facebook Graph API at 600 requests per 600 seconds based on the size of the batch:
$ratelimit = ratelimiter(600, 600);
while (..) {
..
$ratelimit(count($requests));
$response = (new FacebookRequest(
$session, 'POST', '/', ['batch' => json_encode($requests)]
))->execute();
foreach ($response->..) {
..
}
}
Hope this helps someone!

This is essentially the same as #Jeff's answer, but I have tidied the code up a lot and added PHP7.4 type/return hinting.
I have also published this as a composer package: https://github.com/MacroMan/rate-limiter
composer require macroman/rate-limiter
/**
* Class RateLimiter
*
* #package App\Components
*/
class Limiter
{
/**
* Limit to this many requests
*
* #var int
*/
private int $frequency = 0;
/**
* Limit for this duration
*
* #var int
*/
private int $duration = 0;
/**
* Current instances
*
* #var array
*/
private array $instances = [];
/**
* RateLimiter constructor.
*
* #param int $frequency
* #param int $duration #
*/
public function __construct(int $frequency, int $duration)
{
$this->frequency = $frequency;
$this->duration = $duration;
}
/**
* Sleep if the bucket is full
*/
public function await(): void
{
$this->purge();
$this->instances[] = microtime(true);
if (!$this->is_free()) {
$wait_duration = $this->duration_until_free();
usleep($wait_duration);
}
}
/**
* Remove expired instances
*/
private function purge(): void
{
$cutoff = microtime(true) - $this->duration;
$this->instances = array_filter($this->instances, function ($a) use ($cutoff) {
return $a >= $cutoff;
});
}
/**
* Can we run now?
*
* #return bool
*/
private function is_free(): bool
{
return count($this->instances) < $this->frequency;
}
/**
* Get the number of microseconds until we can run the next instance
*
* #return float
*/
private function duration_until_free(): float
{
$oldest = $this->instances[0];
$free_at = $oldest + $this->duration * 1000000;
$now = microtime(true);
return ($free_at < $now) ? 0 : $free_at - $now;
}
}
Usage is the same
use RateLimiter\Limiter;
// Limit to 6 iterations per second
$limiter = new Limiter(6, 1);
for ($i = 0; $i < 50; $i++) {
$limiter->await();
echo "Iteration $i" . PHP_EOL;
}

As an alternate, I've (in the past) created a "cache" folder that stored the API calls so if I try to make the same call again, within a specific time range, it grabs from the cache first (more seamless) until it's okay to make a new call. May end up with archived information in the short term, but saves you from the API blocking you in the long term.

I liked mwp's answer and I wanted to convert it to OO to make me feel warm and fuzzy. I ended up drastically rewriting it to the point that it is totally unrecognizable from his version. So, here is my mwp-inspired OO version.
Basic explanation: Every time await is called, it saves the current timestamp in an array and throws out all old timestamps that arent relevant anymore (greater than the duration of the interval). If the rate limit is exceeded, then it calculates the time until it will be freed up again and sleeps until then.
Usage:
$limiter = new RateLimiter(4, 1); // can be called 4 times per 1 second
for($i = 0; $i < 10; $i++) {
$limiter->await();
echo microtime(true) . "\n";
}
I also added a little syntactic sugar for a run method.
$limiter = new RateLimiter(4, 1);
for($i = 0; $i < 10; $i++) {
$limiter->run(function() { echo microtime(true) . "\n"; });
}
<?php
class RateLimiter {
private $frequency;
private $duration;
private $instances;
public function __construct($frequency, $duration) {
$this->frequency = $frequency;
$this->duration = $duration;
$this->instances = [];
}
public function await() {
$this->purge();
$this->instances[] = microtime(true);
if($this->is_free()) {
return;
}
else {
$wait_duration = $this->duration_until_free();
usleep(floor($wait_duration));
return;
}
}
public function run($callback) {
if(!is_callable($callback)) {
return false;
}
$this->await();
$callback();
return true;
}
public function purge() {
$this->instances = RateLimiter::purge_old_instances($this->instances, $this->duration);
}
public function duration_until_free() {
return RateLimiter::get_duration_until_free($this->instances, $this->duration);
}
public function is_free() {
return count($this->instances) < $this->frequency;
}
public static function get_duration_until_free($instances, $duration) {
$oldest = $instances[0];
$free_at = $oldest + $duration * 1000000;
$now = microtime(true);
if($free_at < $now) {
return 0;
}
else {
return $free_at - $now;
}
}
public static function purge_old_instances($instances, $duration) {
$now = microtime(true);
$cutoff = $now - $duration;
return array_filter($instances, function($a) use ($duration, $cutoff) {
return $a >= $cutoff;
});
}
}

PHP source code to limit access to your API by allowing a request every 5 seconds for any user and using Redix.
Installing the Redis/Redix client :
composer require predis/predis
Download Redix (https://github.com/alash3al/redix/releases) depending on your operating system, then start the service :
./redix_linux_amd64
The following answer indicates that Redix is listening on ports 6380 for RESP protocol and 7090 for HTTP protocol.
redix resp server available at : localhost:6380
redix http server available at : localhost:7090
In your API, add the following code to the header :
<?php
require_once 'class.ratelimit.redix.php';
$rl = new RateLimit();
$waitfor = $rl->getSleepTime($_SERVER['REMOTE_ADDR']);
if ($waitfor>0) {
echo 'Rate limit exceeded, please try again in '.$waitfor.'s';
exit;
}
// Your API response
echo 'API response';
The source code for the script class.ratelimit.redix.php is :
<?php
require_once __DIR__.'/vendor/autoload.php';
Predis\Autoloader::register();
class RateLimit {
private $redis;
const RATE_LIMIT_SECS = 5; // allow 1 request every x seconds
public function __construct() {
$this->redis = new Predis\Client([
'scheme' => 'tcp',
'host' => 'localhost', // or the server IP on which Redix is running
'port' => 6380
]);
}
/**
* Returns the number of seconds to wait until the next time the IP is allowed
* #param ip {String}
*/
public function getSleepTime($ip) {
$value = $this->redis->get($ip);
if(empty($value)) {
// if the key doesn't exists, we insert it with the current datetime, and an expiration in seconds
$this->redis->set($ip, time(), self::RATE_LIMIT_SECS*1000);
return 0;
}
return self::RATE_LIMIT_SECS - (time() - intval(strval($value)));
} // getSleepTime
} // class RateLimit

Related

PHP Calculating with time with a negative start value

I'm trying to convert a certain excel sheet to a php app.
Within the excel sheet you could do time calculations as eg:
A starting time A1 (could be negative), and for every day you add or substract a certain amount of hours and minutes.
- A1= -10:59
- A2= -7:36
- A3= -18:35 (A1 + A2)
- B2= 4:24
- B3= -14:11 (A3 + B2)
And so on ...
Here is some code i've tested with without succes ofcourse...
$startTime = new \DateTime('00:00:00');
$startVal = new \DateInterval('PT10H59M');
$startTime->sub($startVal); // -10:59:00
$timeSpan = new \DateInterval('PT7H36M'); // 7:36:00
$addTime = $startTime->add($timeSpan);
What is the best way to solve this in PHP? I've been testing around with DateTime but this won't allow me te start with a negative time value.
Hope someone can give me a hand.
Kind regards,
Jochem
I have written a simple class to handle this. It works for your given example but I was unable to test further as I cannot get negative times in my version of Excel. Let me know if it fails at some point.
class MyTime
{
private $positive = true;
private $hour=0;
private $minute=0;
/**
* MyTime constructor.
* Split the given time string into hours and minutes and whether it is positive or negative
*
* #param string $timeString In the format '-10:59', '4:35', or optionally just minutes '24'
*/
public function __construct($timeString)
{
if(!empty($timeString))
{
if(strpos($timeString,'-')===0)
{
$this->positive = false;
$timeString = substr($timeString,1);
}
$timeParts = explode(':',$timeString);
if(!empty($timeParts))
{
if(count($timeParts) == 1)
{
$this->hour = 0;
$this->minute = intval($timeParts[0]);
}
else
{
$this->hour = intval($timeParts[0]);
$this->minute = intval($timeParts[1]);
}
}
}
}
public function getHour()
{
return $this->hour;
}
public function getMinute()
{
return $this->minute;
}
/**
* Convert into minutes either negative or positive
* #return int
*/
public function inMinutes()
{
$minutes = ($this->hour*60)+$this->minute;
if(!$this->positive)
{
$minutes *= -1;
}
return $minutes;
}
/**
* Convert back to a string for output
* #return string
*/
public function __toString()
{
return ($this->positive?'':'-').$this->getHour().':'.str_pad($this->getMinute(),2,'0',STR_PAD_LEFT);
}
/**
* Add on the given time which could be negative
* #param MyTime $time
*
* #return $this
*/
public function add(MyTime $time)
{
$newMinutes = $this->inMinutes() + $time->inMinutes();
if($newMinutes<0)
{
$this->hour = (int) ceil($newMinutes/60);
}
else
{
$this->hour = (int) floor($newMinutes/60);
}
$this->minute = abs($newMinutes-($this->hour*60));
if($newMinutes<0)
{
$this->positive = false;
$this->hour *= -1;
}
else
{
$this->positive = true;
}
return $this;
}
}
$time = new MyTime('-10:59');
echo $time."\n";
echo $time->add(new MyTime('-7:36'))."\n";
echo $time->add(new MyTime('4:24'))."\n";
echo $time->add(new MyTime('18:32'))."\n";
$time = new MyTime('10:59');
echo $time."\n";
echo $time->add(new MyTime('-59'))."\n";

Rate Limiting By Session PHP Without Library's

I'm using PHP for my as my backend for my webservice, I need the webservice to be able to set a rate limit of maximum of 1000 requests within the last 24 hours, based off the users PHP session. Is that possible to do it without using a database and just rate limit using only PHP. I have currently already made a rate limiter for 1 request per second per session, but I am looking to set a rate limit of 1000 requests per session in the last 24 hours. PS I'm new to PHP, any help would be great.
Here the code I did for the 1 per second rate limit.
class gaqsession {
public $lastrequest;
public function __construct() {
$this->lastrequest = time();
}
public function ratelimited() {
if($this->lastrequest == time()) {
$this->lastrequest = time();
return true;
} else {
$this->lastrequest = time();
return false;
}
}
}
You can do this provided your sessions are set up to last at least 24 hours. The only real trick is saving rate counts in the session in a way that allows you to maintain them within a 24 hour window. You can do that with a simple array that uses time as keys. Here's a class that adds some features around an array like that in order to easily manage the data, along with some example code that runs it through it's paces.
/**
* Class RateLimitCounter
* Track number of events by time. Intended to be set on a session.
*/
class RateLimitCounter
{
// The time -> request count data
private $timeline = [];
/**
* Log an event in the timeline
*/
public function increment()
{
$now = time();
if (!array_key_exists($now, $this->timeline))
{
$this->timeline[$now] = 0;
}
$this->timeline[$now]++;
}
/**
* Return the total number of events logged in the counter
* #return int
*/
public function getTotal()
{
return array_sum($this->timeline);
}
/**
* Remove any timeline data older than 24 hours
*/
private function trim()
{
// Get the current time
$now = time();
// Time is in seconds, so subtract 1 day worth of seconds
$timeFloor = $now - 86400;
// Filter out all timeline entries more than 24 hours old
$this->timeline = array_filter($this->timeline, function ($key) use ($timeFloor) {
return $key > $timeFloor;
}, ARRAY_FILTER_USE_KEY);
}
public function __serialize(): array
{
return [
'timeline' => $this->timeline
];
}
/**
* Wake up! Set the timeline data and trim data older than 24 hours
* #param array $data
*/
public function __unserialize(array $data): void
{
$this->timeline = $data['timeline'];
$this->trim();
}
}
/**
* Verify that the rate limit has not been exceeded. Bail out if it has been.
* #param $counter
* #return bool
*/
function rateLimit($counter)
{
$limit = 1000;
if ($counter->getTotal() > $limit)
{
// Do whatever you need to here, throw an exception, redirect to an error page, etc.
exit('Rate limit exceeded' . PHP_EOL);
}
return true;
}
/*
* Instantiate a counter - this is what you would do if you do not already have one on the session
*/
$counter = new RateLimitCounter();
/*
* Simulate some prior activity
* Let's get close to the limit then save to the "session"
*/
for ($i = 0; $i <= 995; $i++)
{
$counter->increment();
}
// Mock session
$dummySession = ['counter' => $counter];
// Serialize the session
$serializedSession = serialize($dummySession);
// Unserialize the session
$session = unserialize($serializedSession);
$counter = $session['counter'];
// Do API calls until we hit our limit. There should be 5 remaining.
while (rateLimit($counter))
{
apiCall();
// Don't forget to increment the counter for each call
$counter->increment();
}
// Dummy function to simulate your API call
function apiCall()
{
echo 'Doing something interesting' . PHP_EOL;
}

InvalidArgumentException in Carbon::createFromDate()->age

Hello i'm writing a custom validation for input data in my laravel project. I'm using Carbon::createFromDate()->age in order to get user age and check whether he's 16 or more. I reckon i'm not doing it properly because i get
InvalidArgumentException with such errors:
The separation symbol could not be found
The separation symbol could not be found
A two digit minute could not be found
Unexpected data found.
Trailing data
$rok is a year(1996 eg.) $miesiac is a month and $dzien is a Day. Pesel is an unique ID number of a Polish citizen. From pesel i get the date(year, month, day)
I'm getting some big numbers and i don't know what they mean Here's dd: "year:21586738427 month:1900167 day:9001727"
Here's code of my AppServiceProvider
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Carbon\Carbon;
use Validator;
use Log;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
//Custom validator for pesel validation
Validator::extend('pesel',function($attribute,$value,$parameters)
{
$v = $value;
//check if psesel is 11 chars long
if (strlen($v) != 11)
{
Log::info("Pesel is not 11 chars long");
return;
}
//check whether all chars are numbers
$aInt = array();
for($i = 0; $i < 11; $i++)
{
$val = substr($v,$i+1);
$valInt = (int) $val;
$aInt[$i] = $valInt;
if(is_nan($aInt[$i]))
{
Log::info("User inserted invalid number");
return;
}
}
//check control sum
$wagi = [1,3,7,9,1,3,7,9,1,3,1];
$sum = 0;
for($i = 0;$i < 11;$i++)
{
$sum += $wagi[$i]*$aInt[$i];
if(Log::info(($sum%10)!=0))
{
return;
}
//count the year,month,and day from pesel
$rok = 1900+$aInt[0]*10+$aInt[1];
if($aInt[2]>=2 && $aInt[2]<8)
{
$rok += floor($aInt[2]/2)*100;
}
if($aInt[2]>=8)
{
$rok -= 100;
}
$miesiac = ($aInt[2]%2)*10+$aInt[3];
$dzien = $aInt[4]*10+$aInt[5];
Log::info("Parsing the date in carbon");
//validate whether user is 16 years or more
$userAge = Carbon::createFromDate($rok, $miesiac, $dzien,'Europe/Warsaw')->age;
if($userAge < 16)
{
Log::info("user is not 16 or more");
return;
}
}
});
}
/**
* Register any application services.
*
* #return void
*/
public function register()
{
//
}
}
Ok i solved the problem. It was fault of my code
$val = substr($v,$i+1);
is wrong
it should be
$val = substr($v,$i,1);
Now it works

PHP testing between pthreads and curl

We are planning to building real time bidding and we are evaluating performance of PHP compare to Java in terms of throughput/response times etc.
(Java part is taken care by other member of team)
Initial start:
I have a test script which makes 50 http connection to different servers.
1st approach
- I am using curl_multi_init function and I get response under 7 seconds.
2nd approach
- I am using PHP pthreads api and trying to make parallel calls and expecting same response time or less.But total time on average is around 25 seconds
Here is the code
<?php
$g_request_arr = array(
'0' => array(
'request_url' => 'https://www.google.co.uk/?#q=56%2B12'
),
..
..
..
'49'=>array(
'request_url' => 'https://www.google.co.uk/?#q=256%2B132'
)
);
class ChildThread extends Thread {
public function __construct($urls) {
$this->data = $urls;
}
public function run(){
foreach($this->data as $url_info ){
$url = $url_info['request_url'];
file_get_contents($url);
}
$this->synchronized(function($thread){
$thread->notify();
}, $this);
}
}
$thread = new ChildThread($g_request_arr);
$thread->start();
$thread->synchronized(function($thread){
}, $thread);
?>
I want to know what is missing in above code or is it possible to bring the response under 7 seconds.
You are requesting all the data in one thread, here's a better approach:
<?php
class WebRequest extends Stackable {
public $request_url;
public $response_body;
public function __construct($request_url) {
$this->request_url = $request_url;
}
public function run(){
$this->response_body = file_get_contents(
$this->request_url);
}
}
class WebWorker extends Worker {
public function run(){}
}
$list = array(
new WebRequest("http://google.com"),
new WebRequest("http://www.php.net")
);
$max = 8;
$threads = array();
$start = microtime(true);
/* start some workers */
while (#$thread++<$max) {
$threads[$thread] = new WebWorker();
$threads[$thread]->start();
}
/* stack the jobs onto workers */
foreach ($list as $job) {
$threads[array_rand($threads)]->stack(
$job);
}
/* wait for completion */
foreach ($threads as $thread) {
$thread->shutdown();
}
$time = microtime(true) - $start;
/* tell you all about it */
printf("Fetched %d responses in %.3f seconds\n", count($list), $time);
$length = 0;
foreach ($list as $listed) {
$length += strlen($listed["response_body"]);
}
printf("Total of %d bytes\n", $length);
?>
This uses multiple workers, which you can adjust by changing $max. There's not much point in creating 1000 threads if you have 1000 requests to process.

How to remember 10 last read articles with timestamp in session for a user in Codeigniter?

I would like to make a PHP if condition code that will check if the last 10 articles or 10 minutes from the article reading by the user have already elapsed.
E.g.
A user open a page with id = 235 (this id value is in the url localhost/article/235 )
and this id value will be saved in session with a current timestamp and maybe his IP address
Then he read another article and the same will happen.
I need to remember the clicked stuff for another ten clicks and then reset that only for the first row. E.g. after the 10th click the id and timestamp will not became 11th row but will replace the 1st row in the list.
The php condition in CodeIgniter will then check these values and will update the article hit counter value in the articles table and column counter like this:
$this->db->where('id', $id);
$this->db->set('counter', 'counter+1', FALSE);
$this->db->update('articles');
But before calling this code I need to make this check from the session?
How to do that?
I think storing e.g. 10 entries in the session with timestamps per user will be enough.
Just don't save the same page in the session twice.
And the condition will check the current timestamp with the saved one and if it is more than e.g. 10 minutes or the user have read/clicked another 10 articles it will allow the update counter php code.
I don't need to have this bulletproof. Just to disable the increment using browser's refresh button.
So, if he wants to increment the counter he will need to wait ten minutes or read another 10 articles ;)
You should definitely go for Sessions. It saves you bandwidth consumption and is much easier to handle. Unless, of course, you need the data on the client-side, which, by your explanation, I assume you don't. Assuming you went for sessions, all you gotta do is store an array with the data you have. The following code should do it:
$aClicks = $this->session
->userdata('article_clicks');
// Initialize the array, if it's not already initialized
if ($aClicks == false) {
$aClicks = array();
}
// Now, we clean our array for the articles that have been clicked longer than
// 10 minutes ago.
$aClicks = array_filter(
$aClicks,
function($click) {
return (time() - $click['time']) < 600; // Less than 10 minutes elapsed
}
);
// We check if the article clicked is already in the list
$found = false;
foreach ($aClicks as $click) {
if ($click['article'] === $id) { // Assuming $id holds the article id
$found = true;
break;
}
}
// If it's not, we add it
if (!$found) {
$aClicks[] = array(
'article' => $id, // Assuming $id holds the article id
'time' => time()
);
}
// Store the clicks back to the session
$this->session
->set_userdata('article_clicks', $aClicks);
// If we meet all conditions
if (count($aClicks) < 10) {
// Do something
}
I assumne that $clicks is an array with up to ten visited articles. The id is used as key and the timestamp as value. $id is the id of the new article.
$clicks = $this->session->userdata('article_clicks');
//default value
$clicks = ($clicks)? $clicks : array();
//could be loaded from config
$maxItemCount = 10;
$timwToLive= 600;
//helpers
$time = time();
$deadline = $time - $timeToLive;
//add if not in list
if(! isset($clicks[$id]) ){
$clicks[$id] = $time;
}
//remove old values
$clicks = array_filter($clicks, function($value){ $value >= $deadline;});
//sort newest to oldest
arsort($clicks);
//limit items, oldest will be removed first because we sorted the array
$clicks = array_slice($clicks, 0, $maxItemCount);
//save to session
$this->session->>set_userdata('article_clicks',$clicks)
Usage:
//print how mch time has passed since the last visit
if(isset($clicks[$id]){
echo "visited ".($time-$clicks[$id]). "seconds ago." ;
} else {
echo "first visit";
}
EDIT: you have to use arsort not rsort or the keys will be lost, sorry
Based on Raphael_ code and your question you can try this:
<?php
$aClicks = $this->session
->userdata('article_clicks');
$nextId = $this->session->userdata('nextId');
// Initialize the array, if it's not already initialized
if ($aClicks == false) {
$aClicks = array();
$nextId = 0;
}
// Now, we clean our array for the articles that have been clicked longer than
// 10 minutes ago.
$aClicks = array_filter($aClicks, function($click) {
return (time() - $click['time']) < 600; // Less than 10 minutes elapsed
}
);
// We check if the article clicked is already in the list
$found = false;
foreach ($aClicks as $click) {
if ($click['article'] === $id) { // Assuming $id holds the article id
$found = true;
break;
}
}
// If it's not, we add it
if (!$found) {
$aClicks[$nextId] = array(
'article' => $id, // Assuming $id holds the article id
'time' => time()
);
$nextId++;
$this->session->set_userdata('nextId', $nextId);
}
$this->session->set_userdata('article_clicks', $aClicks);
if (count($aClicks) > 10 && $nextId > 9) {
$this->session->set_userdata('nextId', 0);
echo "OK!";
}
?>
I hope I understood correctly what you need.
Usage:
$this->load->library('click');
$this->click->add($id, time());
The class API is very simple and the code is commented. You can also check if an item expired(), if exists() and you can get() item saved time.
Remember that:
Each item will expire after 10 minutes (see $ttl)
Only 10 items are saved in session (see $max_entries)
class Click
{
/**
* CI instance
* #var object
*/
private $CI;
/**
* Click data holder
* #var array
*/
protected $clicks = array();
/**
* Time until an entry will expire
* #var int
*/
protected $ttl = 600;
/**
* How much entries do we store ?
* #var int
*/
protected $max_entries = 10;
// -------------------------------------------------------------------------
public function __construct()
{
$this->CI =& get_instance();
if (!class_exists('CI_Session')) {
$this->CI->load->library('session');
}
// load existing data from user's session
$this->fetch();
}
// -------------------------------------------------------------------------
/**
* Add a new page
*
* #access public
* #param int $id Page ID
* #param int $time Added time (optional)
* #return bool
*/
public function add($id, $time = null)
{
// If page ID does not exist and limit has been reached, stop here
if (!$this->exist($id) AND (count($this->clicks) == $this->max_entries)) {
return false;
}
$time = !is_null($time) ? $time : time();
if ($this->expired($id)) {
$this->clicks[$id] = $time;
return true;
}
return false;
}
/**
* Get specified page ID data
*
* #access public
* #param int $id Page ID
* #return int|bool Added time or `false` on error
*/
public function get($id)
{
return ($this->exist($id)) ? $this->clicks[$id] : false;
}
/**
* Check if specified page ID exists
*
* #access public
* #param int $id Page ID
* #return bool
*/
public function exist($id)
{
return isset($this->clicks[$id]);
}
/**
* Check if specified page ID expired
*
* #access public
* #param int $id Page ID
* #return bool
*/
public function expired($id)
{
// id does not exist, return `true` so it can added
if (!$this->exist($id)) {
return true;
}
return ((time() - $this->clicks[$id]) >= $this->ttl) ? true : false;
}
/**
* Store current clicks data in session
*
* #access public
* #return object Click
*/
public function save()
{
$this->CI->session->set_userdata('article_clicks', serialize($this->clicks));
return $this;
}
/**
* Load data from user's session
*
* #access public
* #return object Click
*/
public function fetch()
{
if ($data = $this->CI->session->userdata('article_clicks')) {
$this->clicks = unserialize($data);
}
return $this;
}
public function __destruct()
{
$this->save();
}
}
You could easily wrap that into a class of it's own that serializes the information into a string and that is able to manipulate the data, e.g. to add another value while taking care to cap at the maximum of ten elements.
A potential usage could look like, let's assume the cookie last would contain 256 at start:
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(10), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(20), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(30), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(40), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(50), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(60), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(70), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(80), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(90), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(100), "\n";
And the output (Demo):
10,256
20,10,256
30,20,10,256
40,30,20,10,256
50,40,30,20,10,256
60,50,40,30,20,10,256
70,60,50,40,30,20,10,256
80,70,60,50,40,30,20,10,256
90,80,70,60,50,40,30,20,10,256
100,90,80,70,60,50,40,30,20,10
A rough implementation of that:
class StringQueue implements Countable
{
private $size = 10;
private $separator = ',';
private $values;
public function __construct($string) {
$this->values = $this->parseString($string);
}
private function parseString($string) {
$values = explode($this->separator, $string, $this->size + 1);
if (isset($values[$this->size])) {
unset($values[$this->size]);
}
return $values;
}
public function add($value) {
$this->values = $this->parseString($value . $this->separator . $this);
return $this;
}
public function __toString() {
return implode(',', $this->values);
}
public function count() {
return count($this->values);
}
}
It's just some basic string operations, here with implode and explode.

Categories