Archive for 'PHP'

CakePHP Advanced Pagination – sort by derived field

This is my second post on cake. Yesterday I was trying to implement pagination on my cakephp application and got frustrated as it can’t(?) sort by derived fields like SUM(), AVG(). I ransacked for the solution and got many alternate ways. Here is one from Andy Dawson: http://bakery.cakephp.org/articles/view/pagination. He has done a great job. But I wanted to do it by the core pagination method and didn’t feel interested to add a new component as cake has already provided with this feature.

Here comes struggling – I spent a couple of hours to find out a way… and finally got it! yes it is possible to sort by derived fields using the built-in pagination feature of cakephp! Let’s talk about the solution a little later, first I’d like to introduce you with the exact problem I faced.

I was trying to show the list of products in a tabular format with product name and lowest price. The product title is from products table (Product model) and the lowestprice is the MIN of price field from products_of_merchants table(ProductsOfMerchant model). Each Product belongsTo a certain SubCategory and hasMany ProductsOfMerchant selling them on different price(s).

Controller code segment

$this->paginate = array(
	'conditions' => array('Product.sub_category_id' => $subcategory['SubCategory']['id']),
	'fields' => array(
		'Product.id',
		'Product.title',
		'MIN(ProductsOfMerchant.price) AS lowestprice'
	),
	'order' => array('lowestprice' => 'asc'),
	'limit' => 10,
	'group' => 'Product.id'
);
$products = $this->paginate('Product');
$this->set(compact('products'));

It works fine for the first time as default order by lowestprice asc. In the view, I wanted to sort the lowestprice (derived field) column interactively asc/desc.

View code segment

<?php echo $paginator->sort('Lowest Price', 'lowestprice'); ?>

Also tried with

<?php echo $paginator->sort('Lowest Price', 'ProductsOfMerchant.lowestprice'); ?>

I clicked the ‘Lowest Price’ link to sort by desc, no luck! cos it’s a derived field and in debug mode there is no “ORDER BY” clause at the end of the query. I found a lot of ppl looking for the solution of this same issue and here is the solve (+backstage scenes) I’m gonna share with you.

From the cake book I found this Custom Query Pagination.
First I added this function in the Product model for debugging.

Product Model code segment

function paginate($conditions, $fields, $order, $limit, $page = 1, $recursive = null, $extra = array()) {
	print_r(func_get_args()); // for debugging
	$group = $extra['group'];
	return $this->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive', 'group'));
}

Here is the output when I tried to sort by ‘lowestprice’ desc by clicking the ‘Lowest Price’ column header link:

Array
(
    [0] => Array
        (
            [Product.sub_category_id] => 5
        )

    [1] => Array
        (
            [0] => Product.id
            [1] => Product.title
            [2] => MIN(ProductsOfMerchant.price) AS lowestprice
        )

    [2] => Array
        (
        )

    [3] => 10
    [4] => 1
    [5] => 1
    [6] => Array
        (
            [group] => Product.id
        )
)

You can see when you try to sort by a derived field the order by array (array key=2) is empty. This is definitely a bug of the pagination method, but for now we need a patch/fix to get it work and you know it’s never a good idea to modify the core of any framework. The interesting part of the paginate function is it takes an $extra argument which contains an array with ‘group’ key. I added a dummy key-value array to the $paginate array in the controller and wow! it’s automagically appended with the $extra argument of the paginate function in the model. Now all I need is to pass the sort key somehow via the $extra array. This is exactly what I did:

Controller code segment

$this->paginate = array(
	'conditions' => array('Product.sub_category_id' => $subcategory['SubCategory']['id']),
	'fields' => array(
		'Product.id',
		'Product.title',
		'MIN(ProductsOfMerchant.price) AS lowestprice'
	),
	'order' => array('lowestprice' => 'asc'),
	'limit' => 10,
	'group' => 'Product.id',
	'passit' => $this->passedArgs // pass via $extra
);
$products = $this->paginate('Product');
$this->set(compact('products'));

I hope you know what does $this->passedArgs do. If not, no worry, you can see the output of pr($this->passedArgs) from any controller/view.

Here is the patch I applied to the paginate function in the model:

Product Model code segment

function paginate($conditions, $fields, $order, $limit, $page = 1, $recursive = null, $extra = array()) {
	if(empty($order)){
		// great fix!
		$order = array($extra['passit']['sort'] => $extra['passit']['direction']);
	}
	$group = $extra['group'];
	return $this->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive', 'group'));
}

And finally it worked great!

Oh! I forgot, there is a little pain in the view section. I’ve already said this is a patch, the paginate function can’t detect the current sorting direction of the derived fields automatically. The link with the derived field’s sorting column always remains direction:asc no matter you change it manually from the address bar to asc or desc! Here is the fix for view section:

View code segment

<?php
// echo $paginator->sort('Lowest Price', 'lowestprice');
// Use $html->link, not $paginator->sort
echo $html->link(__('Lowest Price', true), array(
	'controller' => 'sub_categories',
	'action' => 'view',
	'page' => $this->passedArgs['page'],
	'sort' => 'lowestprice',
	'direction' => (empty($this->passedArgs['direction']) || $this->passedArgs['direction'] == 'asc')?'desc' : 'asc',
	'limit' => $this->passedArgs['limit']
));
?>

Please turn off debugging by Configure::write(‘debug’, 0) otherwise you may see some annoying warnings and notices of undefined variables. Or you may use isset to check if the vars are set or not and modify the view code accordingly. To me it’s less important and didn’t bother to fix it.

It’s almost perfect now and worked happily ever after.. ;)

If you have any better idea or suggestion please share in the comments.

HAVING clause in CakePHP find query

Recently I’m in love with Cake. Yes it’s true cake is too much addictive. ;) As a novice it’s not that comfortable to start with cakephp framework, cos most of the stuffs from the cake book hardly work perfectly, it’s good they update their code too frequently, but not the cake book. I’ll write several posts on cakephp issues for beginners with solutions in the coming days. Still a lot of ingredients of the cake are mysterious to me, but there is no doubt, I won’t hesitate to give them two thumbs up with 5 star rating :)

Right now I’m working on a price-comparison project using cakephp. This is the first time I’m using cake. I know you’d say why didn’t I try a smaller project first, actually I like to take risk (once my astrologist friend Imon told me this).

Whatever, lets get back to the “title” of this post. One of my queries needed HAVING clause, but did not find any way to use HAVING clause with cakephp find() function. I searched a lot on google, didn’t find anything, even very few people asked for it on popular groups/forums like google/nabble! The SQL query I wanted to run is:

SELECT `Product`.`id` , `Product`.`title` , `Product`.`image_link` , MIN( `ProductsOfMerchant`.`price` ) AS lowest_price, AVG( `ProductReview`.`rating` ) AS average_rating, COUNT( DISTINCT (
`ProductReview`.`id`
) ) AS total_reviews
FROM `products` AS `Product`
LEFT JOIN `products_of_merchants` AS `ProductsOfMerchant` ON ( `ProductsOfMerchant`.`is_active` =1
AND `ProductsOfMerchant`.`product_id` = `Product`.`id` )
LEFT JOIN `product_reviews` AS `ProductReview` ON ( `ProductReview`.`is_approved` =1
AND `ProductReview`.`product_id` = `Product`.`id` )
WHERE `Product`.`is_active` =1
GROUP BY `Product`.`id`
HAVING total_reviews & gt ; =2
ORDER BY `average_rating` DESC
LIMIT 4 

I added the “HAVING” in ‘group’ key of the find() function’s parameters. This is the cakephp code I used and it worked great!

$highRatedProducts = $this->find('all', array(
	'fields' => array('Product.id', 'Product.title', 'Product.image_link', 'MIN(ProductsOfMerchant.price) AS lowest_price', 'AVG(ProductReview.rating) AS average_rating', 'COUNT(DISTINCT(ProductReview.id)) AS total_reviews'),
	'conditions' => array('Product.is_active' => 1),
	'order' => 'average_rating DESC',
	'group' => 'Product.id HAVING total_reviews >= 2',
	'limit' => 4)
);

Please share in comments if you know any better(proper) “Cake way” to accomplish this.

Get Google and Alexa rank of a domain using PHP

There are already a lot of ways to find gpr/alexa rank of a domain using php. But not all of them work on 64bit servers. Here is the code that I always use for finding the google page rank and alexa rank of a website.

Get Google Page rank

function google_page_rank($url) { // URL or domain name
    if (strlen(trim($url))>0) {
        $_url = eregi("http://",$url)? $url:"http://".$url;
        $pagerank = trim(GooglePageRank($_url));
        if (empty($pagerank)) $pagerank = 0;
        return (int)($pagerank);
    }
    return 0;
}

function GooglePageRank($url) {
    $fp = fsockopen("toolbarqueries.google.com", 80, $errno, $errstr, 30);
    if (!$fp) {
        echo "$errstr ($errno)<br />\n";
        } else {
        $out = "GET /search?client=navclient-auto&ch=".CheckHash(HashURL($url))."&features=Rank&q=info:".$url."&num=100&filter=0 HTTP/1.1\r\n";
        $out .= "Host: toolbarqueries.google.com\r\n";
        $out .= "User-Agent: Mozilla/4.0 (compatible; GoogleToolbar 2.0.114-big; Windows XP 5.1)\r\n";
        $out .= "Connection: Close\r\n\r\n";
        fwrite($fp, $out);

        while (!feof($fp)) {
            $data = fgets($fp, 128);
            $pos = strpos($data, "Rank_");
        if($pos === false){} else{
                $pagerank = substr($data, $pos + 9);
            }
        }
        fclose($fp);
        return $pagerank;
    }
}

function StrToNum($Str, $Check, $Magic) {
    $Int32Unit = 4294967296; // 2^32
    $length = strlen($Str);
    for ($i = 0; $i < $length; $i++) {
        $Check *= $Magic;
        if ($Check >= $Int32Unit) {
            $Check = ($Check - $Int32Unit * (int) ($Check / $Int32Unit));
            $Check = ($Check < -2147483648)? ($Check + $Int32Unit) : $Check;
        }
        $Check += ord($Str{$i});
    }
    return $Check;
}

function HashURL($String) {
    $Check1 = StrToNum($String, 0x1505, 0x21);
    $Check2 = StrToNum($String, 0, 0x1003F);
    $Check1 >>= 2;
    $Check1 = (($Check1 >> 4) & 0x3FFFFC0 ) | ($Check1 & 0x3F);
    $Check1 = (($Check1 >> 4) & 0x3FFC00 ) | ($Check1 & 0x3FF);
    $Check1 = (($Check1 >> 4) & 0x3C000 ) | ($Check1 & 0x3FFF);
    $T1 = (((($Check1 & 0x3C0) < < 4) | ($Check1 & 0x3C)) << 2 ) | ($Check2 & 0xF0F );
    $T2 = (((($Check1 & 0xFFFFC000) << 4) | ($Check1 & 0x3C00)) << 0xA) | ($Check2 & 0xF0F0000 );
    return ($T1 | $T2);
}

function CheckHash($Hashnum) {
    $CheckByte = 0;
    $Flag = 0;
    $HashStr = sprintf('%u', $Hashnum) ;
    $length = strlen($HashStr);
    for ($i = $length - 1; $i >= 0; $i --) {
        $Re = $HashStr{$i};
        if (1 === ($Flag % 2)) {
            $Re += $Re;
            $Re = (int)($Re / 10) + ($Re % 10);
        }
        $CheckByte += $Re;
        $Flag ++;
    }
    $CheckByte %= 10;
    if (0!== $CheckByte) {
        $CheckByte = 10 - $CheckByte;
        if (1 === ($Flag % 2) ) {
            if (1 === ($CheckByte % 2)) {
                $CheckByte += 9;
            }
            $CheckByte >>= 1;
        }
    }
    return '7'.$CheckByte.$HashStr;
}

Find Alexa Rank

function alexaRank($domain){
    $remote_url = 'http://data.alexa.com/data?cli=10&dat=snbamz&url='.trim($domain);
    $search_for = '<POPULARITY URL';
    if ($handle = @fopen($remote_url, "r")) {
        while (!feof($handle)) {
            $part .= fread($handle, 100);
            $pos = strpos($part, $search_for);
            if ($pos === false)
            continue;
            else
            break;
        }
        $part .= fread($handle, 100);
        fclose($handle);
    }
    $str = explode($search_for, $part);
    $str = array_shift(explode('"/>', $str[1]));
    $str = explode('TEXT="', $str);

    return $str[1];
}

Hope this will help you. If you know any better(working) solution please let me know via comments.

Problem with resizing corrupted images using PHP image functions

As a programmer we must always think about the exceptional situations. Generally I use my own function to resize uploaded images. It supports jpg, gif, png images with transparency which is “almost” okay.

<?php
// example use
// resizeImage("corrupted_image_1.jpg", "corrupted_image_resized.jpg", 100, 100, "jpg");
function chkImgExt($n){
	$tmp = explode('.', $n);
	$ext = strtolower(array_pop($tmp));
	if($ext == 'jpg' || $ext == 'jpeg' || $ext == 'gif' || $ext == 'png')
		return $ext;
	else
		return false;
}

function resizeImage($src, $dest, $w, $h, $ext){
	$real_path = dirname(__FILE__) . '/';
	$tmpFile = $real_path."tmp_images/".time().'TMP.'.$ext;
	copy($src, $tmpFile); // you may use move_uploaded_file() if the $src is a $_FILES referance
	@chmod($tmpFile, 0777);
	$src = $tmpFile;
	list($width, $height) = @getimagesize($src);
	$new_width = $w;
	$new_height = $h;

	switch($ext){
		case 'jpg':
			$image = imagecreatefromjpeg($src);
			break;
		case 'jpeg':
			$image = imagecreatefromjpeg($src);
			break;
		case 'gif':
			$image = imagecreatefromgif($src);
			break;
		case 'png':
			$image = imagecreatefrompng($src);
			break;
		} 	

	// Resample
	$image_p = @imagecreatetruecolor($new_width, $new_height);
	if ( ($ext == 'gif') || ($ext == 'png') ) {
		$trnprt_indx = imagecolortransparent($image);

		// If we have a specific transparent color
		if ($trnprt_indx >= 0) {

			// Get the original image's transparent color's RGB values
			$trnprt_color = imagecolorsforindex($image, $trnprt_indx);

			// Allocate the same color in the new image resource
			$trnprt_indx = imagecolorallocate($image_p, $trnprt_color['red'], $trnprt_color['green'], $trnprt_color['blue']);

			// Completely fill the background of the new image with allocated color.
			imagefill($image_p, 0, 0, $trnprt_indx);

			// Set the background color for new image to transparent
			imagecolortransparent($image_p, $trnprt_indx);
		}
		// Always make a transparent background color for PNGs that don't have one allocated already
		elseif ($ext == 'png'){
			// Turn off transparency blending (temporarily)
			imagealphablending($image_p, false);

			// Create a new transparent color for image
			$color = imagecolorallocatealpha($image_p, 0, 0, 0, 127);

			// Completely fill the background of the new image with allocated color.
			imagefill($image_p, 0, 0, $color);

			// Restore transparency blending
			imagesavealpha($image_p, true);
		}
	}

	imagecopyresampled($image_p, $image, 0, 0, 0, 0, $new_width, $new_height, $width, $height);

	if(file_exists($dest)){
		@unlink($dest);
	}
	// Output
	switch($ext){
		case 'jpg':
			imagejpeg($image_p, $dest, 100);
			break;
		case 'jpeg':
			imagejpeg($image_p, $dest, 100);
			break;
		case 'gif':
			imagegif($image_p, $dest);
			break;
		case 'png':
			imagepng($image_p, $dest);
			break;
	}

	imagedestroy($image_p);
	unlink($tmpFile);
	return true;
}
?>

But when I tried resizing these 2 images (corrupted_image_1.jpg, corrupted_image_2.jpg) it failed! The error is:

gd-jpeg, libjpeg: recoverable error: Corrupt JPEG data: 9 extraneous bytes before marker 0xd9

I don’t know what does it mean!

First I thought the problem may be due to large file size or GD. I tried with larger file and it worked fine! Then I opened the corrupted images with photoshop and just save as jpg again, and yes it worked. It does not make good sense to me. How a general user will do that? I searched a lot on Google and got a lot of alternative image resizing codes and none of them worked. :(

Now what? Yes I used phpThumb long ago and to me (also most of you) it feels like using a lot of unnecessary codes just for simply resizing a silly image! I can’t believe phpThumb created the thumbnails of both of the corrupted images! yes, using GD! no imagemagick.

I have no idea how phpThumb do it? I never dare to look inside their codes :P
Here is the code using phpThumb to resize the images:

<?php
	require_once('phpthumb/phpthumb.class.php');
	$phpThumb = new phpThumb();
	$capture_raw_data = false; 	$phpThumb->resetObject();
	$phpThumb->setSourceFilename($targetFile); // your source image file
	$output_filename = $tpath; // output file path
	$phpThumb->setParameter('w', 100); // thumbnail width
	$phpThumb->setParameter('q', 100); // thumbnail quality
	$phpThumb->setParameter('config_output_format', 'jpeg'); // preferred thumbnail format 

	if ($phpThumb->GenerateThumbnail()){
		if($phpThumb->RenderToFile($output_filename)){
			// success
		} else {
			$msg = "Error during resizing \n" . $phpThumb->fatalerror . '  ' . $phpThumb->debugmessages;
		}
	} else {
		$msg = "Error with file\n" . $phpThumb->fatalerror . '  ' . $phpThumb->debugmessages;
	}
?>

If you are using your own function for image resizing please check with these 2 files (corrupted_image_1.jpg, corrupted_image_2.jpg). If you see it doesn’t work I would suggest to use phpThumb, it’s free. You should also make sure your web host has the latest php version on the server or go with a php hosting provider, that specializes in it.

Thanks a lot to phpThumb for their amazing work!

How to get currently logged on windows username in PHP and Javascript

It is very simple to get the current windows username in PHP. The following one line of code will output the username of the system where the server is running. If you are running this from localhost then your system login name will be shown. But you can not get the visitor’s system login username.

<?php echo getenv("username"); ?>

By using javascript (actually VBscript) you can get the visitor’s windows username. But there are also limitations. It only works on IE.

<script language="VBscript">
Dim X
set X = createobject("WSCRIPT.Network")
dim U
U=x.UserName
MsgBox "username: " & U
</script>
<script language="Javascript">
var a = U;
alert("Hello, " + a.toString());
</script>

This code will show you the current windows username. But won’t work if run from http://. Open the page from your computer with IE and it’ll work, otherwise not.

Actually it is not possible to get the windows username of your website visitors as it is a security issue. So don’t waste your time if you are trying to do that.

Simplify your query easily

It’s always boring and time consuming writing long INSERT/UPDATE query. Generally when you need to insert a form data with a large number of fields into a single table it does not make any good sense to write the full query manually. For example “INSERT into tbl set name = ‘$_POST[name]‘ , email = ‘$_POST[email]‘ , ….. may be 30 fields! So how can you minimize your effort?

I have written a very simple PHP function for this:

<?php
function genquery($table, $data, $mode = 'insert into', $condition = '', $raw = '') {
	$res = mysql_query("select * from $table limit 1");

	$field_arr = array();
	for($i=0; $i < mysql_num_fields($res); $i++) {
		$field_arr[] = mysql_field_name($res, $i);
	}
	mysql_free_result($res);

	$qstr = $mode." ".$table." set ";
	$arr = array();
	foreach($data as $k => $v) {
		if(!in_array($k, $field_arr)) continue;
		$arr[] = $k." = '".$v."'";
	}
	$qstr .= implode(', ', $arr);

	$qstr .= ' ' . $raw;

	if($mode == 'update' && $condition != ''){
		$qstr .= ' where ' . $condition;
	}
	return $qstr;
}
?>

You must follow a simple way for using this function. The name of the input fields in the form should be same as the fields in the table of your database. I assume your form will be submitted in POST format. In fact a form with many fields is always submitted in post method.

Here is how to use this function:

<?php
include("qfn.php");
if(isset($_POST['submit'])){  // if the form is submitted

$q = genquery("tableName", $_POST, "INSERT INTO", '', '');
mysql_query($q);
}
?>

For updating use:

$q = genquery("tableName", $_POST, "UPDATE", " id = '$_SESSION[id]' ", '');
// the 4th parameter is the condition for update. Use your own condition.

You can use more than one condition like:

$q = genquery("tableName", $_POST, "UPDATE", " id = '$_SESSION[id]' AND uid = '$_SESSION[uid]' ", '');

The 5th parameter is for raw values like

"entry_date = CURDATE(), status = '1' "

You can use the 5th parameter both for INSERT and UPDATE query. But the 4th parameter is only for UPDATE query.

Hope this will help you a lot and save your time & energy.

Download the php file in zip format.

Flickr Uploader Clone: Free Download with source code

I have been searching for flickr uploader clone script online for one of my project. But no luck! There is no such thing on internet. Then I started with analyzing the javascript code of flickr uploader. Lucky that they did not packed their js. Though it was minified by removing white spaces. I used an online javascript code beautifier to make it pretty readable. After that the challenging part began. Cos, the code normally does not work on my local server. After huge hard working of long one week I made it finally. It works great! Even it works for videos too. You can modify the code for uploading any types of file. FYI, Flickr has used YUI (Yahoo User Interface) library for their uploader. So there is no licensing problem if you use this uploader for your own.

For my own need I added 2 extra fields. They are sent to server via POST method along with the FILES. One thing I’ve noticed, it can upload file faster than normal file uploading method. Cos, the file is encoded first via the flash uploader and somehow it is faster than normal.

Upload Multiple File Once

And the great advantage is you can select multiple files during browsing by pressing Ctlr + A or select your files by dragging mouse. By using html <input type=”file” /> you can only select one file at a time which is really so boaring and life-taking when you want to upload hundreds of images.

Flickr YUI Uploader

Here is the online demo. The files are not being saved on my server for my own security!

Download it here completely free in a single zip file!

Modify the index_files/config.js file line no 49,
var _site_root = ‘your script location’;

It won’t work for you until you change _site_root

I have no problem if you use it for your own use. But I do not actually want anyone to put the files/zip on their own site to drive the traffics away from our blog.

If you need any support for modifying or setting it up or any bug report please contact me directly at adnan.eee@gmail.com

Thanks
Have a pain-less uploading experience!