Updating a record based on the time

At the moment I have a model function:


public function updateStatus() /* Set status to 'closed' for any quotes that have reached their expiry date*/

{

	$criteria=new CDbCriteria;

	$criteria->condition="quote_status < 5";

	$criteria->addCondition("expiry_datetime <= '".date('Y-m-d H:i:s')."'");

	

	$quotes=$this->findAll($criteria);


	foreach($quotes as $quote)

	{

		$quote->quote_status=7;

		$quote->update();

	}

}

I run this function in my controller actions:


$model->updateStatus(); // update quote status

The only problem is of course that the record only gets updated when the user accesses that particular action. There are several other actions that would depend on this method. I can obviously call the method on each and every action but are there any other ways of doing this? For example can it be called each time the model is accessed? Or in the loadModel() controller function?

It’s too resource&time-intensive task to update all models on each page request.

Use cron + CConsoleCommand.

But even with a cron it would have to do the check every minute.

you can build a global controller class, and let every controller implement it, in the global controller class, you should override the init() method, in it you can do what you want to.




<?php

class MyController extends Controller

{

	public function init()

	{

        	//your code

	}

}



But i still think that, andy_s is right that to use cron to do it.

If you can run a job with cron every minute, then it’s a better solution, than do the same on each request.

But isn’t the cron going to perform the same operation that would be performed manually via the app?

Also I just checked my hosting server and it says a CRON job cannot be run more than once an hour maximum.

This case, I would not make the status as a fixed value stored on database.

Instead, I would create a function and a scope to handle this:

quote model:




function isClosed(){

return $this->expiry_datetime <= date('Y-m-d H:i:s');

}



This functions allows to identify which model is expired:




if ($model->isClosed()){ ....



But is not a proper method to make queries. So we need to define an appropriated scope:




function scopes(){

 	return array(

      	'closed' => array('condition' =>"expiry_datetime <= '".date('Y-m-d H:i:s')."' ")

	);

}



Now we can do like below:




$models = quote::model()->closed()->findAll();



That’s a good suggestion, but I’m deep in to the development of this app and don’t really want to change the structure at this stage. Plus it’s easier for me to test the app by altering the status directly in the database (there are 5 status values).

I think I will just call the method on each action and monitor the performance!

I see.

So, I have a workaround for you. Modify status at afterFind:

quote model:




public function afterFind(){


   if ($this->expiry_datetime <= date('Y-m-d H:i:s')){

      $this->quote_status = '7'; 

      $this->update();  // think about this, it can decrease performance.

   }

  	return parent::afterFind();

}



You can create an action that does the cron job (just copy all the code that you should do with the shell app and make a page).

Now you can write a cron task on your server that will call the script on the hosting and doing the update as often as you wish.

I don’t understand. My hosting server only allows a cron job to be run once every hour max.

Anyway is it really that much performance decrease if I did this manually?

I have this problem too with some hosting, that doesn’t allow cron job at all.

The trick is to create a page like "www.mysite/ForbiddenCronJob" that will do the task that we are not allowed to put in cron.

Later in your development server (YOUR server, on wich you can set as you want) you can add a cron task like:

wget www.mysite/ForbiddenCronJob

Every 5 minutes. Like that you can do cron task as you wish.

About performance, in this page you can simply do an exec and terminate, thare is a really slighty difference.

The idea is to use our home server for call task on the host server, where cron is forbidden.

If you’re able to run a cron job once every hour, why not create a script which calls your function every minute 59 times and then exists?

You can write that script in PHP, Perl, Ruby, bash, whatever.;)

I assume for that to work I would have to change the max_execution_time setting in PHP, because the script would be running for 1 hour each time right? Do you have an example do demonstrate?

I usually do this:


shell_exec("nohup $commandString > /dev/null 2> /dev/null & echo $!");

It kicks off a script as a background process.

But if you are able to install a PEAR package like System_Daemon, it would be better:

Create Daemons in PHP

Ha! Thanks for pointing that out. I’ve written my own “daemon” some time ago and totally missed that there’s something in PEAR. That makes me think: How cool would a class CDaemon (extends CConsoleApplication) be? On the other hand, making a class like this cross platform safe is surely a nightmare. :)

I wouldn’t mind if you shared your console runner class.

Would be very useful. Especially for people who cannot install PEAR packages on their host. :)

I am curious how you use it with your Mutex class. I’ve tried to detect if the process was running and then inform my app that it ended, but I just end up with either prematurely killed processes or a never ending poll.

I’ll try it with the daemon class (if I can convince my host to install it). ;)

I needed to monitor some service, so i’ve implemented it as a yiic command. It’s a very simple implementation, nothing special. See for yourself:


 <?php

class DaemonCommmand extends CConsoleCommand

{

    public function run($args)

    {

        // Try to create / lock PID file before proceeding.

        // If this fails, another script is already running.

        $pidfile  = Yii::getPathOfAlias('application.runtime.daemon').'.pid';

        $fp = fopen($pidfile,"a+") or die('Could not create pidfile: '.$pidfile."\n");

        flock($fp,LOCK_EX | LOCK_NB) or die("Could not lock pidfile (other process running?)\n");

        ftruncate($fp,0);

        fwrite($fp,getmypid()) or die ('Could not write pidfile: '.$pidfile."\n");


        // Prepare parameters

        $params = Yii::app()->params;

        $period = $params['monitor.period'];

        $max    = $params['monitor.max'];.

        $forever= $max==0;


        // Continue forever or $max times

        while($forever || ($max--!==0))

        {

            //

            // Monitor logic goes here...

            //


            // Sleep som time

            sleep($period);


            // Flush logs to keep memory footprint small

            Yii::getLogger()->flush();

        }

        flock($fp, LOCK_UN);

        fclose($fp);

    }

}



I can start this "daemon" from an admin page. The code to start is like this:


private function startDaemon()

{

    $started=false;

    $command=Yii::getPathOfAlias('application.yiic').' daemon';

    // echo $! will output the PID

    exec($command.' > /dev/null 2>&1 & echo $!',$output);


    // Wait 1 sec and check that the process is still running

    $pid=isset($output[0]) ? (int) $output[0] : -1;

    sleep(1);

    exec('ps -p '.$pid,$op);


    if (isset($op[1])) {

        YII_DEBUG && Yii::trace('Started daemon script with PID '.$pid,'application.monitor');

        return true;

    } else {

        YII_DEBUG && Yii::trace('Could not start daemon script! Check write permissions.','application.monitor');

        return false;

    }

}

You see, it’s not very sophisticated and misses lots of features. PEAR daemon would have been the better option, i guess.