Forking a running process and returning values

Standard

Using pcntl_fork() allows for forking of a running processing in PHP. In this example I fork my shell script and process data and return the results. This would be useful in a case where you had to make api calls to lets say 4 vendors: amazon, itunes, youtube, flickr. You would assign each child id a task to run. They each would then write there results to a shared memory location for further manipulation. I have written this test in CakePHP shell script.

class ForkShell extends AppShell {

    public function main() {

        $total = 0;
        $data = array();
        $result = array();
        $start = (float) array_sum(explode(' ', microtime()));

        echo "Parent PID: ".getmypid().PHP_EOL;

        $this->forkTest($data);

        // Manipulate the results from the fork
        foreach ($data as $key => $value) {
            $total = $total + $value['number'];
        }
        
        $result['sum_of_digits'] = $total;
        $result['data'] = $data;

        echo "Processing time: ".sprintf("%.4f", ((float) array_sum(explode(' ', microtime())) - $start))." seconds".PHP_EOL;
        print_r(Set::reverse($result));
    }

    public function forkTest(&$data = array()) {

        $pids = array();
        $parent_pid = getmypid();

        for ($i = 0; $i < 4; $i++) {
            if (getmypid() == $parent_pid) {
                $pids[] = pcntl_fork();
                echo "Forking child, now has ".count($pids)." elements".PHP_EOL;
            }
        }

        if (getmypid() == $parent_pid) {
	  		 
            // Process children results as they exit, but not before
            while (count($pids) > 0) {

                echo "Parent id: ".getmypid().PHP_EOL;

                // Wait for child to complete / maintains consistency
                $pid = pcntl_waitpid(-1, $status);
                // Open shared memory block (read only)
                $shm_id = shmop_open($pid, "a", 0, 0);
                // Read data based on chunk size to local variable
                $shm_data = unserialize(shmop_read($shm_id, 0, shmop_size($shm_id)));
                // Delete shared memory block
                shmop_delete($shm_id);
                // Close shared memory block
                shmop_close($shm_id);
                // Merge data to memory location of data
                $data = array_merge($data, $shm_data);
                /* Remove all PID entries created */
                foreach ($pids as $key => $tpid) {
                    if ($pid == $tpid) {
                        unset($pids[$key]);
                    }
                }
            }
            $pids = array();
            
        } else {

            echo "Child id: ".getmypid().PHP_EOL;

            // From here you could do API calls or other calculations and bring the results together
            $pdata = array();
            array_push($pdata, array(
                'child_id' => getmypid(),
                'number' => rand(5, 15)
            ));
            $data_str = serialize($pdata);

            // Open shared memory location
            $shm_id = shmop_open(getmypid(), "c", 0644, strlen($data_str));
            
            // attempt to write to shared memory
            if (!$shm_id) {
                exit("Couldn't create shared memory segment");
            } else if (shmop_write($shm_id, $data_str, 0) != strlen($data_str)) {
                exit("Couldn't write shared memory data");
            }

            exit(0);
        }
    }
}

I would not recommend running this script inside of CakePHP. It may cause issues with the caching.

Converting a json object to an array

Standard

Using restful web services relies on json results. You need your json object to be decoded and then converted to a array. Using the set library from CakePHP we can accomplish this. In this example I send a message to Android push gateway, then attempt to read the result.

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://android.googleapis.com/gcm/send');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Authorization: key= ENTER_YOUR_KEY',
    'Content-Type: application/json'
));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(array(
   'registration_ids' => $registation_ids,
   'data' => array("m" => $message),
)));

$result = curl_exec($ch);
if ($result === FALSE) {
   // do error handling
   debug('curl request failed');
}

// The json decoding / converting line
$data = Set::reverse(json_decode($jsonresult));

The data variable now contains an array of information ready to parse.

Memcached optimization

Standard

Swapping out CakePHP’s default file logging for memcache can greatly improve performance. In my case I use couchbase server to manage NoSQL. From there administrative panel you can see every time your app has a page refresh.

Couchbase Server default bucket

Memcached optimization with CakePHP 2.x

To connect CakePHP with memcached place this in your app/Config/core.php

Cache::config('default', array(
	'engine' => 'Memcache',
	'prefix' => Inflector::slug(APP_DIR) . '_',
	'servers' => array(
			'127.0.0.1:11211' // localhost, default port 11211
		), //[optional]
	'duration' => 7200,
	'serialize' => false,
	'persistent' => true, // [optional] set this to false for non-persistent connections
	'compress' => false,
	'probability' => 100 
	)
);

Your application will now benefit from using memcache instead of a standard logging system. In the event that you make major changes to you application you may need to “flush” the memcache bucket to clear the cached schema stored by CakePHP.

Running shells as repeating cronjobs

Standard

It is common to use shells to handle background tasks on your server. Sometimes that task needs to be executed as quickly as possible. For example, you have a queue of tasks that need to process as soon as entered.

CakePHP can be configured on Unix servers to run a modified shell script via crontab.

Begin by modifying the defaultShell that comes with cake. I have created a new script call secondShell.

#!/bin/bash
TERM=dumb
export TERM
cmd="cake"
while [ $# -ne 0 ]; do
   if [ "$1" = "-cli" ] || [ "$1" = "-console" ]; then
      PATH=$PATH:$2
   shift
   else
      cmd="${cmd} $1"
   fi
   shift
done
COUNT=0
while [ $COUNT -le 30 ]; do
   $cmd
   let COUNT=COUNT+1
   sleep 2;
done

The modifications make this shell run ever 2 seconds for 30 occurrences (2 x 30 = 60 seconds)

To run this shell place it in the crontab by using command “crontab -e” in terminal.

*/1     *       *       *       *       /(your path)/app/Lib/Shells/secondShell YourShell -cli /usr/bin -console /path/to/your/app/Console -app /(your path)/app >/dev/null 2>&1

The */1 tells the scheduler to run the CakePHP Shell script every minute. Learn more about cron job scheduling

Wildcard Subdomain Prerouting

Standard

Ever have a need to have wildcard subdomains on your CakePHP application? This is the solution to that problem. Assume all subdomains have there own template folder, and the subdomains need to be able to be created and modified on the fly.

Create a subdomain model, as well as a table in your database called Subdomains with the following schema

-----------------
MYSQL Subdomain Schema
-----------------
sub_id         int (11)
sub_name       varchar (255)
sub_template   varchar (255)
sub_active     tinyint(1)

Initially, the URL is used to extract the subdomain and the host name from the clients entry point. The contents are then assigned to an array in a custom variable using the internal cache. The bootstrap is called within the dispatcher stage of the MVC architecture as shown below.

MVC Structure

CakePHP MVC Structure

Bootstrap.php

preg_match('/^(?:www\.)?(?:(.+)\.)?(.+\..+)$/i', env('HTTP_HOST'), $urlmatches); 
Configure::write('SubdomainHTTP', array('subdomain' => empty($urlmatches[1]) ? false : $urlmatches[1], 'hostURL' => empty($urlmatches[2]) ? false : $urlmatches[2])); 

The subdomain is now passed through the application and can be accessed by configure::read. A custom library called SubdomainRoute writes values to the cache and the router then reads the results. If a template is set after the SubdomainRoute is initialized it assumes the client is accessing our application on a valid subdomain found in the database.

Routes.php

App::uses('SubdomainRoute', 'Routes');

if (Configure::read('SubdomainHTTP.subdomain')) {
    $subdomainRouting = new SubdomainRoute();
}

if (Configure::read('Subdomain.sub_template’) != NULL) {
    Router::connect('/', array('controller' => ‘pages’, 'action' => ‘index’));
}

The subdomain is determined by accessing the mysql subdomain table. The results are stored and later used for routing (above), as well as assigning a template folder for a particular subdomain (below).

app/Lib/Routes/SubdomainRoute.php

App::uses('Subdomain', 'Model');

class SubdomainRoute extends CakeRoute {

    /**
     * Name of the subdomain to use
     * 
     * @var
     */
    private $subdomain = NULL;

    /**
     * Overrides the routes constructor not to use templates
     */
    public function __construct() {

        $this->subdomain = Configure::read('SubdomainHTTP.subdomain');

        if ($this->subdomain != false) {
            $this->setSubdomain();
        }
    }

    /**
     * Determine subdomain (assoc_id), set to global var
     *
     * @return null
     */
    private function setSubdomain() {

        $Subdomain = new Subdomain();
        $subdomain = $Subdomain->find("first", array(
            'conditions' => array('Subdomain.sub_name' => $this->subdomain, 'Subdomain.sub_active' => '1'),
            'recursive' => -1,
            'fields' => array('Subdomain.sub_name', 'Subdomain.sub_template')
                )
        );

        if (!isset($subdomain['Subdomain']['sub_bus_id'])) {
            throw new BadRequestException('The subdomain specified does not exist.');
        }

        Configure::write('Subdomain', array(
            'sub_name' => $subdomain['Subdomain']['sub_name'],
            'sub_template' => $subdomain['Subdomain']['sub_template']
                )
        );
    }
}

The final step is to tell cake which template the subdomain is associated too. This assumes that you have created the templates folder in the directory.

AppController.php

private function setTheme() {

    $subdomainDetails = Configure::read('Subdomain');
    if (isset($subdomainDetails['sub_template'])) {
        return $this->theme = ucfirst($subdomainDetails['sub_template']);
    }
    return $this->theme = “default”;
}