How to Run PHP Scripts In The Background – Simple Examples

Welcome to a tutorial on how to run PHP scripts in the background. So you are looking for a way to “silently run” a massive PHP script? The bad news is, there are no straightforward ways to run PHP scripts in the background, as it is not designed to do so “by default”. But the good news is, there are possible alternatives.

To run PHP scripts in the background, we can either use the popen() or exec() function to run another PHP script in the command line.

  • For Windows servers – pclose(popen("start /B php PATH/SCRIPT.PHP", "r"));
  • For Linux servers – exec("php PATH/SCRIPT.PHP > /dev/null &");

But of course, this is a simplification of what can be done. Let us walk through an example in this guide – Read on!

ⓘ I have included a zip file with all the source code at the start of this tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.

 

 

REAL QUICK TUTORIAL

 

TABLE OF CONTENTS

Download & NotesBackground PHP Process Control
Useful Bits & Links The End

 

 

DOWNLOAD & NOTES

Firstly, here is the download link to the example code as promised.

 

EXAMPLE CODE DOWNLOAD

Click here to download the source code, I have released it under the MIT license, so feel free to build on top of it or use it in your own project.

 

QUICK NOTES

  • If you only want to run a simple background script, stick with 1a-background.php and 1b-trigger.php.
  • For “advanced” task management, go through all the 2-***.php files.

If you spot a bug, please feel free to comment below. I try to answer questions too, but it is one person versus the entire world… If you need answers urgently, please check out my list of websites to get help with programming.

 

BACKGROUND PHP SCRIPT

All right, let us now begin with the simple version of “how to run PHP in the background”.

 

CREATE A COMMAND LINE SCRIPT

1a-background.php
<?php
// (A) RUN FROM COMMAND-LINE ONLY!
if (php_sapi_name() != "cli") { 
  die("Please run this script from command line"); 
}
 
// (B) THIS DUMMY SCRIPT WILL CREATE A DUMMY TEXT FILE
// BUT YOU DO WHATEVER IS REQUIRED IN YOUR OWN PROJECT...
$handle = fopen(__DIR__.DIRECTORY_SEPARATOR."dummy.txt", "w");
fwrite($handle, "Background process successfully ran at " . date("Y-m-d H:i:s"));
fclose($handle);

This should be self-explanatory, start by creating your “background process script”. This can be anything massive that you want to do – Crunch some numbers, lengthy scans, or generating massive reports. For this simple example, we will just create a dummy.txt file on the server.

 

 

THE TRIGGER SCRIPT

1b-run.php
<?php
// (A) FOR WINDOWS
if (strtoupper(substr(php_uname(), 0, 3)) == "WIN") {
  // start /B php PATH/1a-background.php
  $cmd = "start /B php ". __DIR__ . DIRECTORY_SEPARATOR ."1a-background.php";
  pclose(popen($cmd, "r"));
}
// (B) FOR LINUX
else {
  // php PATH/1a-background.php > /dev/null &
  $cmd = "php ". __DIR__ . DIRECTORY_SEPARATOR ."1a-background.php > /dev/null &";
  exec($cmd); 
}

// (C) DONE!
echo "RUNNING!";

Just how do we run 1a-background.php “in the background”? We create a PHP script that will run another PHP script in the command line. Yes, simply access 1b-run.php and check if the dummy.txt file is generated on the server.

P.S. Make sure that PHP has sufficient access rights to run the commands or this will fail.

 

THE POTENTIAL PROBLEM & PREVENTION

This simple method works, but there is a potential problem. What happens when impatient users reload the trigger script multiple times? That spawns multiple instances of the background script and might crash the server – So “checks and locks” need to be put in place. A simple trick that I use:

  • Generate an empty lock.txt when the background processing starts.
  • Delete lock.txt at the end.
  • In the trigger script, we simply don’t run the background script if the lock exists – if (file_exists("lock.txt)) { die("PLEASE WAIT"); }

 

 

PROCESS CONTROL

The above trick will work, but here’s an addition should you need some “process control”. But take note, this will only work on Windows servers. Linux users, you are going to need some tweaks on your own.

 

BACKGROUND TASKS DATABASE TABLE

2a-database.sql
CREATE TABLE `tasks` (
  `user_id` int(11) NOT NULL,
  `process_id` varchar(12) NOT NULL,
  `task_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

ALTER TABLE `tasks`
  ADD PRIMARY KEY (`user_id`),
  ADD KEY `task_date` (`task_date`);
FieldDescription
user_idPrimary and foreign key, the user running the task.
process_idSystem process ID.
task_dateTime when the task is started.

The idea of this table is to keep track of the number of background processes.

  • We register the process ID when a user runs a background task; Delete the process ID when the task is complete.
  • Only allow one background task per user.
  • Or we can also do a COUNT(*) FROM `tasks` and set a “global limit”.

 

 

PHP BACKGROUND TASKS LIBRARY

2b-bg-core.php
<?php
class Background {
  // (A) CONSTRUCTOR - CONNECT TO DATABASE
  private $pdo = null;
  private $stmt = null;
  public $error = "";
  function __construct () {
    try {
      $this->pdo = new PDO(
        "mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=".DB_CHARSET, 
        DB_USER, DB_PASSWORD, [
          PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
          PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ]
      );
    } catch (Exception $ex) { die($ex->getMessage()); }
  }

  // (B) DESTRUCTOR - CLOSE DATABASE CONNECTION
  function __destruct () {
    if ($this->stmt!==null) { $this->stmt = null; }
    if ($this->pdo!==null) { $this->pdo = null; }
  }

  // (C) START A BACKGROUND SCRIPT
  function start ($uid, $script) {
    // (C1) CHECK IF USER ALREADY HAS A RUNNING PROCESS
    $this->stmt = $this->pdo->prepare("SELECT * FROM `tasks` WHERE `user_id`=?");
    $this->stmt->execute([$uid]);
    $check = $this->stmt->fetch();
    if (is_array($check)) {
      $this->error = "User already has a running process.";
      return false;
    }
    
    // (C2) RUN SCRIPT IN BACKGROUND
    // start /B wmic process call create "PATH/php.exe -f PATH/1a-background.php USER-ID | find "ProcessId"
    $cmd = 'start /B wmic process call create "'.PHP_PATH.' -f '
           .$script.' '.$uid.' | find "ProcessId"';
    $fp = popen($cmd, "r");

    // (C3) GET PROCESS ID
    // NOTE: THIS WORKED ON WINDOWS 10
    // BUT DIFFERENT WINDOW VERSIONS MAY OUTPUT DIFFERENTLY
    // YOU MAY NEED TO TWEAK THIS PART ACCORDINGLY
    $processID = 0;
    while (!feof($fp)) {
      $line = fread($fp, 1024);
      if (strpos($line, "ProcessId") !== false) {
        preg_match('/\d+/',$line, $processID);
        $processID = $processID[0];
      }
    }
    pclose($fp);

    // (C4) REGISTER ENTRY
    $this->stmt = $this->pdo->prepare("INSERT INTO `tasks` (`user_id`, `process_id`) VALUES (?,?)");
    $this->stmt->execute([$uid, $processID]);

    // (C5) DONE
    return true;
  }

  // (D) BACKGROUND SCRIPT HAS ENDED
  function end ($uid) {
    $this->stmt = $this->pdo->prepare("DELETE FROM `tasks` WHERE `user_id`=?");
    $this->stmt->execute([$uid]);
    return true;
  }
  
  // (E) KILL TASK
  function kill ($uid) {
    // (E1) CHECK IF USER HAS PENDING TASK
    $this->stmt = $this->pdo->prepare("SELECT * FROM `tasks` WHERE `user_id`=?");
    $this->stmt->execute([$uid]);
    $task = $this->stmt->fetch();
    if (!is_array($task)) {
      $this->error = "User does not have any pending tasks.";
      return false;
    }

    // (E2) WINDOWS KILL TASK
    pclose(popen("taskkill /PID ".$task['process_id']." /F", "r"));

    // (E3) CLOSE ENTRY
    $this->end($uid);

    // (E4) DONE
    return true;
  }
}

// (F) SETTINGS
// ! CHANGE DATABASE SETTINGS TO YOUR OWN
define('DB_HOST', 'localhost');
define('DB_NAME', 'test');
define('DB_CHARSET', 'utf8');
define('DB_USER', 'root');
define('DB_PASSWORD', '');
// ! CHANGE PHP PATH TO YOUR OWN
define('PHP_PATH', 'c:/xampp/php/php.exe');

// (G) NEW BACKGROUND OBJECT
$BG = new Background();

This is quite intimidating at first, but keep calm and look carefully.

  • On creating a $BG = new Background() object, the constructor will automatically connect to the database; The destructor will close the connection when done.
  • There are only 3 functions in this library!
    • function start() The so-called “step 1” to run a script in the background, register the user ID and process ID in the database; We also do checks, don’t run the script if the user already has a running background task.
    • function end() This should be called when the task is completed. It literally just deletes the database entry.
    • function kill() Manual intervention… Calls the Windows taskkill and ends a script that might potentially be hung.

That’s all, but a quick reminder to change the settings to your own.

 

TRIGGER SCRIPT WITH PROCESS CONTROL

2c-trigger.php
<?php
// (A) DEMO - USER ID 999 WANTS TO EXPORT A DUMMY REPORT
$uid = 999;
$script = __DIR__ . DIRECTORY_SEPARATOR . "2d-background.php"; // NOTE: ABSOLUTE PATH
 
// (B) RUN!
require "2b-bg-core.php";
echo $BG->start($uid, $script)
  ? "RUNNING!"
  : $BG->error ;

Remember the earlier “create a PHP script to run another PHP script”? This is the “upgraded” version that uses the task library – On running the background script, this will also register the user ID and process ID into the database.

 

 

BACKGROUND SCRIPT WITH PROCESS CONTROL

2d-background.php
<?php
// (A) RUN FROM COMMAND-LINE ONLY!
if (php_sapi_name() != "cli") { 
  die("Please run this script from command line"); 
}

// (B) THIS DUMMY SCRIPT WILL CREATE A DUMMY TEXT FILE
// BUT YOU DO WHATEVER IS REQUIRED IN YOUR OWN PROJECT...
$handle = fopen(__DIR__.DIRECTORY_SEPARATOR."dummy.txt", "w");
fwrite($handle, "USER ID: ". $argv[1] ." | RUN AT: ". date("Y-m-d H:i:s"));
fclose($handle);

// FOR TESTING $BG->KILL(), IF YOU WANT THE SCRIPT TO "HANG"
// sleep(99999);

// (C) REMEMBER TO "END" THE TASK IN THE DATABASE
require __DIR__.DIRECTORY_SEPARATOR."2b-bg-core.php";
$BG->end($argv[1]);

Yes, this is pretty much the same as the “simple version”. Except that at the end of the script, we call the library $BG->end() to close the task.

Just one small note to the beginners here – Notice how the user ID is passed in via $argv[1] instead of $_SESSION? Yes, this is a command-line script. There are no cookies nor sessions. If you need to pass variables, modify $BG->start() or just change the table.

 

KILL TASK SCRIPT

2e-kill.php
<?php
// DEMO - KILL USER ID 999 TASK
require "2b-bg-core.php";
echo $BG->kill(999)
  ? "KILLED"
  : $BG->error ;

Bad things do happen… Here is how to clean things up, or to cancel a task halfway.

 

USEFUL BITS & LINKS

That’s all for the code, and here is a small extra that may be useful to you.

 

INFOGRAPHIC CHEAT SHEET

Run PHP Scripts In The Background (Click To Enlarge)

 

THREADING ALTERNATIVES

Of course, the shell script is only one of the many ways to do parallel processing. There are many other packages that you can check out, and here are a few that I find to be pretty interesting:

 

LINKS & REFERENCES

 

THE END

Thank you for reading, and we have come to the end of this guide. I hope that it has helped you with your project, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!

Leave a Comment

Your email address will not be published. Required fields are marked *