Find if featured image on page has changed - php

While looking into this question I came up with the following solution that is called from canDelete() in an extension to File:
protected function isFileInUse()
{
$owner = $this->getOwner();
$dataObjectSubClasses = ClassInfo::subclassesFor('DataObject');
$classesWithFileHasOne = [];
foreach ($dataObjectSubClasses as $subClass) {
$hasOnes = array_flip($subClass::create()->hasOne());
if (array_key_exists($owner->class, $hasOnes)) {
$classesWithFileHasOne[$subClass] = $hasOnes[$owner->class];
}
}
$threshold = (Director::get_current_page()->class == 'AssetAdmin') ? 1 : 2;
$uses = 0;
foreach ($classesWithFileHasOne as $class => $relation) {
$uses += count($class::get()->filter("{$relation}ID", $this->owner->ID));
if ($uses >= $threshold) {
return true;
}
}
return false;
}
There is one edge case I can't get around though. If, say, a featured image is changed on a blog post then if there is exactly one other use of the same image then with this approach it will still allow it to be deleted. This is because until the page is saved the current change doesn't count towards uses of the image.
The threshold is set differently in CMS Pages and the Media Manager to allow an image to be deleted from within the page that is using it.
Is there a way that I can access the containing page (or other element - we're using Elemental) from within my File extension to see if its associated image has changed?

This is the solution I eventually came up with. I'm not entirely happy with having to inspect the request but couldn't see any other solution:
public function canDelete($member = null)
{
return !$this->isFileInUse();
}
/**
* Check if the file is in use anywhere on the site
* #return bool True if the file is in use
*/
protected function isFileInUse()
{
$owner = $this->getOwner();
$dataObjectSubClasses = ClassInfo::subclassesFor('DataObject');
$classesWithFileHasOne = [];
foreach ($dataObjectSubClasses as $subClass) {
$hasOnes = array_flip($subClass::create()->hasOne());
if (array_key_exists($owner->class, $hasOnes)) {
$classesWithFileHasOne[$subClass] = $hasOnes[$owner->class];
}
}
$threshold = ($this->isAssetAdmin() || ($this->isFileAttach($classesWithFileHasOne))) ? 1 : 2;
$uses = 0;
foreach ($classesWithFileHasOne as $class => $relation) {
$uses += count($class::get()->filter("{$relation}ID", $this->owner->ID));
if ($uses >= $threshold) {
return true;
}
}
return false;
}
/**
* Are we in the asset manager rather than editing a Page or Element?
* #return bool
*/
protected function isAssetAdmin()
{
return 'AssetAdmin' === Director::get_current_page()->class;
}
/**
* Is the current action attaching a file to a field that we're interested in?
* #param array $classesWithFileHasOne Classes with a relationship we're interested in and the name of the
* relevant field
* #return bool
*/
protected function isFileAttach($classesWithFileHasOne)
{
$controller = Controller::curr();
$field = $controller->request->allParams()['FieldName'];
return (preg_match('/attach$/', $controller->requestParams['url']) &&
($controller->action == 'EditForm')
&& (in_array($field, array_values($classesWithFileHasOne))));
}

Related

Drupal 8 custom module not showing up in block layout

I have a custom block module for Drupal 8. It is working on my localhost version of drupal (version 8.7.8). When I upload it to the web server (Version 8.7.11), I can enable the module, but it doesn't show up when I try to place the block on the block layout page. I don't have much control of the web server - files are uploaded via a git repository, but other modules I've added work without issues.
My module is just 2 files:
modules/custom/ischool_section_title_level_two/ischool_section_title_level_two.info.yml
name: iSchool Section Title Level Two
description: Provides a block that shows the Level Two title, or Level One if there is no Level Two.
core: 8.x
package: Custom
dependencies:
- block
type: module
modules/custom/ischool_section_title_level_two/src/plugin/block/iSchoolSectionTitlelevel_two.php
<?php
namespace Drupal\ischool_section_title_level_two\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Provides a block that shows the Level Two section title, or Level One title if there is no level Two
*
* #Block(
* id = "ischool_section_title_level_two",
* admin_label = #Translation("iSchool Section Title Level Two"),
* category = #Translation("Custom"),
* context_definitions = {
* "node" = #ContextDefinition("entity:node", label = #Translation("Node"))
* }
* )
*/
//code adapted from http://hussainweb.me/an-easier-way-to-get-the-current-node-in-a-block-plugin-in-drupal-8/
//and https://design.briarmoon.ca/tutorials/drupal-8/getting-the-parent-node-of-a-drupal-8-node
class iSchoolSectionTitlelevel_two extends BlockBase {
public function build() {
$node = $this->getContextValue('node');
if (empty($node)) {
return [
'#markup' => "",
];
}
$L1_Title = $node->getTitle();
$L2_Title = $node->getTitle();
$currentNode = $node;
while (true) {
$parent_node = $this->getParentNode($currentNode);
if (empty($parent_node)){
break;
}
$L2_Title = $L1_Title;
$L1_Title = $parent_node->getTitle();
$currentNode = $parent_node;
}
return [
'#markup' => $L2_Title,
];
}
private function getParentNode($node){
if (empty($node)) return null;
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
$links = $menu_link_manager->loadLinksByRoute('entity.node.canonical', ['node' => $node->id()]);
// Because loadLinksByRoute() returns an array keyed by a complex id
// it is simplest to just get the first result by using array_pop().
/** #var \Drupal\Core\Menu\MenuLinkInterface $link */
$link = array_pop($links);
if (empty($link)) return null;
/** #var \Drupal\Core\Menu\MenuLinkInterface $parent */
if ($link->getParent() && $parent = $menu_link_manager->createInstance($link->getParent())) {
if (!method_exists($parent, "getUrlObject")) return null;
$urlObj = $parent->getUrlObject();
if (is_null($urlObj)) return null;
if (!method_exists($urlObj, "getRouteParameters")) return null;
$route = $urlObj->getRouteParameters();
if (empty($route)) return null;
if (!isset($route['node'])) return null;
$parent_node = \Drupal::entityManager()->getStorage('node')->load($route['node']);
return $parent_node;
}
else return null;
}
// cache this block for a definite time.
public function getCacheMaxAge() {
return 43200;
}
}
This was an issue with the capitalization of the folders.
The 2nd file should have been in the /src/Plugin/Block/ folder but instead was in the /src/plugin/block/ folder (missing the initial caps).
On the local windows machine, this didn't make any difference. On the LAMP stack machine it resulted in the block not showing.

CodeIgniter DataMapper crash

I inherit old cms build on CodeIgniter 2.0.2 with DataMapper ORM 1.8.0 and the website is with ads. When the ads were under a few thousands it was working fine. Now the ads are close to 20 thousands and it just crash. And there is not error log. The problem is on homepage and sometimes on ad page. I can't figure it out why. I had to edit this function located in "./application/libraries/datamapper.php" so it won't crash.
/**
* Process Query
*
* Converts a query result into an array of objects.
* Also updates this object
*
* #ignore
* #param CI_DB_result $query
*/
protected function _process_query($query)
{
if ($query->num_rows() > 0)
{
// Populate all with records as objects
$this->all = array();
$this->_to_object($this, $query->row());
// don't bother recreating the first item.
$index = ($this->all_array_uses_ids && isset($this->id)) ? $this->id : 0;
$this->all[$index] = $this->get_clone();
if($query->num_rows() > 1)
{
$model = get_class($this);
$first = TRUE;
foreach ($query->result() as $key =>$row)
{
if($first)
{
$first = FALSE;
continue;
}
$item = new $model();
$this->_to_object($item, $row);
if($this->all_array_uses_ids && isset($item->id))
{
$this->all[$item->id] = $item;
} //HERE IS THE EDIT
else if($key < 12)
{
$this->all[] = $item;
}
}
}
// remove instantiations
$this->_instantiations = NULL;
// free large queries
if($query->num_rows() > $this->free_result_threshold)
{
$query->free_result();
}
}
else
{
// Refresh stored values is called by _to_object normally
$this->_refresh_stored_values();
}
}
On line "HERE IS THE EDIT" , I add if the key is less than 12 to add then to $this->all . If I put more than 12 , it crashes.
My question is how can I overcome this? Has anyone come a cross this? On the server I use php 7 and etc. It is as modern server.
I set:
public $free_result_threshold = 50;
public $production_cache = FALSE; //I turn it on but it didn't help
Any idea would be helpful.

Store forms in session and back button

I'm trying to achieve the following scenario:
1. user display the page addBook.php
2. user starts filling the form
3. but when he wants to select the book Author from the Author combo box, the Author is not yet created in the database so the user clicks a link to add a new Author
5. user is redirected to addAuthor.php
6. the user fill the form and when he submits it, he goes back to addBook.php with all the previous data already present and the new Author selected.
The things is: I have scenarios where there is more than one level of recursion. (Example: Add Book => Add Author => Add Country)
How can I do that?
At step #3, the link submit the form so that I can save it in session.
To handle recursion, I can use a Stack and push the current from on the Stack each time I click a link. And pop the last form of the Stack when the user completes the action correctly or click a cancel button.
My problem is:
How can I handle the back button of the browser?
If instead of clicking the "cancel" button, the user click on the back button, how could I kown that I need to pop the last element?
Do you known some common pattern to achieve that?
You must use javascript on the client and hook into the window unload event, serialize the form and send the answer to the server, which saves it in the session.
$(window).unload(function() {
$.ajax({
url : 'autosave.php',
data : $('#my_form').serialize()
});
});
on server
// autosave.php
$_SESSION['autosave_data'] = $_POST['autosave_data'];
// addbook.php
if (isset($_SESSION['autosave_data'])) {
// populate the fields
}
This is the solution I developed to answer my problem.
As the problem was not a client side problem but truly a server side one. Following the php classes I used in my project:
First the main class of the stack functionality. The inclusion need to be done before the session_start as the object will be stored in the session
class Stack {
private $stack;
private $currentPosition;
private $comeFromCancelledAction = false;
public function __construct() {
$this->clear();
}
/* ----------------------------------------------------- */
/* PUBLICS METHODS */
/* ----------------------------------------------------- */
/**
* Clear the stack history
*/
public function clear() {
$this->stack = array();
$this->currentPosition = -1;
}
/**
* get the current position of the stack
*/
public function getCurrentPosition() {
return $this->currentPosition;
}
/**
* Add a new element on the stack
* Increment the current position
*
* #param $url the url to add on the stack
* #param $data optionnal, the data that could be stored with this $url
*/
public function add($url, &$data = array()) {
if (count($this->stack) != $this->currentPosition) {
// the currentPosition is not the top of the stack
// need to slice the array to discard dirty urls
$this->stack = array_slice($this->stack, 0, $this->currentPosition+1);
}
$this->currentPosition++;
$this->stack[] = array('url' => $url, 'data' => $data, 'previousData' => null, 'linked_data' => null);
}
/**
* Add the stack position parameter in the URL and do a redirect
* Exit the current script.
*/
public function redirect() {
header('location:'.$this->addStackParam($this->getUrl($this->currentPosition)), 301);
exit;
}
/**
* get the URL of a given position
* return null if the position is not valid
*/
public function getUrl($position) {
if (isset($this->stack[$position])) {
return $this->stack[$position]['url'];
} else {
return null;
}
}
/**
* get the Data of a given position
* return a reference of the data
*/
public function &getData($position) {
if (isset($this->stack[$position])) {
return $this->stack[$position]['data'];
} else {
return null;
}
}
/**
* Update the context of the current position
*/
public function storeCurrentData(&$data) {
$this->stack[$this->currentPosition]['data'] = $data;
}
/**
* store some data that need to be fixed in sub flow
* (for example the id of the parent object)
*/
public function storeLinkedData($data) {
$this->stack[$this->currentPosition]['linked_data'] = $data;
}
/**
* Update the context of the current position
*/
public function storePreviousData(&$data) {
$this->stack[$this->currentPosition]['previousData'] = $data;
}
/**
* Compute all linked data for every positions before the current one and return an array
* containing all keys / values
* Should be called in sub flow to fixed some data.
*
* Example: if you have tree pages: dad.php, mum.php and child.php
* when creating a "child" object from a "dad", the dad_id should be fixed
* but when creating a "child" object from a "mum", the mum_id should be fixed and a combo for choosing a dad should be displayed
*/
public function getLinkedData() {
$totalLinkedData = array();
for($i = 0; $i < $this->currentPosition; $i++) {
$linkedData = $this->stack[$i]['linked_data'];
if ($linkedData != null && count($linkedData) > 0) {
foreach($linkedData as $key => $value) {
$totalLinkedData[$key] = $value;
}
}
}
return $totalLinkedData;
}
/**
* Main method of the Stack class.
* Should be called on each page before any output as this method should do redirects.
*
* #param $handler StackHandler object that will be called at each step of the stack process
* Let the caller to be notified when something appens.
* #return the data
*/
public function initialise(StackHandler $handler) {
if (!isset($_GET['stack']) || !ctype_digit($_GET['stack'])) {
// no stack info, acces the page directly
$this->clear();
$this->add($this->getCurrentUrl()); //add the ?stack=<position number>
$this->storeLinkedData($handler->getLinkedData());
$this->redirect(); //do a redirect to the same page
} else {
// $_GET['stack'] is set and is a number
$position = $_GET['stack'];
if ($this->currentPosition == $position) {
// ok the user stay on the same page
// or just comme from the redirection
if (!empty($_POST['action'])) {
// user submit a form and need to do an action
if ($_POST['action'] == 'cancel') {
$currentData = array_pop($this->stack);
$this->currentPosition--;
$handler->onCancel($currentData);
// redirect to the next page with ?stack=<current position + 1>
$this->redirect();
} else {
// store the action for future use
$this->stack[$this->currentPosition]['action'] = $_POST['action'];
$currentData = $this->getData($this->currentPosition);
list($currentData, $nextUrl) = $handler->onAction($currentData, $_POST['action']);
// store current form for future use
$this->storeCurrentData($currentData);
// add the new page on the stack
$this->add($nextUrl);
// redirect to the next page with ?stack=<current position + 1>
$this->redirect();
}
} else if (isset($this->stack[$this->currentPosition]['action'])) {
// no action, and an action exists for this position
$currentData = $this->getData($this->currentPosition);
$action = $this->stack[$this->currentPosition]['action'];
if ($this->comeFromCancelledAction) {
//we return from a cancelled action
$currentData = $handler->onReturningFromCancelledAction($action, $currentData);
$this->comeFromCancelledAction = false;
} else {
$previousData = $this->getPreviousData();
if ($previousData != null) {
//we return from a sucessful action
$currentData = $handler->onReturningFromSuccesAction($action, $currentData, $previousData);
$this->resetPreviousData();
}
}
$this->storeCurrentData( $currentData );
}
$currentData = $this->getData($this->currentPosition);
if ($currentData == null) {
$currentData = $handler->getInitialData();
$this->storeCurrentData( $currentData );
}
return $currentData;
} else if ($this->getUrl($position) == $this->getCurrentUrl()) {
// seems that the user pressed the back or next button of the browser
// set the current position
$this->currentPosition = $position;
return $this->getData($position);
} else {
// the position does not exist or the url is incorrect
// redirect to the last known position
$this->redirect();
}
}
}
/**
* call this method after completing an action and need to redirect to the previous page.
* If you need to give some data to the previous action, use $dataForPreviousAction
*/
public function finishAction($dataForPreviousAction = null) {
$pop = array_pop($this->stack);
$this->currentPosition--;
$this->storePreviousData($dataForPreviousAction);
$this->redirect();
}
/* ----------------------------------------------------- */
/* PRIVATE METHODS */
/* ----------------------------------------------------- */
/**
* get the previous data for the current position
* used when a sub flow finish an action to give some data to the parent flow
*/
private function &getPreviousData() {
if (isset($this->stack[$this->currentPosition])) {
return $this->stack[$this->currentPosition]['previousData'];
} else {
return null;
}
}
/**
* get the current url without the stack parameter
*
* Attention: this method calls "basename" on PHP_SELF do strip the folder structure
* and assume that every pages are in the same directory.
*
* The "stack" parameter is removed from the query string
*
* Example: for the page "http://myserver.com/path/to/a.php?id=1&stack=2"
* PHP_SELF will be: /path/to/a.php
* QUERY_STRING wille be: id=1&stack=2
* This method will return: "a.php?id=1"
*/
private function getCurrentUrl() {
$basename = basename($_SERVER['PHP_SELF']);
if ($_SERVER['QUERY_STRING'] != '') {
return $basename.$this->removeQueryStringKey('?'.$_SERVER['QUERY_STRING'], 'stack');
} else {
return $basename;
}
}
/**
* add the "stack" parameter in an url
*/
private function addStackParam($url) {
return $url . (strpos($url, '?') === false ? '?' : '&') . 'stack=' . $this->currentPosition;
}
/**
* Usefull private method to remove a key=value from a query string.
*/
private function removeQueryStringKey($url, $key) {
$url = preg_replace('/(?:&|(\?))'.$key.'=[^&]*(?(1)&|)?/i', "$1", $url);
return $url != '?' ? $url : '';
}
/**
* reset the previous data so that the data are not used twice
*/
private function resetPreviousData() {
$this->stack[$this->currentPosition]['previousData'] = null;
}
}
Then define the abstract StackHandler class
abstract class StackHandler {
/**
* return the initial data to store for this current page
*/
public function &getInitialData() {
return null;
}
/**
* return an array containing the key/values that need to be fixed in sub flows
*/
public function getLinkedData() {
return null;
}
/**
* user ask to go to a sub page
*/
public function onAction(&$currentData, $action) {
$currentData = $_POST;
$nextUrl = $_POST['action'];
return array($currentData, $nextUrl);
}
public function onCancel(&$currentData) {
}
public function onReturningFromCancelledAction($action, &$currentData) {
}
public function onReturningFromSuccesAction($action, &$currentData, $previousData) {
}
}
Then add the following lines at the top of your pages. Adapt the handler it to fit your needs.
// be sure that a stack object exist in the session
if (!isset($_SESSION['stack'])) {
$_SESSION['stack'] = new Stack();
}
$myDad = $_SESSION['stack']->initialise(new DadStackHandler());
class DadStackHandler extends StackHandler {
/**
* return the initial data to store for this current page
*/
public function &getInitialData() {
if(! empty($_GET['id_dad']) && ctype_digit($_GET['id_dad'])){
// update
$myDad = new Dad($_GET['id_dad']);
} else {
// creation
$myDad = new Dad();
}
return $myDad;
}
/**
* return an array containing the key/values that need to be fixed in sub flows
*/
public function getLinkedData() {
$linkedData = array();
if (! empty($_GET['id_dad']) && ctype_digit($_GET['id_dad'])) {
$linkedData['id_dad'] = $_GET['id_dad'];
}
return $linkedData;
}
/**
* user ask to go to a sub page
*/
public function onAction(&$myDad, $action) {
//in order not to loose user inputs, save them in the current data
$myDad->name = $_POST['name'];
$nextUrl = null;
// find the next url based on the action name
if ($action == 'child') {
$nextUrl = 'child.php';
}
return array($myDad, $nextUrl);
}
public function onCancel(&$myDad) {
// probably nothing to do, leave the current data untouched
// or update current data
return $myDad;
}
public function onReturningFromCancelledAction($action, &$myDad) {
// probably nothing to do, leave the current data untouched
// called when returning from child.php
return $myDad;
}
public function onReturningFromSuccesAction($action, &$myDad, $newId) {
// update the id of the foreign field if needed
// or update the current data
// not a good example as in real life child should be a list and not a foreign key
// $myDad->childId = $newId;
$myDad->numberOfChildren++;
return $myDad;
}
}
...
if (user submit form and all input are correct) {
if ($myDad->save()) {
// the user finish an action, so we should redirect him to the previous one
if ($_SESSION['stack']->getCurrentPosition() > 0) {
$_SESSION['stack']->finishAction($myDad->idDad);
} else {
// default redirect, redirect to the same page in view more or redirect to a list page
}
}
}
I hope this could help others.

SimpleBrowser returns empty html

I am using SimpleBrowser that is a part of SimpleTest PHP framework.
The idea is to imitate user interactions with the website and record returned HTML code into a file for further comparison. But something goes wrong here as empty HTML is sometimes returned.
getTransportError() returns Nothing fetched
It happens in completely random places and I can't use back() function because most pages are submitted forms.
require_once('simpletest/browser.php');
class TesterBrowser extends SimpleBrowser
{
/**
* Test the page against the reference. If reference is missing, is it created
* Uses md5 checksum to check if files are identical
*
* #param string $forcename Optional. Substitude autogenerated filename.
* #param boolean $forceRef Optional. Force file to be saved as the reference
*
* #access public
*
* #return void
*/
public function testPage($forcename = "")
{
//who called me?
//$callers=debug_backtrace();
//$whocalledme = $callers[1]['function'];
//get the current source
$html = $this->getContent();
//generate filename
$filename = empty($forcename) ? preg_replace('/[^\w\-'. ''. ']+/u', '-', $this->getUrl()) : $forcename;
$filename .= ".html";
//is there a gauge?
if(file_exists("ref/".$filename) && filesize(dirname(__FILE__)."/ref/".$filename) > 0)
{
//is there a difference
file_put_contents(dirname(__FILE__)."/actual/".$filename, $html);
if(filesize(dirname(__FILE__)."/actual/".$filename) == 0)
{
return false;
}
if(md5_file(dirname(__FILE__)."/actual/".$filename) != md5_file(dirname(__FILE__)."/ref/".$filename))
{
echo $this->getUrl() . " (" . $filename . ") has changed \r\n";
}
}
else
{
file_put_contents(dirname(__FILE__)."/ref/".$filename, $html);
if(filesize(dirname(__FILE__)."/ref/".$filename) == 0)
{
return false;
}
}
return true;
}
/**
* Output the string to the terminal
*
* #param mixed $string String to output
*
* #access public
*
* #return void
*/
public function output($string)
{
echo date("d-m-Y H:i:s") . " - $string... \r\n";
//update date so that it will be the same on every page
exec('date -s "24 JUN 2013 10:00:00"');
}
/**
* Restore the server date using external NTP server
*
* #access public
*
* #return void
*/
public function restoreDate(){
$this->output("Restoring the date&time from NTP server");
exec("ntpdate 0.uk.pool.ntp.org");
exec("hwclock -systohc");
}
}
And the way tests are performed:
class Tester
{
public $browser = null;
const BASEURL = "http://ticketing/";
function __construct(){
$this->browser = new TesterBrowser();
$this->browser->setConnectionTimeout(180);
//get the list of class method to be run
$methods = array();
foreach(get_class_methods($this) as $var)
{
if(0 === strpos($var, 'test')) //they all start with test
{
$methods[] = $var;
}
}
$methods[] = "cleanUp";
//now we need to run these methods
foreach($methods as $m){
while($this->$m() == false){
$this->browser->output("Empty page, trying again");
sleep(5);
}
}
}
//index page
function testGetIndexPage()
{
$this->browser->output("Getting index page");
$this->browser->get(self::BASEURL);
return $this->browser->testPage();
}
//try to enter wrong password
function testWrongPassword()
{
$this->browser->output("Entering wrong credentials");
$this->browser->setField("username", "wrong");
$this->browser->setField("password", "wrong");
$this->browser->clickSubmitByName("submit");
return $this->browser->testPage("wrong-credentials");
}
//Delete ticket though admin
function testDeleteTicketThroughAdmin()
{
$this->browser->output("Deleting the ticket through admin page");
$this->browser->setField("bulk[]", "375341");
$this->browser->setField("bulkaction", "delete");
$this->browser->clickSubmit("Do Action");
return $this->browser->testPage("deleted-ticket-admin");
}
//Restore the date
function cleanUp()
{
$this->browser->restoreDate();
return true;
}
}
$tester = new Tester();
There are of course much more test performed and this is a stripped version.
I have googled a lot about this problem, there seems to be no adequate documentation whatsoever.
Solved. It was a timeout issue although for some reason no adequate error message is implemented.
$this->browser->setConnectionTimeout(180);

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