PHP code for handling recursive categories/hierarchical data

I’m just going to paste the code I wrote here… you can read some other tutorial to understand what it’s doing, but for some reason they have really incomplete examples, so here’s the whole shabang. It will even keep your nodes in alphabetical order. It has a function for displaying a drop down list too; wrap it in <select> tags.

<?php
class Category {
    function getParent($id) {
        return mysql_safe_query('SELECT parent.* FROM categories AS child, categories AS parent WHERE parent.id = child.parent_id AND child.id = %s LIMIT 1', $id);
    }

    function getChildren($id, $direct = false) {
        if($direct) {
            return mysql_safe_query('SELECT * FROM categories WHERE parent_id = %s ORDER BY left_val ASC', $id);
        } else {
            return mysql_safe_query('SELECT child.* FROM categories AS child, categories AS parent WHERE child.left_val >= parent.left_val AND child.right_val <= parent.right_val AND parent.id = %s ORDER BY child.left_val ASC', $id);
        }
    }

    function insert($name, $parent_id) {
        $result = mysql_safe_query('SELECT * FROM categories WHERE parent_id <=> %s AND name < %s ORDER BY name DESC LIMIT 1', $parent_id, $name);
        if(mysql_num_rows($result) > 0) {
            // insert between children
            $row = mysql_fetch_assoc($result);
            mysql_safe_query('UPDATE categories SET left_val = left_val + 2 WHERE left_val > %s', $row['right_val']);
            mysql_safe_query('UPDATE categories SET right_val = right_val + 2 WHERE right_val > %s', $row['right_val']);
            mysql_safe_query('INSERT INTO categories (parent_id, left_val, right_val, name) VALUES (%s, %s, %s, %s)',
                $parent_id, $row['right_val'] + 1, $row['right_val'] + 2, $name);
        } else {
            $result = mysql_safe_query('SELECT * FROM categories WHERE id <=> %s LIMIT 1', $parent_id);
            if(mysql_num_rows($result) > 0) {
                // insert first child
                $row = mysql_fetch_assoc($result);
                mysql_safe_query('UPDATE categories SET left_val = left_val + 2 WHERE left_val > %s', $row['left_val']);
                mysql_safe_query('UPDATE categories SET right_val = right_val + 2 WHERE right_val > %s', $row['left_val']);
                mysql_safe_query('INSERT INTO categories (parent_id, left_val, right_val, name) VALUES (%s, %s, %s, %s)',
                    $parent_id, $row['left_val'] + 1, $row['left_val'] + 2, $name);
            } else {
                // insert at beginning of tree
                mysql_safe_query('UPDATE categories SET left_val = left_val + 2, right_val = right_val + 2');
                mysql_safe_query('INSERT INTO categories (parent_id, left_val, right_val, name) VALUES (%s, %s, %s, %s)',
                    null, 1, 2, $name);
            }
        }
    }

    function delete($id) {
        $result = mysql_safe_query('SELECT * FROM categories WHERE id = %s LIMIT 1', $id);
        $row = mysql_fetch_assoc($result);
        mysql_safe_query('DELETE FROM categories WHERE id = %s LIMIT 1', $id);
        mysql_safe_query('UPDATE categories SET parent_id = %s WHERE categories.parent_id = %s', $row['parent_id'], $id); // relink parent
        mysql_safe_query('UPDATE categories SET left_val = left_val - 1, right_val = right_val - 1 WHERE left_val > %s AND right_val < %s', $row['left_val'], $row['right_val']); // update children
        mysql_safe_query('UPDATE categories SET left_val = left_val - 2 WHERE left_val > %s', $row['right_val']); // update left values of nodes to right
        mysql_safe_query('UPDATE categories SET right_val = right_val - 2 WHERE right_val > %s', $row['right_val']); // update right values of encompassing nodes and nodes to right
    }

    function incPostCount($id, $amount=1) {
        $result = mysql_safe_query('SELECT * FROM categories WHERE id = %s', $id);
        $row = mysql_fetch_assoc($result);
        return mysql_safe_query('UPDATE categories SET num_posts = num_posts + %s WHERE left_val <= %s AND right_val >= %s',
            $amount, $row['left_val'], $row['right_val']);
    }

    function decPostCount($id, $amount=1) {
        return self::incPostCount($id, -$amount);
    }

    function getPath($id) {
        $result = mysql_safe_query('SELECT * FROM categories WHERE id = %s', $id);
        $row = mysql_fetch_assoc($result);
        return mysql_safe_query('SELECT * FROM categories WHERE left_val <= %s AND right_val >= %s ORDER BY left_val ASC',
            $row['left_val'], $row['right_val']);
    }

    function printOptions($selected=null, $format=null, $mult=null, $add=null) {
        if(!isset($format)) $format = '<option value="%s" style="padding-left:%spx"%s>%s</option>';
        if(!isset($mult))   $mult = 15;
        if(!isset($add))    $add = 3;
        $depth = 0;
        $result = mysql_safe_query('SELECT * FROM categories ORDER BY categories.left_val ASC');
        while($row = mysql_fetch_assoc($result)) {
            if(isset($last)) {
                if($row['left_val'] == $last['left_val'] + 1) {
                    ++$depth;
                    $next_right = $last_right + 1;
                } elseif($row['left_val'] > $last['right_val'] + 1) {
                    $levels = $row['left_val'] - $last['right_val'] - 1;
                    $depth -= $levels;
                }
            }
            $last = $row;
            if($row['id'] == $selected) $selected_str = ' selected="selected"';
            else $selected_str = '';
            echo sprintf($format, $row['id'], $depth*$mult+$add, $selected_str, $row['name'], $row['num_posts'], $depth);
        }
    }

    function printTree($format=null) {
        if(!isset($format)) $format = '<li><a href="index.php?cat=%s">%s</a> (%s)';
        $depth = 0;
        $result = mysql_safe_query('SELECT * FROM categories ORDER BY categories.left_val ASC');
        echo '<ul>';
        while($row = mysql_fetch_assoc($result)) {
            if(isset($last)) {
                if($row['left_val'] == $last['left_val'] + 1) {
                    echo '<ul>';
                    ++$depth;
                    $next_right = $last_right + 1;
                } elseif($row['left_val'] > $last['right_val'] + 1) {
                    $levels = $row['left_val'] - $last['right_val'] - 1;
                    echo str_repeat('</li></ul></li>', $levels);
                    $depth -= $levels;
                } else {
                    echo '</li>';
                }
            }
            $last = $row;
            echo sprintf($format, $row['id'], $row['name'], $row['num_posts'], $depth);
        }
        echo str_repeat('</li></ul>', $depth+1);
    }

    function printAdminTree() {
        self::printTree('<li><a href="index.php?cat=%s">%s</a> (<a href="category_delete.php?id=%1$s">Delete</a>)');
    }
}

Please leave a comment if you found this useful!

Posted in

Flash Player 10 on Ubuntu 64-bit

There’s a wad of tutorials out there on how to to install this plugin, but I’ve found they either don’t work, or over complicate things.

If you haven’t already found it, download the Flash Player 10 64-bit plugin for linux. Then,

tar xvzf libflashplayer-10.0.22.87.linux-x86_64.so
sudo cp libflashplayer.so /usr/lib/firefox-3.0.10/plugins

You might be running a different version of Firefox by the time you read this tutorial, so bust a “locate firefox | grep plugins” to find the appropriate folder.

Hash/unhash HTML

Here’s a little class I wrote that lets you hash HTML and other things so that you can do some processing on just the text, and then unhash the HTML again.

class ht
{
        static $hashes = array();

        # hashes everything that matches $pattern and saves matches for later unhashing
       function hash($text, $pattern) {
                return preg_replace_callback($pattern, array(self,'push'), $text);
        }

        # hashes all html tags and saves them
       function hash_html($html) {
                return self::hash($html, '`<[^>]+>`');
        }

        # hashes and saves $value, returns key
       function push($value) {
                if(is_array($value)) $value = $value[0];
                static $i = 0;
                $key = "\x05".++$i."\x06";
                self::$hashes[$key] = $value;
                return $key;
        }

        # unhashes all saved values found in $text
       function unhash($text) {
                return str_replace(array_keys(self::$hashes), self::$hashes, $text);
        }

        function get($key) {
                return self::$hashes[$key];
        }

        function clear() {
                self::$hashes = array();
        }
}
Posted in

PHP string ends with

There’s a hundred ways to do this, but here’s the one I came up with. It doesn’t use regex’s so it should be pretty quick.

function ends_with($str, $suffix) {
    return substr($str, -strlen($suffix)) == $suffix;
}

function starts_with($str, $prefix) {
    return substr($str, 0, strlen($prefix)) == $prefix;
}

For completeness, I added the corresponding starts_with function.

Posted in

Random String

Just some functions for generating random strings in PHP.

function randstr($len=8, $chars='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') {
    $i = 0;
    $str = '';
    $randmax = strlen($chars)-1;

    for($i=0; $i<$len; ++$i) {
        $str .= $chars[mt_rand(0,$randmax)];
    }

    return $str;
}

function randkey($len=32) {
    return randstr($len, '`1234567890-=qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./ ~!@#$%^&*()_+{}|:"<>?');
}

randkey is good for generating salt strings and such, randstr is good for stuff like short URLs. Of course, you can pass in whatever characters you want.

Posted in

jQuery Select Text Range

Here’s a jQuery function I wrote which you can use to select a range of text in an input field.

$.fn.selectRange = function(start, end) {
    return this.each(function() {
        if(this.setSelectionRange) {
            this.focus();
            this.setSelectionRange(start, end);
        } else if(this.createTextRange) {
            var range = this.createTextRange();
            range.collapse(true);
            range.moveEnd('character', end);
            range.moveStart('character', start);
            range.select();
        }
    });
};

Simple Rounded Corners with CSS

I know there are umpteen billion tutorials on rounded corners out there, but here’s an easy way that I like to do it. Requires only one image, and allows you to nudge your text as close to the corners are you like.

If you want a 3px radius corner, have a black background, and white foreground, you need to create a 6×6 (i.e. radius*2) image of a white circle on a black background. This method won’t support transparency unfortunately.

The HTML:

<div id="content">
    <div id="tl"></div><div id="tr"></div><div id="bl"></div><div id="br"></div>
    Your content here.
</div>

The CSS:

#content {
    position:relative;
    background-color: #fff;
    width:600px;
    height:300px;
}

#tl,#tr,#bl,#br{
    background: #000 url(images/circle.gif);
    width:3px;
    height:3px;
    position:absolute;
}

#tl{top:0;left:0}
#tr{top:0;right:0;background-position:3px 0}
#bl{bottom:0;left:0;background-position:0 3px}
#br{bottom:0;right:0;background-position:3px 3px}

Adjust all the “3px”s to match your radius, and modify the background attribute to meet your needs, and style content however you want. You probably want to put some padding on there. The important thing is that #content has position:relative so that the corners are aligned to the content box rather than the page.

I used this technique on my personal site, mnbayazit.com.

Vertically center text with CSS

Anyone who has used CSS for awhile knows that vertically aligning stuff isn’t easy. Vertically aligning just text, however, is pretty simple. If your container is 30px tall, just set the line-height to 30px too and your text will be perfectly centered! Hurray! This only works for a single line of text however… if you want more, you’re probably better off fighting with margins, padding, or absolute positioning.

Draw an Unfilled Circle

OpenGL doesn’t seem to have any functions for drawing an unfilled circle, so you can use this code instead. It uses lines, so you can adjust the line thickness with glLineWidth and anti-alias it with glEnable(GL_LINE_SMOOTH) if you want.

void drawCircle(GLfloat x, GLfloat y, GLfloat r) {
    static const double inc = M_PI / 12;
    static const double max = 2 * M_PI;
    glBegin(GL_LINE_LOOP);
    for(double d = 0; d < max; d += inc) {
        glVertex2f(cos(d) * r + x, sin(d) * r + y);
    }
    glEnd();
}

If you want a filled circle, I would suggest you just set glPointSize to whatever size you want, and then plot a single point with glBegin(GL_POINTS) where ever you want your circle. Otherwise, you can use the above code with GL_POLYGON if you want more control. If you want a smoother circle, decrease the size of “inc”.