MySQL administration class

I implemented this class for administrative MySQL tasks. It is based on MySql and provides additional functionalities like retrieving CREATE and DROP queries for existing tables or the database. These functions are used by to “main methods”: backup() and restore(). backup() writes the whole database (or only a part of it) in a zip file, including the creation queries and a backup info file. restore() can read and interpret these files and regenerates the database structure including the contents. The table backup/restore works with direct file I/O of the MySQL server, so if you have a webspace that has a distributed system of web server and MySQL server, then the web server might not be able to access the generated files (as they are not stored in the local file system of the web server). Simply check it if it works (maybe I will have published a modified version of this class soon to address this possible issue).

Sample source code

$dba = new MySqlAdministration('test', 'root', '', 'localhost');

// Get the name of the database
$r = $dba->getDatabaseName();
print '$db->getDatabaseName() = ' . print_r($r, true) . '<br/>';

// This would be the query to delete the database
$r = $dba->getDatabaseDropQuery('test');
print '$db->getDatabaseDropQuery() = ' . print_r($r, true) . '<br/>';

// This would be the query to delete the table "test"
$r = $dba->getTableDropQuery('test');
print '$db->getTableDropQuery(...) = ' . print_r($r, true) . '<br/>';

// This would be the query to create the database
$r = $dba->getDatabaseCreationQuery();
print '$db->getDatabaseCreationQuery() = ' . print_r($r, true) . '<br/>';

// This would be the query to create all the tables (structure) as they are now
$r = $dba->getTableCreationQuery();
print '$db->getTableCreationQuery() = ' . print_r($r, true) . '<br/>';

// This would be the query to create the table 'test'
$r = $dba->getTableCreationQuery('test');
print '$db->getTableCreationQuery("test") = ' . print_r($r, true) . '<br/>';

// Lock the table, so that we can query multiple requests
$readlock = array('test');
$writelock = array('test2');
$dba->lock($readlock, $writelock);
// ... Here we do some stuff ...
// Unlock the table, this will be automatically done on destruction
$dba->unlock();

// Get table information (this is a method derived from MySql)
$r = $dba->getTableStatus('test');
print '$db->getTableStatus(...) = ' . print_r($r, true) . '<br/>';

// Make a backup of the database in a zip file
$tablePrefix = '';  // We don't have a table prefix here
$tables = null;     // null or array() means ALL tables
$comments = 'This is my backup BLA BLA ...'; // A comment for the info file
$dropTablesBeforeInsert = true; // We drop the tables before we recreate them
$dropAndRecreateDatabase = false; // We do not drop the whole database

$r = $dba->backup($tablePrefix, $tables, $comments, $dropTablesBeforeInsert, $dropAndRecreateDatabase);
print '$db->backup(...) = ' . print_r($r, true) . '<br/>';

// This is the file you can download then
$zipArchive = $r['zipfile']; 

// Restore the database:
$tablePrefix = '';  // We don't change the table prefix, but we could
$r = $dba->restore($zipArchive, $tablePrefix);
print '$db->restore(...) = ' . print_r($r, true) . '<br/>';

// Delete the zip file ...
FileSystem::delete($zipArchive);

Output


$db->getDatabaseName() = test

$db->getDatabaseDropQuery() = DROP DATABASE IF EXISTS `test`;

$db->getTableDropQuery(...) = DROP TABLE IF EXISTS `test`;

$db->getDatabaseCreationQuery() = CREATE DATABASE IF NOT EXISTS `test` /*!40100 DEFAULT CHARACTER SET utf8 */;

$db->getTableCreationQuery() = CREATE TABLE IF NOT EXISTS `test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `rid` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=30 DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `test2` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'The primary key',
  `name` varchar(128) NOT NULL COMMENT 'Name of the person',
  `email` text NOT NULL COMMENT 'email address of the person',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='An example table for emails';

$db->getTableCreationQuery("test") = CREATE TABLE IF NOT EXISTS `test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `rid` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=30 DEFAULT CHARSET=utf8;

$db->getTableStatus(...) = Array (
    {test} => Array (
        {name} => test
        {engine} => MyISAM
        {version} => 10
        {row_format} => Fixed
        {rows} => 0
        {avg_row_length} => 0
        {data_length} => 0
        {max_data_length} => 2533274790395903
        {index_length} => 1024
        {data_free} => 0
        {auto_increment} => 30
        {create_time} => 2010-08-05 16:34:55
        {update_time} => 2010-08-05 16:34:55
        {check_time} =>
        {collation} => utf8_general_ci
        {checksum} =>
        {create_options} =>
        {comment} =>
    )
    {test2} => Array (
        {name} => test2
        {engine} => MyISAM
        {version} => 10
        {row_format} => Dynamic
        {rows} => 0
        {avg_row_length} => 0
        {data_length} => 0
        {max_data_length} => 281474976710655
        {index_length} => 2048
        {data_free} => 0
        {auto_increment} => 1
        {create_time} => 2010-08-05 16:34:55
        {update_time} => 2010-08-05 16:34:55
        {check_time} => 2010-08-05 16:34:55
        {collation} => utf8_general_ci
        {checksum} =>
        {create_options} =>
        {comment} => An example table for emails
    )
)

$db->backup(...) = Array (
    {zipfile} => /Applications/XAMPP/xamppfiles/temp/2010-8-5-MySqlBackup.zip
    {backupinfo} => /mysqlbackup.test.1281022607.create.sql
    {comments} => This is my backup BLA BLA ...
    {time} => 2010-08-05 15:36:47 GMT
    {database} => test
    {prefix} =>
    {create} => /mysqlbackup.test.1281022607.create.sql
    {tables} => Array (
        {test} => Array (
            {name} => test
            {file} => /mysqlbackup.test.1281022607.test.tdata
        )
        {test2} => Array (
            {name} => test2
            {file} => /mysqlbackup.test.1281022607.test2.tdata
        )
    )
)

$db->restore(...) =

Class source code

<?php
/**
 * MySQL backup / restore and administrative tools. Backups the whole database
 * (or only a part) into a ZIP file, including an info file. The restore function
 * accepts the ZIP file and is able to restore the database (with table creations).
 * @package de.atwillys.sw.php.swLib
 * @author Stefan Wilhelm
 * @copyright Stefan Wilhelm, 2009-2010
 * @license GPL
 * @version 1.0
 * @uses Tracer
 * @uses MySql
 * @uses MySqlException
 * @uses FileSystem
 * @uses ZipFile
 */
class MySqlAdministration extends MySql {

    /**
     * MySQL wrapper class constructor
     * @param string $database
     * @param string $user
     * @param string $password
     * @param string $server
     */
    public final function __construct($database='', $user='', $password='', $server='') {
        parent::__construct($database, $user, $password, $server);
    }

    /**
     * Locks the specified tables (array with the table names)
     * in the database. If both read and write lock is not specified,
     * all tables in the database will be write locked.
     * CAUTION: USE unlock() method when you leave the critical section.
     * @param array $readLockTables
     * @param array $writeLockTables
     */
    public final function lock($readLockTables=null, $writeLockTables=null) {
        if(empty($readLockTables) && empty($readLockTables)) {
            // No parameters: write lock all
            $writeLockTables = $this->getTableNames();
        } else if((!is_array($readLockTables) && !empty($readLockTables) ) || ( !is_array($writeLockTables)) && !empty($writeLockTables) ) {
            // No array and not null, array(), false: invalid.
            throw new MySqlException('The arguments for the tables to lock is incorrect.');
        }

        // conditionize
        if(!empty($readLockTables)) {
            foreach($readLockTables as $key => $table) {
                $readLockTables[$key] = !in_array($table, $writeLockTables) ? self::escape(trim($table)) : false;
            }
            $readLockTables = array_filter($readLockTables);
            $readLockTables = !empty($readLockTables) ? (implode(' READ,', $readLockTables) . ' READ') : '';
        } else {
            $readLockTables = '';
        }
        if(!empty($writeLockTables)) {
            foreach($writeLockTables as $key => $table) {
                $writeLockTables[$key] = self::escape(trim($table));
            }
            $writeLockTables = '' . implode(' WRITE,', $writeLockTables) . ' WRITE';
        } else {
            $writeLockTables = '';
        }

        self::trace("Lock tables");
        $this->query('LOCK TABLES ' . $readLockTables . (($readLockTables!='' && $writeLockTables!='') ? ',' : '') . $writeLockTables);
    }

    /**
     * Unlocks all table locks
     */
    public final function unlock() {
        self::trace('Unlock all tables');
        try {
            $this->query("UNLOCK TABLES");
        } catch(MySqlException $e) {
            self::trace('Falied to unlock tables');
        }
    }

    /**
     * Returns the SQL query for creating the database in the same
     * manner as the actual one is. The query always includes an
     * "IF NOT EXISTS" clause.
     * @return string
     */
    public final function getDatabaseCreationQuery() {
       $r = reset($this->query("SHOW CREATE DATABASE `" . $this->getDatabaseName() . "`" ));
       $r = array_change_key_case($r, CASE_LOWER);
       $r = explode("\n", $r['create database'] . ';', 2);
       $r[0] = str_ireplace('CREATE DATABASE', 'CREATE DATABASE IF NOT EXISTS', $r[0]);
       $r = implode("\n", $r);
       return $r;
    }

    /**
     * Returns the SQL query for creating a table that is identical
     * to the existing table. If no table is specified, the method
     * generates a creation query for alltables in the database.
     * @param string $table
     * @param bool $ifNotExists
     * @return string
     */
    public final function getTableCreationQuery($table=null, $ifNotExists=true) {
       if(is_null($table)) {
           $tables = $this->getTableNames();
       } else {
           $tables = array(self::escape($table));
       }

       $queries = array();
       foreach($tables as $table) {
           $r = reset($this->query("SHOW CREATE TABLE `" . $table . "`" ));
           $r = array_change_key_case($r, CASE_LOWER);
           $r = $r['create table'] . ';';
           if($ifNotExists) {
               $r = explode("\n", $r, 2);
               $r[0] = str_ireplace('CREATE TABLE', 'CREATE TABLE IF NOT EXISTS', $r[0]);
               $r = implode("\n", $r);
           }
           $queries[] = $r;
           $queries[] = '';
       }

       return implode("\n", $queries);
    }

    /**
     * Returns the SQL query for removing a table from database
     * @param string $table
     * @return string
     */
    public function getTableDropQuery($table) {
        return "DROP TABLE IF EXISTS `" . self::escape($table) . "`;";
    }

    /**
     * Returns the SQL query for deleting the whole database
     * @return string
     */
    public function getDatabaseDropQuery() {
        return "DROP DATABASE IF EXISTS `" . self::escape($this->getDatabaseName()) . "`;";
    }

    /**
     * Backups the table structure as SQL file and all tables as query output,
     * if the $tablePrefix (if spexified) matches the begin of the table name
     * (case insensitive). Returns an array containing information about the
     * backup files of all tables, the prefixes, and the table names without
     * prefix, as well as a boolean flag that indicated weather the table was
     * backuped or not.
     * @param string $tablePrefix
     * @param array $tables
     * @param string $comments
     * @param bool $dropTablesBeforeInsert
     * @param bool $dropAndRecreateDatabase
     * @return array
     */
    public final function backup($tablePrefix='', $tables=null, $comments='', $dropTablesBeforeInsert=true, $dropAndRecreateDatabase=false) {
        // Conditionize and check arguments
        if(empty($tables)) {
            $tables = $this->getTableNames();
        } else if(!is_array($tables)) {
            throw new MySqlException('If specified, then the tables (names) to backup must be pass as array');
        }

        if(!settype($comments, 'string')) {
            throw new MySqlException('Comment must be a string');
        }

        $date = new UtcDate();
        $backupFolder = FileSystem::getTempDirectory() . "/mysqlbackup";

        $return = array(
            'zipfile' => '',
            'backupinfo' => '',
            'comments' => $comments,
            'time' => $date->toString(),
            'database' => $this->getDatabaseName(),
            'prefix' => $tablePrefix,
            'create' => '',
            'tables' => array()
        );

        // Delete backup folder to have a new folder with proper write conditions
        if(FileSystem::exists($backupFolder)) {
            FileSystem::delete($backupFolder) ;
        }
        FileSystem::mkdir($backupFolder);
        $filebase =  $backupFolder . '/mysqlbackup.' . $this->getDatabaseName() . '.' . $date->getTimeStamp() . '.';

        // Main backup in critical section surrounded by lock(), try {} catch() {}, unlock()
        $ee = null;
        {
            // Lock all tables READ/WRITE
            $structureData = '';
            $this->lock();
            try {
                // Backup structure
                if($dropAndRecreateDatabase) {
                    $structureData .= str_ireplace('`' . $this->getDatabaseName() . '`', '<<DATABASE>>', $this->getDatabaseDropQuery()) . "\n\n";
                }
                $structureData .=  str_ireplace('`' . $this->getDatabaseName() . '`','<<DATABASE>>', $this->getDatabaseCreationQuery()) . "\n\n";

                // Iterate tables
                foreach($tables as $table) {
                    $table = trim($table);
                    if(empty($tablePrefix) || stripos($table, $tablePrefix) === 0) {
                        $name = empty($tablePrefix) ? $table : str_ireplace($tablePrefix, '', $table);
                        $file =  $filebase . $name . '.tdata';
                        $this->query("SELECT * FROM `" . self::escape($table) . "` INTO OUTFILE '$file'");
                        // Save data for further usage
                        $return['tables'][$name] = array(
                            'name' => $name,
                            'file' => str_ireplace($backupFolder, '', $file)
                        );

                        // If desired, drop original table
                        if($dropTablesBeforeInsert) {
                            $dt = $this->getTableDropQuery($table);
                            if(!empty($tablePrefix)) {
                                $dt = str_replace($table, '<<PREFIX>>'. $name, $dt);
                            }
                            $structureData .= $dt . "\n\n";
                            unset($dt);
                        }

                        // Create table:
                        $ct = $this->getTableCreationQuery($table);
                        if(!empty($tablePrefix)) {
                            $ct = explode("\n", $ct, 2);
                            $ct[0] = str_replace($table, '<<PREFIX>>'. $name, $ct[0]);
                            $ct = implode("\n", $ct);
                        }
                        $structureData .=  $ct . "\n\n";
                        unset($ct);
                    }

                    // Export structure to file
                    $file = $filebase . "create.sql";
                    if(@file_put_contents($file, $structureData) === false) {
                        throw new Exception('Could not write structure file');
                    } else {
                        $return['create'] = str_ireplace($backupFolder, '', $file);;
                    }
                }

                // Generate backup info
                $return['backupinfo'] = str_ireplace($backupFolder, '', $file);
                $backupInfo = json_encode($return);
                $file = $filebase . "backup.json";
                if(@file_put_contents($file, $backupInfo) === false) {
                    throw new Exception('Could not write backup info file');
                }
            } catch(Exception $e) {
                Tracer::traceException($e);
                $ee = $e;
            }
            // unlock the tables
            $this->unlock();
        }

        // Exception handling after unlock()
        if(!is_null($ee)) {
            try {
                FileSystem::delete($backupFolder);
            } catch(Exception $e) {
            }
            throw $ee;
        }
        // Compress backup folder
        $zip = FileSystem::getTempDirectory() . '/' . $date->getYear() . '-' . $date->getMonth() .  '-' . $date->getDay() . '-MySqlBackup.zip';
        ZipFile::compress($backupFolder, $zip);
        FileSystem::delete($backupFolder);
        $return['zipfile'] = $zip;
        return $return;
    }

    /**
     * Restores the database content from zip archive. If $tablePrefix
     * is not spexified, the argument will be read from the backup info file in
     * the zip archive. The database is the database actually selected in this
     * object.
     * @param string $zipArchive
     * @param string $tablePrefix
     */
    public final function restore($zipArchive, $tablePrefix=null) {
        $backupFolder = FileSystem::getTempDirectory() . "/mysqlbackup";
        if(FileSystem::exists($backupFolder)) {
            FileSystem::delete($backupFolder);
        }
        try {
            ZipFile::extract($zipArchive,  FileSystem::getTempDirectory());
            if(!FileSystem::exists($backupFolder)) {
                throw new Exception('Backup folder does not exist after extracting zip archive');
            } else {
                $info = FileSystem::find($backupFolder, "*.backup.json");
                if(count($info) == 0) {
                    throw new Exception('Could not find a backup info file in zip archive');
                } else if(count($info) > 1) {
                    throw new Exception('Threr are more than one backup info files in zip archive. Do not know which to use');
                } else {
                    // Get arguments or default arguments from info file
                    $info = json_decode(FileSystem::readFile(reset($info)), true);

                    if(is_null($tablePrefix)) {
                        if(isset($info['prefix']) && strlen(trim($info['prefix'])) > 0) {
                            $tablePrefix = $info['prefix'];
                        } else {
                            throw new Exception('No table prefix specified and no prefix in the backup info file');
                        }
                    }

                    if(!isset($info['create'])) {
                        throw new Exception('Missing "create" entry in info file');
                    } else if(!isset($info['tables']) || !is_array($info['tables'])) {
                        throw new Exception('Missing tables entry in info file');
                    } else if(empty($info['tables'])) {
                        throw new Exception('Tables entry in info file empty, no tables to restore registered.');
                    } else {
                        $tablePrefix = addslashes(trim($tablePrefix));
                        $create = FileSystem::readFile($backupFolder . '/'. trim($info['create'], " /"));
                        $create = str_replace('<<PREFIX>>', $tablePrefix, $create);
                        $create = str_replace('<<DATABASE>>', $this->getDatabaseName(), $create);
                        $create = str_replace("\n", "", str_replace("\r","", $create));
                        $tables = array();
                        Tracer::trace_r($info['tables'], '$info["tables"]');
                        foreach($info['tables'] as $table) {
                            $tables[$tablePrefix . $table['name']] = $backupFolder . '/'. trim($table['file'], " /");
                        }

                        Tracer::trace_r($tables, '$tables');
                        foreach(explode(';', $create) as $query) {
                            $query = trim($query);
                            if(!empty($query)) {
                                Tracer::trace($query);
                                $this->query($query);
                            }
                        }
                        foreach($tables as $tableName => $tableFile) {
                            @chmod($tableFile, 0777);
                            $query = "LOAD DATA INFILE '$tableFile' INTO TABLE `" . self::escape($tableName) . "`";
                            if(!empty($query)) {
                                Tracer::trace($query);
                                $this->query($query);
                            }
                        }
                    }
                }
            }
        } catch(Exception $e) {
            FileSystem::delete($backupFolder);
            throw $e;
        }
        FileSystem::delete($backupFolder);
    }
};
?>
Posted in PHP | Tagged , , | Comments Off

MySQL wrapper class

One of the older classes, but nonetheless worth to publish as it may be of use. The MySQL wrapper contains methods to obtain information about the database, tables, as well as normal queries, queries to objects or arrays and key-value based requests. Multiple instances are possible, as each object has an own link to its database connection. The instances automatically connect to the database and throw exceptions (MySqlException) on error. It is also the base class for a MySQL administration class, which is published on this site as well.

Sample source code

<?php
// This example referrs to a table, which was generated usin the query:
//
//    SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
//    CREATE TABLE IF NOT EXISTS `test` (
//      `id` int(11) NOT NULL AUTO_INCREMENT,
//      `rid` int(11) NOT NULL,
//      PRIMARY KEY (`id`)
//    ) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
//
//
// (it is just a renamed relation table of on of my projects)

// The database object
$db = new MySql('test', 'root', '', 'localhost');

// Returns information about the database
$r = $db->getStatus();
print '$db->getStatus() = ' . print_r($r, true) . '<br/>';

// Returns information about the status and fields of all tables
$r = $db->getTableStatus();
print '$db->getTableStatus() = ' . print_r($r, true) . '<br/>';

// Returns information about the fields of a table
$r = $db->getTableFields('test');
print '$db->getTableFields() = ' . print_r($r, true) . '<br/>';

// Query an insert, where the fields are defined as array keys and the values
// as the array values. The return value is the id if the just inserted row.
$id = $db->queryInsertInto('test', array('rid' => rand(0, 1000)));
print '$db->queryInsertInto(...) = ' . print_r($id, true) . '<br/>';

// Query the fields of one entry. This method returns directly the content
// if the filter is an ID (numeric). Otherwise it returns an array containing
// the rows with associative values (even if there is only one matching row).
$r = $db->queryFields('test', array('id','rid'), $id);
print '$db->queryFields(' . $id . ') = ' . print_r($r, true) . '<br/>';

// This method works with SQL syntax and returns an array of objects
$r = $db->queryObjects('SELECT * FROM test WHERE id=' . $id);
print '$db->queryObjects(...) = ' . print_r($r, true) . '<br/>';

// This method works with SQL syntax and returns an array of field-arrays
$r = $db->queryArrays('SELECT * FROM test WHERE id=' . $id);
print '$db->queryArrays(...) = ' . print_r($r, true) . '<br/>';

// Change a row
$r = $db->queryUpdateTable('test', array('id' => $id, 'rid' => rand(0, 1000)));
print '$db->queryUpdateTable(...) = ' . print_r($r, true) . '<br/>';

// Delete the row
$r = $db->queryDeleteFrom('test', $id);
?>

Output


Class source code

<?php
/**
 * Exception thrown by class MySql and MySqlAdministration
 * @package de.atwillys.sw.php.swLib
 * @author Stefan Wilhelm
 * @copyright Stefan Wilhelm, 2007-2010
 * @license GPL
 * @version 1.0
 */
class MySqlException extends Exception {
}
?>

<?php
/**
 * MySQL wrapper, which provides auto connect/disconnect, normal SQL query,
 * escaping, and special query methods like queryInsertInto(), queryDeleteFrom(),
 * queryUpdate(), query to objects, query information about the database or
 * tables - all kind of queries you use regularely.
 * @package de.atwillys.sw.php.swLib
 * @author Stefan Wilhelm
 * @copyright Stefan Wilhelm, 2007-2010
 * @license GPL
 * @version 1.0
 * @uses MySqlException
 * @uses Tracer
 */
class MySql implements ITracable {

    /**
     * Class configuration
     * @staticvar array
     */
    private static $config = array(
        'server' => 'localhost',
        'database' => '',
        'user' => '',
        'password' => ''
    );

    /**
     * SQL connection link /id
     * @var unknown
     */
    private $link;

    /**
     * Server connection, usual localhost
     * @var string
     */
    private $server;

    /**
     * SQL server login user
     * @var string
     */
    private $user;

    /**
     * SQL login password
     * @var string
     */
    private $password;

    /**
     * SQL database
     * @var string
     */
    private $database;

    /**
     * Connected flag
     * @var bool
     */
    private $connected;

    /**
     * Defines class defaults
     * @param string $database
     * @param string $user
     * @param string $password
     * @param string $server
     */
    public static final function config($database=null, $user=null, $password=null, $server=null) {
        if(!is_null($database)) self::$config['database'] = (empty($database) ? 'localhost' : $database);
        if(!is_null($server)) self::$config['server'] = trim($server);
        if(!is_null($user)) self::$config['user'] = trim($user);
        if(!is_null($password)) self::$config['password'] = $password;
    }

    /**
     * Escapes the value of a mysql query value text (so that no injection
     * possible and normal text values do not cause an unexpected error)
     * Null is escaped with NULL
     * @param string $query_value
     * @return string
     */
    public static final function escape($query_value) {
        if(empty($query_value)) {
            return is_null($query_value) ? 'NULL' : '';
        } else {
            return mysql_real_escape_string($query_value);
        }
    }

    /**
     * MySQL wrapper class constructor
     * @param string $database
     * @param string $user
     * @param string $password
     * @param string $server
     */
    public function __construct($database='', $user='', $password='', $server='') {
        self::trace("($database, $user, (pwd), $server)");
        $this->server      = $server != '' ? $server : self::$config['server'];
        $this->user        = $user	!= '' ? $user : self::$config['user'];
        $this->password    = $password != '' ? $password : self::$config['password'];
        $this->database    = $database != '' ? $database : self::$config['database'];
        $this->connected   = false;
        $this->link        = null;
        try { $this->connect(); } catch(Exception $e) { Tracer::traceException($e); }
    }

    /**
     * MySQL wrapper class destructor
     */
    public function __destruct() {
        self::trace("Disconnect ...");
        $this->disconnect(true);
    }

    /**
     * Server connected inicator
     * @return bool
     */
    public final function isConnected() {
        return $this->connected;
    }

    /**
     * Returns the name of the database
     * @return string
     */
    public final function getDatabaseName() {
        return $this->database;
    }

    /**
     * Connect to server
     */
    public final function connect() {
        if(!$this->connected) {
            self::trace("Connecting");
            $this->link = @mysql_connect($this->server, $this->user, $this->password);
            if(!$this->link) {
                throw new MySqlException("Connecting to database failed");
            } else if(!mysql_select_db($this->database)) {
                @mysql_close($this->link);
                throw new MySqlException("Failed to select database");
            } else {
                if(function_exists('mysql_set_charset')) {
                    @mysql_set_charset('utf8', $this->link);
                } else {
                    @mysql_query("SET NAMES 'utf8'", $this->link);
                }
                $this->connected = true;
            }
        }
    }

    /**
     * Disconnect MySQL server
     */
    public final function disconnect() {
        @mysql_close($this->link);
        $this->link = null;
        $this->connected = false;
    }

    /**
     * Returns the database structure and status as assoc. array.
     * @return array
     */
    public final function getStatus() {
        $database = array(
            'name' => $this->getDatabaseName(),
            'tables' => array()
        );
        $tables = $this->getTableStatus();
        foreach($tables as $table) {
            $name = $table['name'];
            $table['fields'] = $this->getTableFields($name);
            $database['tables'][$name] = $table;
        }
        return $database;
    }

    /**
     * Returns the list of table names in the database
     * @return array
     */
    public final function getTableNames() {
        if(!$this->isConnected()) {
            $this->connect();
        }
        $result = array();
        $tables = $this->query("SHOW TABLES FROM " . $this->database);

        foreach($tables as $table) {
            $table = reset($table);
            $result[] = $table;
        }
        return $result;
    }

    /**
     * Returns an assoc. array with information about all tables
     * @return array
     */
    public final function getTableStatus() {
        $tables = array();
        $r = $this->query("SHOW TABLE STATUS FROM " . $this->getDatabaseName() . ";");
        foreach($r as $table) {
            $table = array_change_key_case($table, CASE_LOWER);
            $tables[$table['name']] = $table;
        }
        return $tables;
    }

    /**
     * Returns the field specifications of the database fields
     * @param string $table
     * @return array
     */
    public final function getTableFields($table) {
        $result = array();
        $table = self::escape($table);
        $fields = $this->query("SHOW FULL COLUMNS FROM `$table`");
        foreach($fields as $field) {
            $field = array_change_key_case($field, CASE_LOWER);
            $result[$field['field']] = $field;
        }
        return $result;
    }

    /**
     * Returns the resource to the query. The result resource has to be
     * set free manually.
     * @param string $query
     * @return resource
     */
    public final function queryResource($query) {
        self::trace("query=$query");
        if(empty($query)) {
            throw new MySqlException("Query failed: no query rule specified.");
        } else if(!$this->isConnected()) {
            $this->connect();
        }
        $result = @mysql_query($query, $this->link);

        if($result === false) {
            Tracer::trace('MySql ERROR=' . mysql_error());
            throw new MySqlException('MySql query failed');
        } else {
            return $result;
        }
    }

    /**
     * Perform query. Returns array of assoc arrays
     * @param string $query
     * @return array&
     */
    public final function & query($query) {
        $result = $this->queryResource($query);
        $data = array();
        if(is_resource($result)) {
            while($row = mysql_fetch_assoc($result)) {
                $data[] = $row;
            }
            @mysql_free_result($result);
        }
        return $data;
    }

    /**
     * Query SELECT form the table, where all fields specified by the field names
     * array are fetched into a structure (assoc. array).
     * @param string $table
     * @param array $fieldNames
     * @param string $idOrFilter
     * @param int $numRows
     * @param int $fromRow
     * @return void
     */
    public final function & queryFields($table, $fieldNames=array(), $idOrFilter='', $numRows=0, $fromRow=0) {
        $fromRow = intval($fromRow);
        $numRows = intval($numRows);
        $table = '`' . self::escape(trim($table)) . '`';

        if(empty($fieldNames)) {
            $fields = '*';
        } else if(!is_array($fieldNames)) {
            throw new MySqlException('Specified field names must be passed as array');
        } else {
            $fields = array();
            foreach($fieldNames as $key => $f) {
                $fields[] = '`' . self::escape($f) . '`';
            }
            $fields = implode(',', $fields);
        }

        $query = "SELECT $fields FROM $table";

        if(is_numeric($idOrFilter)) {
            $query .= ' WHERE id=' . intval($idOrFilter);
        } else if(!empty($idOrFilter)) {
            $query .= " WHERE $idOrFilter";
        }

        if($numRows > 0) {
            $query .= ($fromRow > 0) ? (" LIMIT $fromRow, $numRows") : (" LIMIT $numRows");
        }

        $r = $this->query($query);

        return $r;
    }

    /**
     * Query into a result instance.
     * @return array
     */
    public final function & queryArrays($query) {
        $result = $this->queryResource($query);
        $data = array();
        while($row = mysql_fetch_array($result)) $data[] = $row;
        @mysql_free_result($result);
        return $data;
    }

    /**
     * Query into a result instance.
     * @return array
     */
    public final function & queryObjects($query, $class=null) {
        $result = $this->queryResource($query);
        if(!is_null($class) && !class_exists($class) && !swlib::hasClass($class)) {
            throw new MySqlException('Class does not exist:' . $class);
        } else {
            $data = array();
            if(is_null($class)) {
                while($o = mysql_fetch_object($result)) {
                    $data[] = $o;
                }
            } else {
                while($o = mysql_fetch_object($result, $class)) {
                    $data[] = $o;
                }
            }
            @mysql_free_result($result);
            return $data;
        }
    }

    /**
     * Query UPDATE table, where the SET <key>='<value>' are the array key/value pairs.
     * $filter is according to "WHERE id=<id>" or "WHERE name="test" LIMIT 1";
     * @param string $table
     * @param array $keyValuePairs
     * @param string $filter
     * @param int $limit
     * @return void
     */
    public final function queryUpdateTable($table, array $keyValuePairs, $filter='', $limit=1) {
        $table = self::escape($table);
        $limit = (is_numeric($limit) && $limit > 0) ? " LIMIT " . intval($limit) : '';
        if(empty($filter)) {
            if(isset($keyValuePairs['id'])) {
                $filter = "WHERE id=" . self::escape($keyValuePairs['id']);
                unset($keyValuePairs['id']);
            } else {
                throw new MySqlException('You must set a filter (WHERE anyfield=anyvalue) if you do not have the filed "id"');
            }
        }
        foreach($keyValuePairs as $key => $value) {
            $keyValuePairs[$key] = "`$key`='" . self::escape($value) . "'";
        }
        $keyValuePairs = implode(',', $keyValuePairs);
        $query = "UPDATE `$table` SET $keyValuePairs $filter $limit";
        $this->query($query);
    }

    /**
     * Query INSERT INTO table, where the SET <key>='<value>' are the array key/value pairs.
     * Returns the complete just inserted row.
     * @param string $table
     * @param array $keyValuePairs
     * @return array
     */
    public final function queryInsertInto($table, array $keyValuePairs) {
        $table = trim($table);
        if(empty($table)) {
            throw new MySqlException('Cannot append data in database without a specified table (empty string)');
        } else {
            $table = '`' . self::escape($table)  . '`';
            $keys = $values = array();

            if(isset($keys['id']) && empty($keys['id'])) {
                $keys['id'] = null;
            }
            foreach($keyValuePairs as $key => $value) {
                if(empty($key)) {
                    throw new MySqlException('Database field key is empty');
                } else {
                    $keys[] = '`' . self::escape(trim($key)) . '`';
                    $values[] = "'" . self::escape($value) . "'";
                }
            }
            $keys = implode(',', $keys);
            $values = implode(',', $values);
            $query = "INSERT INTO $table ($keys) VALUES ($values);";
            $this->query($query);
            $r = $this->query('SELECT LAST_INSERT_ID();');
            if(empty($r)) {
                throw new MySqlException('Could not get autoincrement id of insert action');
            } else {
                $r = reset($r);
                return reset($r);
            }
        }
    }

    /**
     * Query DELETE FROM table. If $idOrFilter is numeric, then the filter
     * will be automatically set to "WHERE id=$idOrFilter", if filter is a
     * text, then the filter text will be used.
     * @param string $table
     * @param array $idOrFilter
     * @return void
     */
    public final function queryDeleteFrom($table, $idOrFilter) {
        $idOrFilter = trim($idOrFilter);
        if(is_numeric($idOrFilter)) {
            $idOrFilter = "id='$idOrFilter'";
        } else if(empty($idOrFilter)) {
            throw new MySqlException('No id/filter given to indicate the row to delete');
        }
        $this->query("DELETE FROM $table WHERE $idOrFilter");
    }

    /**
     * Saves the a BLOB in the database in a file in the file system. Write
     * the condition like "id=1". The WHERE is added by the method.
     * @param string $file
     * @param string $table
     * @param string $field
     * @param string $condition
     */
    public final function queryBlobToFile($file, $table, $field, $condition) {
        $file = trim($file);
        $table = self::escape(trim($table));
        $field = self::escape(trim($field));
        $condition = str_replace(';', '', $condition); // no multiple commands allowed.
        if(file_exists($file)) {
            throw new MySqlException('The file to write the BLOB in already exists');
        } else if(!is_dir(dirname($file))) {
            throw new MySqlException('Parent directory of the file to save does not exist');
        } else if(!is_writable(dirname($file))) {
            throw new MySqlException('Parent directory of the file to save is not writable for you');
        } else {
            $query = "SELECT `$field` FROM `$table` WHERE $condition LIMIT 1 INTO DUMPFILE '$file'";
            self::trace($query);
            $this->query($query);
        }
    }

    /**
     * Performs a query to write a BLOB field in the database from a
     * existing file in the file system.
     * @param string $file
     * @param string $table
     * @param string $field
     */
    public final function queryFileToBlob($file, $table, $field, $condition) {
        // All the submethods throw exceptions if something goes wrong.
        $file = trim($file);
        $table = self::escape(trim($table));
        $field = self::escape(trim($field));
        $condition = str_replace(';', '', $condition); // no multiple commands allowed.
        $data = self::escape(FileSystem::readFile($file));
        self::trace("UPDATE `$table` SET `$field`='[BLOB of " . (strlen($data) >> 10) . "kb]' WHERE $condition");
        $this->query("UPDATE `$table` SET `$field`='$data' WHERE $condition");
    }

    /**
     * Private tracing method
     * @param string $text
     * @param int $level
     */
    protected function trace($text, $level=1) {
        if(!Tracer::tracedClass(__CLASS__)) return;
        Tracer::trace($text, $level);
    }
};
?>
Posted in PHP | Tagged , , | Comments Off

Configuration access wrapper class

During a chat with a friend about the convenience of object-style access to PHP configuration, which are defined as structured associative arrays, we came to a lightweight wrapper class that addresses these issues. As the configuration array is already in use by other modules, it should not be changed, and a copy to objects would lead to a higher memory usage. So the solution is based on cascaded references to the configuration array. The example shows what it is about:

Sample source code

<?php
// This is an example config array which is e.g. loaded by including
// a config.php or the like.
$theConfigurationStructure = array(
    'ref' => 'no',
    'database' => array(
        'server' => 'unlocalhost',
        'user' => 'me',
        'password' => 'none',
        'database' => 'Example database',
        'tables' => array(
            'contacts' => array(
                'id', 'name', 'email'
            ),
            'cache' => array(
                'id', 'uri', 'chksum', 'expires', 'etag', 'lastmodified'
            ),

        )
    ),
    'aNumber' => 10,
    'a_text' => 'nothing in there'
);

// This is the instance of the wrapper class, which "points" to
// the root node of the array
$cfg = new ConfigAccess($theConfigurationStructure);

// Access to a scalar config entry
print 'config.aNumber = ' . $cfg->aNumber . '<br/>';

// Access to a scalar config entry in a sub-configuration
print 'config.database.user = ' . $cfg->database->user . '<br/>';

// Access to an array config entry in a sub-configuration
// Note here we have to escape, because the class returns an object
// instead of an array. If we went the array instead, we call the toArray()
// escape function
print 'config.database.tables.cache = ' . print_r($cfg->database->tables->cache->toArray(), true) . '<br/>';
?>

Class source code

<?php
/**
 * Wrapper class for object-style access to an associative configuration
 * array. Note that the configuration keys must be conform to the convention of
 * variables (word characters and numbers). Otherwise the array key cannot be
 * converted to an object property.
 * @class ConfigAccess
 * @package de.atwillys.php.misc
 * @author Stefan Wilhelm
 * @license GPLq
 */
class ConfigAccess {

    /**
     * Reference to the config array content
     * @var array
     */
    private $__r = null;

    /**
     * Constructor, stores a reference to the root or sub-branch of the
     * assiciative configuration array.
     * @param array $cref
     */
    public final function __construct(array & $cref) {
        $this->__r = &$cref;
    }

    /**
     * Returns the content of the condiguration array (or sub-array) with the
     * key $p. If this is an array, then a ConfigAccess object will be returned
     * instead, which "points" to the sub-branch of the configuration.
     * @param string $p
     * @return mixed
     */
    public final function __get($p) {
        if(!isset($this->__r[$p])) {
            throw new Exception("Config entry '$p' does not exist");
        }
        return is_array($this->__r[$p]) ? new self(&$this->__r[$p]) : $this->__r[$p];
    }

    /**
     * Config setter, this is NOT allowed. Hence, the method throws an exception.
     * @param string $p
     * @param string $v
     */
    public final function __set($p,  $v) {
        throw new Exception('You cannot modify the configuration');
    }

    /**
     * This method is an escape for the case that the content of a configuration
     * is an array and shall not be represented with a ConfigAccess object.
     * @return array
     */
    public final function toArray() {
        return $this->__r;
    }
}
?>
Posted in PHP | Tagged , | Comments Off

Calendar class library

Because there are already thousands of calendars in the net, I added my own one as well ;-). This is a highly maintainable and easily extendable bunch of classes to display month and day calendars with events. It consists the following classes:

  • The main calendar class GregorianCalendar, which organizes the bundle and provides the interface to the application/script.
  • the renderers GregorianMonthCalendarRenderer and GregorianDayCalendarRenderer, which are the base for the visualization. They are fed with preprocessed data.
  • The events are managed by the GregorianCalendarEventController. This class is to load, filter and save events in files or a data base, or every possible source (that’s why it is a separate class).
  • The class GregorianCalendarEvent is the base class for your own events, managed by the controller and used by the other classes.

That’s all. The sample code illustrates that you don’t need to do much to have a functioning calendar, and that it is very easy to extend it.
The class sources are normally split in more class files and here collected in one block. Normally every class is in a file with the same name as the class and the extension “.class.php”.

Sample source code

<?php
/**
 * Sample Gegorian Month Calendar
 * @package de.atwillys.sw.php.cal.sample
 * @author Stefan Wilhelm, 2010
 * @license GPL
 *
 *
 * If you want to make your own renderer, overload these methods:
 *
 */
class SampleMonthCalendarRenderer extends GregorianMonthCalendarRenderer {

    //    public function onStart() {
    //    }
    //
    //    public function onEnd() {
    //    }
    //
    //    public function onWeekStart($weekId) {
    //    }
    //
    //    public function onWeekEnd($weekId) {
    //    }
    //
    //    public function onDay(IDate $day, $isPadding, $isSelected, $events) {
    //    }
}
?>

<?php
/**
 * Sample Gegorian Day Calendar
 * @package de.atwillys.sw.php.cal.sample
 * @author Stefan Wilhelm, 2010
 * @license GPL
 *
 *
 * Here I added the block view of the sample events, it is more or less
 * like a trace dump in the day calendar table:
 *
 */
class SampleDayCalendarRenderer extends GregorianDayCalendarRenderer {

    /**
     * Returns a day calendar Event block representation
     * @param GregorianCalendarEvent $event
     * @return string
     *
     * Principally all you have to do is to overload this method as well:
     */
    protected function renderEvent(GregorianCalendarEvent $event) {
        $tick = $this->getParent()->getHourDivision();
        $t1 = new LocalDate($event->getStart());
        $t2 = new LocalDate($event->getEnd());
        $o  = $event->name . " (" . $event->id . ")";
        $o .= '<br/>' . sprintf("%02d:%02d", $t1->getHour(), $t1->getMinute()) .
                ' to ' . sprintf("%02d:%02d", $t2->getHour(), $t2->getMinute());
        $t1 = $tick * floor($event->getStart() / $tick);
        $t2 = $tick * floor($event->getEnd() / $tick);
        $t1 = new LocalDate($t1);
        $t2 = new LocalDate($t2);
        $o .= '<br/>' . sprintf("%02d:%02d", $t1->getHour(), $t1->getMinute()) .
                ' to ' . sprintf("%02d:%02d", $t2->getHour(), $t2->getMinute());
        return $o;
    }
}
?>

<?php
/**
 * Sample Gegorian Calendar event
 * @package de.atwillys.sw.php.cal.sample
 * @author Stefan Wilhelm, 2010
 * @license GPL
 *
 * This is a sample event, which is saved in the data base, the table is
 * build with the SQL:
 *
 *    SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
 *    CREATE TABLE IF NOT EXISTS `events` (
 *      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
 *      `name` text NOT NULL,
 *      `begin` bigint(20) NOT NULL,
 *      `end` bigint(20) NOT NULL,
 *      `type` text NOT NULL,
 *      `data` text NOT NULL COMMENT 'serialized text data',
 *      `flags` set('p','d') NOT NULL DEFAULT 'p' COMMENT 'pending, deleted',
 *      PRIMARY KEY (`id`)
 *    ) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
 *
 */
class SampleEvent extends GregorianCalendarEvent {

    /**
     * GregorianCalendarEvent constructor
     * @param string $nameOrDBArray=''
     * @param mixed $start=null
     * @param mixed $end=null
     * @param array $data=array()
     * @param string $type=null
     * @param mixed $id=null
     */
    public function __construct($nameOrDBArray='', $start=null, $end=null, array $data=array(), $type=null, $id=null) {
        if(!is_array($nameOrDBArray)) {
            parent::__construct($nameOrDBArray, $start, $end, $data, $type, $id);
        } else {
            $this->unserialize($nameOrDBArray);
        }
    }

    /**
     * Returns an associatove array to save the event in a database. The
     * data are JSON encoded
     * @return string
     */
    public function serialize() {
        $r = array(
            'id' => $this->id,
            'name' => $this->name,
            'begin' => $this->start,
            'end' => $this->end,
            'type' => $this->type,
            'data' => $this->data
        );
        unset($r['data']['resource']);
        $r['data'] = json_encode($r['data'], JSON_FORCE_OBJECT);
        return $r;
    }

    /**
     * Sets all object variables corresponding to the assoc array returned by
     * serialize().
     * @param array $r
     */
    public function unserialize(array $r) {
        $this->id = $r['id'];
        $this->name = $r['name'];
        $this->start = $r['begin'];
        $this->end = $r['end'];
        $this->type = $r['type'];
        $this->data = empty($r['data']) ? array() : json_decode($r['data'], true);
    }
}
?>

<?php
/**
 * Sample Gegorian Calendar event controller
 * @package de.atwillys.sw.php.cal.sample
 * @author Stefan Wilhelm, 2010
 * @license GPL
 *
 * This is the sample event controller, organizes the database and provides the
 * funtionality to obtain and save data.
 * As mentioned, it belongs to a table build with the SQL query:
 *
 *    SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
 *    CREATE TABLE IF NOT EXISTS `events` (
 *      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
 *      `name` text NOT NULL,
 *      `begin` bigint(20) NOT NULL,
 *      `end` bigint(20) NOT NULL,
 *      `type` text NOT NULL,
 *      `data` text NOT NULL COMMENT 'serialized text data',
 *      `flags` set('p','d') NOT NULL DEFAULT 'p' COMMENT 'pending, deleted',
 *      PRIMARY KEY (`id`)
 *    ) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
 *
 */
class SampleEventController extends GregorianCalendarEventController {

    /**
     * The database settings, for the hackers: this sample is NOT
     * on the atwillys server ...
     * @var array
     */
    private $database = array(
        'database' => 'samplecalendar',
        'user' => 'cal',
        'password' => 'cal',
        'server' => 'localhost'
    );

    /**
     * Database
     * @return MySql
     */
    protected function db() {
        if(is_array($this->database)) {
            $this->database = new MySql($this->database['database'],
            $this->database['user'], $this->database['password'],
            $this->database['server']);
        }
        return $this->database;
    }

    /**
     * Returns all events that start and end between a given period.
     * @param mixed $from
     * @param mixed $to
     * @return SampleEvent[]
     */
    public function getEventsBetween($from, $to) {
        $from = new LocalDate($from);
        $to = new LocalDate($to);
        $r = $this->db()->query('SELECT * FROM events WHERE begin >= ' .
                Mysql::escape($from->getTimeStamp(), true)  . ' && end <' .
                MySql::escape($to->getTimeStamp(), true) . ' && NOT FIND_IN_SET(\'d\', flags)' );
        foreach($r as $k => $v) $r[$k] = new SampleEvent($v);
        return $r;
    }

    /**
     * Returns all events that do not end before and do not start after the given
     * time period.
     * @param mixed $from
     * @param mixed $to
     * @return SampleEvent[]
     */
    public function getEventsWhichMatch($from, $to) {
        $from = new LocalDate($from);
        $to = new LocalDate($to);
        $r = $this->db()->query('SELECT * FROM events WHERE NOT end < ' .
                Mysql::escape($from->getTimeStamp(), true)  . ' && NOT begin >' .
                MySql::escape($to->getTimeStamp(), true) . ' && NOT FIND_IN_SET(\'d\', flags)' );
        foreach($r as $k => $v) $r[$k] = new SampleEvent($v);
        return $r;
    }

    /**
     * Returns the event by id, null if not found
     * @param int $id
     */
    public function getEvent($id) {
        $r = $this->db()->query('SELECT * FROM events WHERE id= ' .
                Mysql::escape($id, true) . ' LIMIT 1');
        if(!empty($r)) {
            $event = new SampleEvent();
            $event->unserialize(reset($r));
            return $event;
        } else {
            return null;
        }
    }

    /**
     * Saves an event
     * @param SampleEvent $event
     */
    public function saveEvent(SampleEvent $event) {
        $r = $event->serialize();
        if(!empty($r['id']) && is_numeric($r['id'])) {
            $this->db()->queryUpdateTable('events', $r);
        } else if(empty($r['id'])) {
            $r['id'] = null;
            $this->db()->queryInsertInto('events', $r);
        } else {
            throw new Exception('Sample event id is invalid: ' . $id);
        }
    }

}
?>

<?php
/**
 * Here the index.php, which uses the classes above ...
 * Note: GET is not sanatized
 * Note: There is a if(false) {} to add events in your sample MySql table
 */
require_once('./lib/swlib.class.php');

// Start the swlib
swlib::start(array(
    'use_ob' => true,
    'use_session' => true,
    'var_path' => '/tmp',
    'tmp_path' => '/tmp',
    'var_extension' => '.tmp',
    'include_paths' => array(
        dirname(__FILE__) . '/include'
    )
));

// Let's set another time zone for local dates
date_default_timezone_set('Europe/London');

try {

    // Instantize our event controller
    $rec = new SampleEventController();

    // Instantize the main calendar object and make settings ...
    $cal = new GregorianCalendar();
    $cal->setEventController($rec);         // Set our own event controller
    $cal->setDayDisplayFrom("06:00:00");    // The day cal will ignore 05:59
    $cal->setDayStartsAt("08:00:00");       // From 06:00 to 07:59 is grayed
    $cal->setDayEndsAt("19:59:59");         // From 20:00 to 22:59 is grayed
    $cal->setDayDisplayTo("22:59:59");      // From 23:00 to 23:59 is ignored
    $cal->setHourDivision(900);             // One tick is 15 minutes (resolution)

    // These are our renderers:
    $monthCal = new SampleMonthCalendarRenderer();
    $dayCal = new SampleDayCalendarRenderer();

} catch(Exception $e) {
    print "exception:" . $e->getMessage();
    Tracer::traceException($e);
}

try {
    // The day to show in the month calendar, note that you should
    // SANATIZE this before ...
    if(isset($_GET['month-calendar-show'])) {
        $_SESSION['month-calendar-show'] = $_GET['month-calendar-show'];
    }
    if(isset($_SESSION['month-calendar-show'])) {
        $cal->setDateToShow($_SESSION['month-calendar-show']);
    }
} catch(Exception $e) {
    Tracer::traceException($e);
    $cal->setDateToShow( $cal->getToday() );
}

try {
    // The selected day in the month/day calendar, note that you should
    // SANATIZE this before ...
    if(isset($_GET['month-calendar-select'])) {
        $_SESSION['month-calendar-select'] = $_GET['month-calendar-select'];
    }
    if(isset($_SESSION['month-calendar-select'])) {
        $cal->setSelectedDate($_SESSION['month-calendar-select']);
    }
} catch(Exception $e) {
    Tracer::traceException($e);
    $cal->setSelectedDate( $cal->getDateToShow() );
}

if(false)
{
    // Here a piece of code that creates some events in your database
    MySql::traced(true);

    // We round the timestamp to half an our
    $from = new LocalDate(); // new local date without argument is now.
    $from->setTimeStamp(round($from->getTimeStamp()/1800)*1800);
    $to = $from->add(9 * 3600); // events takes place for 9 hurs
    $ev = new SampleEvent('Overlapping 0', $from, $to, array('who' => uniqid(),
        'test1' => rand(0, 10)), 'sample' );
    $rec->saveEvent($ev); // save it in the database
    Tracer::trace("Overlapping 0 = $ev"); // and trce it ...

    $from = $from->add(5*1800);
    $to = $from->add(5*1800);
    $ev = new SampleEvent('Overlapping 1', $from, $to, array('who' => uniqid(),
        'test1' => rand(0, 10)), 'sample' );
    $rec->saveEvent($ev);
    Tracer::trace("Overlapping 1 = $ev");

    $from = $from->add(2435);
    $to = $from->add(9873);
    $ev = new SampleEvent('Overlapping 2', $from, $to, array('who' => uniqid(),
        'test1' => rand(0, 10)), 'sample' );
    $rec->saveEvent($ev);
    Tracer::trace("Overlapping 2 = $ev");

    $from = $from->add(234);
    $to = $from->add(4827);
    $ev = new SampleEvent('Overlapping 3', $from, $to, array('who' => uniqid(),
        'test1' => rand(0, 10)), 'sample' );
    $rec->saveEvent($ev);
    Tracer::trace("Overlapping 3 = $ev");

    $from = $from->add(4321);
    $to = $from->add(12345);
    $ev = new SampleEvent('Overlapping 4', $from, $to, array('who' => uniqid(),
        'test1' => rand(0, 10)), 'sample' );
    $rec->saveEvent($ev);
    Tracer::trace("Overlapping 4 = $ev");

}

// The view starts here:

?><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html><head>
<title>Sample calendar</title>
<link rel="stylesheet" href="index.css" type="text/css" />
<link rel="stylesheet" href="include/gregorian-month-calendar.css" type="text/css" />
<link rel="stylesheet" href="include/gregorian-day-calendar.css" type="text/css" />
<link rel="stylesheet" href="include/sample-calendar.css" type="text/css" />
</head><body>
<?php

try {
    print '<table id="sample-calendar"><tr>';
    print '<td class="month-calendar-block">';
    print "\n" . $cal->renderMonthCalendar($monthCal) . "\n";
    print $testout . "\n";
    print '</td>';
    print '<td class="day-calendar-block">';
    print "\n" . $cal->renderDayCalendar($dayCal) . "\n";
    print '</td>';
    print '</tr></table>';
} catch(Exception $e) {
    print "exception:" . $e->getMessage();
    Tracer::traceException($e);
}
?>
</body></html>

Output

As the output is not that small, I made a html snapshot of one page, which you can find it here.

Class source code

<?php
/**
 * Gegorian Calendar management with the ability to render day calendars and
 * month calendars using the corresponding rendering classes. Also manages
 * the events for selected time spans.
 * @package de.atwillys.sw.php.swLib
 * @author Stefan Wilhelm
 * @copyright Stefan Wilhelm, 2010
 * @license GPL
 * @version 1.0
 * @uses GregorianCalendarEvent
 * @uses GregorianCalendarEventController
 * @uses GregorianDayCalendarRenderer
 * @uses GregorianMonthCalendarRenderer
 */
class GregorianCalendar {

    /**
     * The date (and time) to show.
     * @var IDate
     */
    private $dateToShow = null;

    /**
     * The (real) today, set this only in the constructor for history purposes
     * @var IDate
     */
    private $today = null;

    /**
     * The (real) date which is selected, e.g. to displad a day calendar
     * @var IDate
     */
    private $dateSelected = null;

    /**
     * The class to instantiate, must be an IDate implementation, normally
     * LocalDate or UtcDate
     * @var IDate
     */
    private $dateClass = 'LocalDate';

    /**
     * Defines if the week starts with Sunday or Monday
     * @var bool
     */
    private $weekStartsWithSunday = false;

    /**
     * A numeric array which contains the weekday indices for using with localized
     * day name PHP functions.
     * @var array
     */
    private $weekDayIndex = array(1,2,3,4,5,6,0);

    /**
     * Contains the weekday abbreviations
     * @var array
     */
    private $weekDayAbbr = array('sun','mon','tue','wed','thu','fri','sat');

    /**
     * Defines the time period of division in a day calendar (in seconds).
     * @var int
     */
    private $hourDivision = 3600;

    /**
     * Defines when the day starts for the calendar (timestamp format in seconds)
     * @var int
     */
    private $dayStartsAt = '00:00:00';

    /**
     * Defines when the day ends for the calendar (timestamp format in seconds)
     * @var int
     */
    private $dayEndsAt = '23:59:59';

    /**
     * Every time between 00:00:00 and $dayDisplayedFrom (les than $dayDisplayedFrom)
     * will not be displayed
     * @var int
     */
    private $dayDisplayedFrom = '00:00:00';

    /**
     * Every time > $dayDisplayedTo to '23:59:59' will not be displayed
     * @var int
     */
    private $dayDisplayedTo = '23:59:59';

    /**
     * Reference to the event controller object
     * @var GregorianCalendarEventController
     */
    private $eventController = null;

    /**
     * Constructor
     * @param IDate $dateToShow
     * @param IDate $today
     * @param string $dateClass
     */
	public function  __construct($dateToShow=null, $today=null, $dateClass='LocalDate') {
        $this->dateClass = $dateClass;
        $this->setToday($today);
        $this->setDateToShow($dateToShow);
        $this->setWeekStartsOnSunday(false);
        $this->setDayStartsAt($this->dayStartsAt);
        $this->setDayEndsAt($this->dayEndsAt);
        $this->setDayDisplayFrom($this->dayDisplayedFrom);
        $this->setDayDisplayTo($this->dayDisplayedTo);
        $this->setSelectedDate($this->getToday());
	}

    /**
     * Returns of a time $t is/takes place at the day $day. The day must be normed
     * to 00:00:00h.
     * @param IDate $day
     * @param IDate $t
     * @return bool
     */
    protected final function isSameDay(IDate $day, IDate $t) {
        return ($t->getTimeStamp() >= $day->getTimeStamp()) && ($t < $day->getTimeStamp() + 86400);
    }

    /**
     * Returns the date specified as today (which is normally today, but can
     * be specified for historic date referencing)
     * @return IDate
     */
    public final function getToday() {
        return $this->today;
    }

    /**
     * Returns the date specified as selected (e.g. for displaying a day calendar)
     * @return IDate
     */
    public final function getSelectedDate() {
        return $this->dateSelected;
    }

    /**
     * Returns the date which has to be displayed. This can be a time with hours
     * minues and seconds. The renderers have to adjust the displayed interval.
     * For a month calendar, the calenar will e.g. render the month July 2010 if
     * $dateToShow is 2010-07-15 03:05:30.
     */
    public final function getDateToShow() {
       return $this->dateToShow;
    }

    /**
     * Returns if the week shall start on Sunday (or Monday)
     * @return bool
     */
    public final function getWeekStartsOnSunday() {
        return $this->weekStartsWithSunday;
    }

    /**
     * Returns the week day index dependent on $weekStartsWithSunday, either
     * [0,1,2,3,4,5,6] (if Sun first day) or [1,2,3,4,5,6,0] (if Mon first day)
     * @return bool
     */
    public final function getWeekDayIndex() {
        return $this->weekDayIndex;
    }

    /**
     * Returns the week day abbreviation depending on $weekStartsWithSunday
     * @param int $index
     * @return string
     */
    public final function getWeekDayAbbreviation($index) {
        $index -= 1;
        if(!isset($this->weekDayIndex[$index])) {
            throw new Exception("No such week day index: $index");
        } else {
            return $this->weekDayAbbr[$this->weekDayIndex[$index]];
        }
    }

    /**
     * Retuens the time period of on division in a day calendar (in seconds).
     * @return int
     */
    public function getHourDivision() {
        return $this->hourDivision;
    }

    /**
     * Returns when the day starts for the calendar
     * @return int
     */
    public final function getDayStartsAt() {
        return $this->dayStartsAt;
    }

    /**
     * Returns when the day ends for the calendar
     * @return int
     */
    public final function getDayEndsAt() {
        return $this->dayEndsAt;
    }

    /**
     * Every time between 00:00:00 and getDayDisplayedFrom()
     * will not be displayed
     * @return int
     */
    public final function getDayDisplayedFrom() {
        return $this->dayDisplayedFrom;
    }

    /**
     * Every time > getDayDisplayedTo() to '23:59:59' will not be displayed
     * @return int
     */
    public final function getDayDisplayedTo() {
        return $this->dayDisplayedTo;
    }

    /**
     * Returns a reference to the event controller, creates a standard controller
     * of no controller instantiated/set.
     * @return GregorianCalendarEventController
     */
    public final function getEventController() {
        if(!$this->eventController instanceof GregorianCalendarEventController) {
            $this->eventController = new GregorianCalendarEventController();
        }
        return $this->eventController;
    }

    /**
     * Returns the date specified as today (which is normally today, but can
     * be specified for historic date referencing)
     * @param mixed $today
     */
    public final function setToday($today) {
        $today = is_null($today) ? (new $this->dateClass()) : (new $this->dateClass(strval($today)));
        if(!($today instanceof IDate)) {
            throw new Exception('Date classes used here must implement IDate');
        }
        $this->today = $today;
        $this->today->setSerial(null, null, null, 0, 0, 0);
    }

    /**
     * Sets the date specified as selected (e.g. for displaying a day calendar)
     * @return IDate
     */
    public final function setSelectedDate($selected) {
        $selected = is_null($selected) ? (new $this->dateClass()) : (new $this->dateClass(strval($selected)));
        if(!($selected instanceof IDate)) {
            throw new Exception('Date classes used here must implement IDate');
        }
        $this->dateSelected = $selected;
    }

    /**
     * Stets the date which has to be displayed. This can be a time with hours
     * minues and seconds. The renderers have to adjust the displayed interval.
     * For a month calendar, the calenar will e.g. render the month July 2010 if
     * $dateToShow is 2010-07-15 03:05:30.
     * @param mixed $dateToShow
     */
    public final function setDateToShow($dateToShow) {
        $dateToShow = !is_null($dateToShow) ? (new $this->dateClass(strval($dateToShow))) : (($this->today instanceof IDate) ?  clone $this->today : new $this->dateClass());
        if(!($dateToShow instanceof IDate)) {
            throw new Exception('Date classes used here must implement IDate');
        }
        $this->dateToShow = $dateToShow;
    }

    /**
     * Sets if the week shall start on Sunday (or Monday)
     * @param bool $bool
     */
    public final function setWeekStartsOnSunday($bool) {
        $this->weekStartsWithSunday = $bool ? true : false;
        $this->weekDayIndex = $this->weekStartsWithSunday ? array(0,1,2,3,4,5,6) : array(1,2,3,4,5,6,0);
    }

    /**
     * Sets when the day starts for the calendar
     * @param mixed $time
     */
    public final function setDayStartsAt($time) {
        $time = new $this->dateClass($time);
        $this->dayStartsAt = 3600*$time->getHour() + 60*$time->getMinute() + $time->getSecond();
    }

    /**
     * Sets when the day ends for the calendar
     * @param mixed $time
     */
    public final function setDayEndsAt($time) {
        $time = new $this->dateClass($time);
        $this->dayEndsAt = 3600*$time->getHour() + 60*$time->getMinute() + $time->getSecond();
    }

    /**
     * Every time between 00:00:00 and $time (les than $time)
     * will not be displayed
     * @param mixed $time
     */
    public final function setDayDisplayFrom($time) {
        $time = new $this->dateClass($time);
        $this->dayDisplayedFrom = 3600*$time->getHour() + 60*$time->getMinute() + $time->getSecond();
    }

    /**
     * Every time > $time to '23:59:59' will not be displayed
     * @param mixed $time
     */
    public final function setDayDisplayTo($time) {
        $time = new $this->dateClass($time);
        $this->dayDisplayedTo = 3600*$time->getHour() + 60*$time->getMinute() + $time->getSecond();
    }

    /**
     * Sets a new event controller
     * @param GregorianCalendarEventController $controller
     */
    public final function setEventController($controller) {
        if(is_null($controller) || $controller instanceof GregorianCalendarEventController) {
            $this->eventController = $controller;
        } else {
            throw new Exception('Calendar event controllers must be null (to unset) or GregorianCalendarEventController');
        }
    }

    /**
     * Sets the time period of on division in a day calendar (in seconds).
     * @param int $period
     */
    public function setHourDivision($period) {
        if(!is_numeric($period)) {
            throw new Exception('Day division period must be numeric');
        } else {
            $period = intval($period);
            if($period <= 0) {
                throw new Exception('Day division period must be > 0');
            } else if(3600 % $period != 0) {
                throw new Exception('An special amount of division periods must fit exactly 1 hour (3600 MOD period = 0)');
            } else {
                $this->hourDivision = $period;
            }
        }
    }

    /**
     * Returns a rendered representation of a month calendar
     * @param GregorianMonthCalendarRenderer $renderer
     * @return array
     */
	public final function renderMonthCalendar(GregorianMonthCalendarRenderer $renderer) {
        if(!$renderer instanceof GregorianMonthCalendarRenderer) {
            throw new Exception('You must use a GregorianMonthCalendarRenderer to render this');
        }
        $renderer->setParent($this);
        $monthStart = new LocalDate($this->dateToShow->getYear() . "-" . $this->dateToShow->getMonth() . "-01");
        $monthEnd   = $monthStart->getNext('month');
        $monthEnd->setTimeStamp($monthEnd->getTimeStamp()-1);
        $calStart   = $monthStart->getLast($this->weekStartsWithSunday ? 'sunday' : 'monday');
        $calEnd     = $monthEnd->getNext($this->weekStartsWithSunday ? 'sunday' : 'monday');

        if($calEnd->inWeeks() - $calStart->inWeeks() < 6) {
            $calEnd = $calEnd->getNext($this->weekStartsWithSunday ? 'sunday' : 'monday');
        }
        $calEnd->setTimeStamp($calEnd->getTimeStamp()-1);

        $selected = clone $this->getSelectedDate();
        $selected->setSerial(null,null,null,0,0,0);
        $selected = $selected->getTimeStamp();

        Tracer::trace('TZ           = ' . $monthEnd->getTimeZoneName());
        Tracer::trace('Date to show = ' . $this->dateToShow . '/' . $this->dateToShow->getWeekDay());
        Tracer::trace('Month start  = ' . $monthStart . '/' . $monthStart->getWeekDay());
        Tracer::trace('Month end    = ' . $monthEnd . '/' . $monthEnd->getWeekDay());
        Tracer::trace('Cal start    = ' . $calStart . '/' . $calStart->getWeekDay());
        Tracer::trace('Cal end      = ' . $calEnd . '/' . $calEnd->getWeekDay());
        Tracer::trace('Cal selected = ' . $this->getSelectedDate() . '/' . $selected);
        Tracer::trace("Num of weeks = " . ($calEnd->inWeeks() - $calStart->inWeeks()));

        $today = clone $this->today;
        $ts_s = $monthStart->getTimeStamp();
        $ts_e = $monthEnd->getTimeStamp();
        $wday = $week = 0;

        $events = array();
        $dayPeriod = 24*3600-1;

        $renderer->onStart();
        $renderer->onWeekStart(0);
        for($day = new LocalDate($calStart); $day->getTimeStamp() <= $calEnd->getTimeStamp(); $day = $day->getNext('day')) {
            if($wday > 6) {
                $wday = 0;
                $renderer->onWeekEnd($week++);
                $renderer->onWeekStart($week);
            }
            $events = $this->getEventController()->getEventsWhichMatch($day, $day->getTimeStamp()+$dayPeriod);
            $ts = $day->getTimeStamp();
            $renderer->onDay(clone $day, $ts < $ts_s || $ts > $ts_e, $ts==$selected, $events);
            $wday++;
        }
        $renderer->onEnd();
		return $renderer->getOutput();
	}

    /**
     * Returns a rendered representation of a day calendar
     * @param GregorianDayCalendarRenderer $renderer
     * @param $date=null
     */
    public final function renderDayCalendar(GregorianDayCalendarRenderer $renderer, $date=null) {
        if(!$renderer instanceof GregorianDayCalendarRenderer) {
            throw new Exception('You must use a GregorianDayCalendarRenderer to render this');
        }
        if(!is_null($date)) {
            $date = new $this->dateClass($date);
        } else {
            $date = clone $this->getSelectedDate();
        }

        $renderer->setParent($this);
        $dayStart = new $this->dateClass($date);
        $dayEnd   = new $this->dateClass($date);
        $dayStart->setSerial(null, null, null, 0, 0, 0);
        $dayEnd->setSerial(null, null, null, 23, 59, 59);
        $dayStartsAt = $dayStart->getTimeStamp() + $this->getDayStartsAt();
        $dayEndsAt   = $dayStart->getTimeStamp() + $this->getDayEndsAt();
        $displayFrom = $dayStart->getTimeStamp() + $this->getDayDisplayedFrom();
        $displayTo   = $dayStart->getTimeStamp() + $this->getDayDisplayedTo();
        $tick        = $this->getHourDivision();

        Tracer::trace('Date to show = ' . $date);
        Tracer::trace('Day start    = ' . $dayStart . '/' . $dayStart->toTimeString());
        Tracer::trace('Day end      = ' . $dayEnd . '/' . $dayEnd->toTimeString());
        Tracer::trace('Cal start    = ' . $dayStartsAt . '/' . ($this->getDayStartsAt()/3600));
        Tracer::trace('Cal end      = ' . $dayEndsAt . '/' . ($this->getDayEndsAt()/3600));

        $events = $this->getEventController()->getEventsWhichMatch($displayFrom, $displayTo);
        $map    = array();

        if(!empty($events)) {
            // Conditionize events: Sort the events by timestamp, assure that keys
            // are the the start timestamps.
            function lmb_sortCallback($a,$b) { return $a->getStart() > $b->getStart() ? 1 : -1; }
            usort($events, lmb_sortCallback);
            $ev = $events;
            $events = array();
            foreach($ev as $event) {
                $events[$event->getId()] = $event;
            }
            unset($ev);

            foreach($events as $event) Tracer::trace("EVENT: $event");

            // Generate a 2D map, where the row keys are the tick timestamps
            $map = array();
            for($i=$displayFrom; $i<=$displayTo; $i+=$tick) {
                $map[$i] = array();
            }

            // Place the events in the map, beginning in column 0, first all events
            // that do not overlap (to shrink the loop after that)
            $ev = $events;
            while(!empty($ev)) {
                $event = array_shift($ev);
                $row = $tick * floor($event->getStart() / $tick);

                if($row < $displayFrom) {
                    $row = $displayFrom;
                    $events[$row] = $event;
                }

                $end = $tick * ceil($event->getEnd() / $tick);
                $id  = $event->getId();
                $col = 0;
                // Loop over all known columns to check if there is space ...
                while(isset($map[$row][$col])) {
                    // Check if ther is space ...
                    $fits = true;
                    for($i=$row; $i<=$end; $i+=$tick) {
                        if($map[$i][$col]) {
                            // Not here, try next column
                            $fits = false;
                            break;
                        }
                    }
                    if($fits) {
                        break;
                    } else {
                        $col++;
                    }
                }
                // New column required, create it for the whole day
                if(!isset($fits) || !$fits) {
                    for($i=$displayFrom; $i<=$displayTo; $i+=$tick) {
                        $map[$i][$col] = false;
                    }
                }
                // Place the event in the row and column (and the rows that the
                // event takes place, mark the start row as ID, all other as
                // true (means "busy")
                $map[$row][$col] = $id;
                for($i=$row+$tick; $i<$end; $i+=$tick) {
                    $map[$i][$col] = true;
                }
            }

            // Cleanup
            unset($ev, $fits, $col, $row, $id, $end, $event, $i, $key, $nCols, $nEvents);

//            // Trace the map
//            foreach($map as $key => $row) {
//                $o = strftime("%H:%M:%S", $key) . " = ";
//                foreach($row as $id) {
//                    if($id === true) {
//                        $o .= ' busy';
//                    } else if($id === false) {
//                        $o .= ' free';
//                    } else {
//                        $o .= " " . sprintf("%04d", $id);
//                    }
//                }
//                Tracer::trace_r($o);
//            }
//            unset($o, $key, $row, $id);
        }

        $renderer->onStart($date, $tick, $displayFrom, $dayStartsAt, $dayEndsAt, $displayTo, &$events);
        for($time = $dayStart->getTimeStamp(); $time <= $dayEnd->getTimeStamp(); $time += $tick) {
            $renderer->onTick(new $this->dateClass($time), $tick, $time < $dayStartsAt || $time > $dayEndsAt, $time < $displayFrom || $time > $displayTo, &$events, &$map);
        }
        $renderer->onEnd();
		return $renderer->getOutput();
    }
}
?>

<?php
/**
 * Gegorian Month Calendar renderer. Overwrite this class if you want to have an
 * alterantive rendered representation of the month calendar.
 * @package de.atwillys.sw.php.swLib
 * @author Stefan Wilhelm
 * @copyright Stefan Wilhelm, 2010
 * @license GPL
 * @version 1.0
 * @uses GregorianCalendar
 * @uses GregorianCalendarEvent
 */
class GregorianMonthCalendarRenderer {

    /**
     * Defines the parent calendar
     * @var GregorianCalendar
     */
    private $parent = null;

    /**
     * The output of the rendering process
     * @var string
     */
    protected $output = '';

    /**
     * Constructor
     * @param GregorianCalendar $parentGregorianCalendar
     */
    public function __construct($parentGregorianCalendar = null) {
        $this->parent = $parentGregorianCalendar;
    }

    /**
     * Sets the parent of the calendar
     * @param GregorianCalendar $calendar
     */
    public final function setParent(GregorianCalendar $calendar) {
        if(!is_null($this->parent) && $this->parent !== $calendar) {
            throw new Exception('You cannot re-assign the parent calendar instance');
        } else {
            $this->parent = $calendar;
        }
    }

    public final function getParent() {
        return $this->parent;
    }

    /**
     * Returns the output of the rendering process
     * @return string
     */
    public final function getOutput() {
        return $this->output;
    }

    /**
     * Callback to start the calendar - every HTML content before the weeks
     * (and days in the weeks). Normally a table, thead, th with weekday names.
     * @return void
     */
    public function onStart() {
        $date = $this->getParent()->getDateToShow();
        $o = '';
        $o .= '<table class="gregorian-month-calendar">';
        $o .= '<caption>';
        $o .= '<a class="prev" href="' . $_SERVER['PHP_SELF'] . '?month-calendar-show=' . $date->getLast('month')->toDateString() . '">' . '&nbsp;' . '</a>';
        $o .= '<a class="next" href="' . $_SERVER['PHP_SELF'] . '?month-calendar-show=' . $date->getNext('month')->toDateString() . '">' . '&nbsp;' . '</a>';
        $o .= '<a class="title">' . ucfirst(strftime("%B %Y", $date->getTimeStamp())) . '</a>';
        $o .= '</caption>';
        for($i=1; $i<=7; $i++) {
            $o .= '<col class="' . $this->getParent()->getWeekDayAbbreviation($i) .'" />';
        }
        $o .= '<tr>';
        foreach($this->getParent()->getWeekDayIndex() as $i) {
            $o .= '<th><a>' . strtoupper(substr(gmstrftime('%A', ($i-4)*24*3600), 0, 1)) .'</a></th>';
        }
        $o .= '</tr>';
        $this->output .= $o;
    }

    /**
     * All HTML to close the table, table footer etc.
     * @return void
     */
    public function onEnd() {
        $this->output .= '</table>';
    }

    /**
     * Start a new week, normally a <tr> or an additional <td></td> with the
     * calendar week (e.g. CW45) etc.
     * @return void
     */
    public function onWeekStart($weekId) {
        $this->output .= '<tr>';
    }

    /**
     * Finish a new week, normally a </tr>
     * @return void
     */
    public function onWeekEnd($weekId) {
        $this->output .= "</tr>\n";
    }

    /**
     * Renders a day in the week (normally a <tr>$dateDayOfMonthTwoDigits</tr>
     * @param IDate $day
     * @param bool isPadding
     * @param bool isSelected
     * @param GregorianMonthCalendarRenderer[] $events
     * @return mixed
     */
    public function onDay(IDate $day, $isPadding, $isSelected, $events) {
        $class = array();
        $class[] = $this->getParent()->getWeekDayAbbreviation($day->getWeekDay());
        switch(Math::signz($day->getTimeStamp() - $this->getParent()->getToday()->getTimeStamp())) {
            case -1: $class[] = 'passed'; break;
            case 0 : $class[] = 'today'; break;
        }
        if(!empty($events)) $class[] = 'has-events';
        if($isPadding) $class[] = 'padding';
        if($isSelected) $class[] = 'selected';
        $class = 'class="' . implode(' ', $class) . '"';
        $title = 'title="' . ucwords(strftime("%A, %B %e, %Y", $day->getTimeStamp())) . (count($events) == 0 ? '' : ' (' . count($events) . ' events)').  '"';
        $date = '<a href="' . $_SERVER['PHP_SELF'] . '?month-calendar-select=' . $day->toDateString() . '">' . sprintf("%02d", $day->getDay()) . '</a>';
        $day->isPadding = $isPadding;
        $this->output .= "<td $class $title>$date</td>";
    }
}
?>

<?php
/**
 * Gegorian Day Calendar renderer class. Overwrite this class if you want a
 * different rendered representation.
 * @package de.atwillys.sw.php.swLib
 * @author Stefan Wilhelm
 * @copyright Stefan Wilhelm, 2010
 * @license GPL
 * @version 1.0
 * @uses GregorianCalendar
 * @uses GregorianCalendarEvent
 */
class GregorianDayCalendarRenderer {

    /**
     * Defines the parent calendar
     * @var GregorianCalendar
     */
    private $parent = null;

    /**
     * The output of the rendering process
     * @var string
     */
    protected $output = '';

    /**
     * The HTML element id of the table
     * @var string
     */
    protected $tableId = '';

    /**
     * Constructor
     * @param GregorianCalendar $parentGregorianCalendar
     */
    public function __construct($parentGregorianCalendar = null) {
        $this->parent = $parentGregorianCalendar;
    }

    /**
     * Sets the parent of the calendar
     * @param GregorianCalendar $calendar
     */
    public final function setParent(GregorianCalendar $calendar) {
        if(!is_null($this->parent) && $this->parent !== $calendar) {
            throw new Exception('You cannot re-assign the parent calendar instance');
        } else {
            $this->parent = $calendar;
        }
    }

    /**
     * Returns the parent Calendar object (which the renderer is for)
     * @return GregorianCalendar
     */
    public final function getParent() {
        return $this->parent;
    }

    /**
     * Sets the id of the table that contains the day calendar
     * @param string $id
     */
    public final function setTableId($id) {
        if(empty($id)) {
            $this->tableId = '';
        } else {
            $this->tableId = trim($id);
        }
    }

    /**
     * Returns the id of the table that contains the day calendar
     * @return string
     */
    public final function getTableId() {
        return $this->tableId;
    }

    /**
     * Returns the output of the rendering process
     * @return string
     */
    public final function getOutput() {
        return $this->output;
    }

    /**
     * Callback to start the calendar - every HTML content before the weeks
     * (and days in the weeks). Normally a table, thead, th with weekday names.
     * @param IDate $date
     * @param int $tick
     * @param int $displayFrom
     * @param int $dayStartsAt
     * @param int $dayEndsAt
     * @param int $displayTo
     * @param GregorianCalendarEvent[] &$events
     * @return void
     */
    public function onStart($date, $tick, $displayFrom, $dayStartsAt, $dayEndsAt, $displayTo, &$events) {
        $id = empty($this->tableId) ? '' : ('id="' . str_replace('#', '', $this->tableId) . '" ');
        $o = '';
        $o .= '<table ' . $id . 'class="gregorian-day-calendar">';
        $o .= '<caption>' . ucwords(strftime("%A, %B %e, %Y", $date->getTimeStamp())) . '</caption>';
        $this->output .= "$o\n";
    }

    /**
     * All HTML to close the table, table footer etc.
     * @return void
     */
    public function onEnd() {
        $this->output .= "</table>\n";
        $this->events = array();
    }

    /**
     * Renders one tick with the division period, padding means that the day
     * of the calendar has either not started yet or already ended (according
     * to $dayStartsAt and $dayEndsAt)
     * @param IDate $date
     * @param int $tick
     * @param bool $isPadding
     * @param bool $hidden
     * @param GregorianCalendarEvent[] &$events
     * @param array() &$eventMap
     * @return void
     */
    public function onTick(IDate $date, $tick, $isPadding, $hidden, &$events, &$eventMap) {
        if($hidden) return;
        $class = array();
        if($isPadding) $class[] = 'padding';
        if($date->getMinute() == 0) {
            $class[] = 'full-hour';
            $fullHourTime = sprintf("%02d:%02d", $date->getHour(), $date->getMinute());
        } else {
            $fullHourTime = '&nbsp;';
        }        

        $time = sprintf("%04d-%02d-%02d %02d:%02d", $date->getYear(), $date->getMonth(), $date->getDay(), $date->getHour(), $date->getMinute());

        if(!empty($events)) {
            $ts = $date->getTimeStamp();
            $row = &$eventMap[$ts];
            $td = '';
            $td .= '<td class="space"></td>' . "\t";
            foreach($row as $index => $col) {
                if($col === true) {
                    $td .= '<td class="space"></td>' . "\t";
                    $td .= '<!-- rowspan -->'; // event running, managed by rowspan
                } else if(is_null($col)) {
                    $td .= '<!-- colspan -->'; // event running, managed by colspan
                } else if($col === false) {
                    $td .= '<td class="space"></td>' . "\t";
                    $td .= '<td class="data">&nbsp;</td>'; // an empty cell
                } else if(is_numeric($col)) {
                    $td .= '<td class="space"></td>' . "\t";
                    $event = $events[$col]; // by ID

                    // $rows = (ceil($event->getEnd() / $tick) - ($ts / $tick));
                    $cols = count($row) - $index; // index starts at 0 --> min cols=1, max=count(...)
                    $rows = 1;
                    for($i=$ts; $i < $ts + ($tick * count($eventMap)); $i+=$tick) {
                        if(isset($eventMap[$i+$tick][$index]) && $eventMap[$i+$tick][$index] === true) {
                            $rows++;
                        } else {
                            break;
                        }
                    }

                    for($i=$ts; $i < $ts+($tick * $rows); $i+=$tick) {
                        if(!isset($eventMap[$i][$index+1]) || $eventMap[$i][$index+1] !== false) {
                            $cols = 1;
                            break;
                        }
                    }

                    if($cols > 1) {
                        for($i=$ts; $i < $ts+($tick * $rows); $i+=$tick) {
                            for($j=$index; $j < count($row); $j++) {
                                $eventMap[$i][$j] = null;
                            }
                        }
                        $row[$index] = $event->getId();
                        // Take the spacers into account
                        $cols = 2*$cols-1;
                    }
                    $td .= $this->renderEvent($event, $rows, $cols);
                }
                $td .= "\n";
            }
        } else {
            $td = '<td class="space"></td><td></td>';
        }

        $o = '';
        $o .= '<tr value="' . $time . '" ' .  (empty($class) ? '' : (' class="' . implode(' ', $class)) . '"') . '>' . "\n";
        $o .= "\t" . '<td class="time" title="' . $time . '" >' . $fullHourTime .  '</td>' . "\n";
        $o .= $td;
        $o .= '</tr>' . "\n";
        $this->output .= $o;
    }

    /**
     * Returns a day calendar string representation of an event
     * @param GregorianCalendarEvent $event
     * @param int $rows
     * @params int $cols
     * @return string
     */
    protected function renderEvent(GregorianCalendarEvent $event, $rows, $cols) {
        $td = '<td class="data event" rowspan="' . $rows . '" colspan="' . $cols . '">';
        $td .= (method_exists($event, 'render')) ? $event->render($this) : '';
        $td .= "</td>";
        return $td;
    }
}
?>

<?php
/**
 * Base class for Gegorian Calendar Events. Events are defined using a unique
 * identifier (e.g. the primary key of a database or file), a start timestamp
 * and an end timestamp. The configuration of the GregorianCalendar class
 * decides if these timestamps are interpreted as local or UTC. Further "fixed"
 * properties are the type of event (e.g. "meeting", "festival" ...) and an
 * associative data array that contains variable information about the particular
 * event. All keys in the array can be accessed like properties (if e.g.
 *  $data = array(
 *      'where' => 'there',
 *      'who' => 'me',
 *      'why' => 'because'
 *  );
 *
 * Then $event->where === 'there'.
 * Note that you should choose "PHP-variable-name-conform" array keys for this,
 * alternatively you can use the data getter: $event->getData("who") === "me".
 *
 * @package de.atwillys.sw.php.swLib
 * @author Stefan Wilhelm
 * @copyright Stefan Wilhelm, 2010
 * @license GPL
 * @version 1.0
 */
class GregorianCalendarEvent {

    /**
     * A unique identifier of the event
     * @var string
     */
    protected $id = '';

    /**
     * The name of the event
     * @var string
     */
    protected $name = 'New Event';

    /**
     * The timestamp when the event starts
     * @var IDate
     */
    protected $start = 0;

    /**
     * The timestamp when the event ends
     * @var IDate
     */
    protected $end = 0;

    /**
     * The type of event
     * @var string
     */
    protected $type = '';

    /**
     *
     * @var array
     */
    protected $data = array();

    /**
     * GregorianCalendarEvent constructor
     * @param string $name=''
     * @param mixed $start=null
     * @param mixed $end=null
     * @param array $data=array()
     * @param string $type=null
     * @param mixed $id=null
     */
    public function __construct($name='', $start=null, $end=null, array $data=array(), $type=null, $id=null) {
        if($start instanceof IDate) {
            $start = $start->getTimeStamp();
        } else if(empty($start)) {
            $start = time();
        } else if(!is_numeric($start)) {
            throw new Exception('GregorianCalendarEvent start time must be an integer timestamp');
        }

        if($end instanceof IDate) {
            $end = $end->getTimeStamp();
        } else if(empty($end)) {
            $end = $start + 3600;
        } else if(!is_numeric($end)) {
            throw new Exception('GregorianCalendarEvent end time must be an integer timestamp');
        }

        if($end <= $start) {
            throw new Exception('GregorianCalendarEvent end time must be later than the start time');
        }

        $this->start = $start;
        $this->end = $end;

        if(!empty($name)) $this->name = trim($name);
        if(!empty($id)) $this->id = $id;
        if(!empty($data)) $this->data = $data;
        if(!empty($type)) $this->type = trim($type); // implicit toString
    }

    /**
     * Returns a property if the correcponding getter (get<$ame>)exists OR the
     * key exsts in the data array. Returns null if none of both exists. Note
     * that values stored in the data array are inaccessible if a the key is
     * e.g. "from", "to", "name" etc.
     * @param string $name
     */
    public function __get($name) {
        if(method_exists($this, 'get' . $name)) {
            $name = 'get' . $name;
            return $this->$name();
        } else if(isset($this->data[$name])) {
            return $this->data[$name];
        } else {
            return null;
        }
    }

    /**
     * Sets a property if the correcponding setter (set<$ame>) exists. If no
     * setter is found, then the value will be stored in the data array. Note
     * that values stored in the data array are inaccessible if a the key is
     * e.g. "from", "to", "name" etc. The setter will be called instead.
     * @param string $name
     * @param mixed $value
     * @return void
     */
    public function __set($name,  $value) {
        if(method_exists($this, 'set' . $name)) {
            $name = 'set' . $name;
            $this->$name($value);
        } else {
            $this->data[$name] = $value;
        }
    }

    /**
     * String representation
     * @return string
     */
    public function  __toString() {
        return '['
            . strftime('%Y-%m-%d %H:%M:%S %Z', $this->start)
            . ' - '
            . strftime('%Y-%m-%d %H:%M:%S %Z', $this->end)
            . '] '
            . $this->name
            . ' {id="' . $this->id . '", type="'. $this->type
            . '", data=["' . implode('", "', $this->data) . '"]'
            . '}';
    }

    /**
     * Returns the id of the event
     * @return mixed
     */
    public function getId() {
        return $this->id;
    }

    /**
     * Returns the name of the event
     * @return string
     */
    public function getName() {
        return $this->name;
    }

    /**
     * Returns when the event starts
     * @return IDate
     */
    public function getStart() {
        return $this->start;
    }

    /**
     * Returns when the event ends
     * @return IDate
     */
    public function getEnd() {
        return $this->end;
    }

    /**
     * Returns the type of event
     * @return string
     */
    public function getType() {
        return $this->type;
    }

    /**
     * Returns the data array or a valued in the data array specified using
     * $key, null if not found
     * @return mixed
     */
    public function getData($key=null) {
        if(!empty($key)) {
            return isset($this->data[$key]) ? $this->data[$key] : null;
        } else {
            return $this->data;
        }
    }

    /**
     * Sets the id of the event
     * @param mixed $id
     */
    public function setId($id) {
        if(!is_scalar($id)) {
            throw new Exception('An calendar event ID cannot be an object or array');
        } else {
            $this->id = $id;
        }
    }

    /**
     * Sets the name of the event
     * @param string $name
     */
    public function setName($name) {
        $this->name = trim($name);
    }

    /**
     * Sets the timestamp when the event starts
     * @param int $from
     */
    public function setFrom($from) {
        if(is_numeric($from)) {
            $this->start = intval($from);
        } else if($from instanceof IDate) {
            $this->start = $from->getTimeStamp();
        } else {
            throw new Exception('"from"-data is no timestamp');
        }
    }

    /**
     * Sets when the event ends
     * @param int $to
     */
    public function setTo($to) {
        if(is_numeric($to)) {
            $this->end = intval($to);
        } else if($to instanceof IDate) {
            $this->end = $to->getTimeStamp();
        } else {
            throw new Exception('"to"-data is no timestamp');
        }
    }

    /**
     * Sets the event type
     * @param string $type
     */
    public function setType($type) {
        $this->type = trim(strtolower($type));
    }

    /**
     * Sets the custom/user data array
     * @param array $data
     */
    public function setData(array $data) {
        $this->data = $data;
    }
}
?>

<?php
/**
 * Gegorian Calendar event controller. Overwrite the methods to customize
 * the controller (load from file, from database etc. The class ist not
 * abstract because it will be instantiated by the calendar if no derived
 * controller is specified. This class methods returns empty date, which have
 * no effect.)
 * @package de.atwillys.sw.php.swLib
 * @author Stefan Wilhelm
 * @copyright Stefan Wilhelm, 2010
 * @license GPL
 * @version 1.0
 * @uses GregorianCalendarEvent
 */
class GregorianCalendarEventController {

    /**
     * Returns all events that start and end between a given period. E.g. if
     * $from and $to are the timestamps for 00:00 to 23:59:59 on the same day,
     * then the method would return all events that take place exactly on this
     * day (start and end on this day).
     * @param mixed $from
     * @param mixed $to
     * @return GregorianCalendarEvent[]
     */
    public function getEventsBetween($from, $to) {
        return array();
    }

    /**
     * Returns all events that do not end before and do not start after the given
     * time period. If e.g. $from and $to are timespamps corresponding to 00:00
     * and 23::59:59 on the same day, then this method would return all events
     * that partially or completely take place on this day.
     * @param mixed $from
     * @param mixed $to
     * @return GregorianCalendarEvent[]
     */
    public function getEventsWhichMatch($from, $to) {
        return array();
    }

    /**
     * Returns an event given by its identifier
     * @return GregorianCalendarEvent
     */
    public function getEvent($id) {
        return null;
    }

}
?>

CSS recommendation

/**
 * Common CSS
 * @author Stefan Wilhelm
 * @package de.atwillys.php.swlib.css.std
 */

body, abbr, acronym, address, area, b, base, basefont, big, blockquote, br, button,
caption, center, cite, code, col, colgroup, dd, del, del, dfn, dir, div, dl, dt, em,
fieldset, font, form, h1, h2, h3, h4, h5, hr, i, iframe, img, input, ins, isindex,
kbd, label, li, map, menu, object, ol, option, p, pre, s, samp, select, small, span,
strike, strong, sub, sup, table, tbody, td, textarea, tfoot, th, thead, title, tr,
tt, u, ul, var, a, a:link, a:hover, a:visited, hr, img, code, pre, tt, samp, input,
textarea, select, option {
    font-family: "Helvetica", "Verdana", monospace;
    background: transparent;
    margin: 0px 0px 0px 0px;
    padding: 0px 0px 0px 0px;
    text-decoration: none;
    color: #000000;
    font-size: 11pt;
    line-height: 13pt;
    border: none 0px;
}

body {
    padding:10px 10px 10px 10px;
}

h1 {
    font-size: 17pt;
}

h2 {
    font-size: 16pt;
}

h3 {
    font-size: 14pt;
}

h4, h5 {
    font-size: 11pt;
}

a:link, a:visited {
    font-weight: bold;
    color: #000099;
}

a:hover {
    color: #990000;
}

img {
    margin: 5px 5px 5px 5px;
}

hr {
    color:black;
    background-color:black;
    border:0px none;
    border-bottom:1px solid black;
    width:100%
}

code, pre, tt, samp {
    font-family: monospace;
    color:#000000;
}

form {
    margin:0px;
    padding:2px;
    border:0px none;
}

/**
 * CSS for Gregorian month calendars
 * @author Stefan Wilhelm
 * @package de.atwillys.php.swlib.cal
*/

table.gregorian-month-calendar {
    width: 245px;
    border-collapse: collapse;
    border-spacing: 0px;
}

table.gregorian-month-calendar caption {
    margin: 0px 0px 0px 0px;
    padding: 0px 0px 0px 0px;
    background: #bbbbbb;
    color: #111111;
    vertical-align: middle;
    text-align: center;
    font-size: 14pt;
    line-height: 18pt;
    margin-bottom: 2px;
    background-color: #eeeeee;
    border: ridge 1px #666666;
    border-radius: 3px;
    -webkit-border-radius: 3px;
    -moz-border-radius: 3px;
}

table.gregorian-month-calendar caption a,
table.gregorian-month-calendar caption a:link,
table.gregorian-month-calendar caption a:visited {
    display:block;
    margin: 0px 0px 0px 0px;
    padding: 0px 0px 0px 0px;
    background: #bbbbbb;
    color: #111111;
    vertical-align: middle;
    text-align: center;
    font-size: 14pt;
    line-height: 18pt;
    background-color: #eeeeee;
    border: ridge 1px #666666;
    border-radius: 3px;
}

table.gregorian-month-calendar caption a.prev,
table.gregorian-month-calendar caption a.prev:link,
table.gregorian-month-calendar caption a.prev:visited {
    margin: 0px 10px 0px 10px;
    border: none;
    width: 10px;
    float: left;
    background: transparent;
    background-image: url("prev.png");
    background-repeat: no-repeat;
    background-position: center center;
}

table.gregorian-month-calendar caption a.next,
table.gregorian-month-calendar caption a.next:link,
table.gregorian-month-calendar caption a.next:visited {
    margin: 0px 10px 0px 10px;
    border: none;
    width: 10px;
    float: right;
    background: transparent;
    background-image: url("next.png");
    background-repeat: no-repeat;
    background-position: center center;
}

table.gregorian-month-calendar caption a.title,
table.gregorian-month-calendar caption a.title:link,
table.gregorian-month-calendar caption a.title:visited {
    width:auto;
}

table.gregorian-month-calendar th,
table.gregorian-month-calendar td {
    vertical-align: middle;
    text-align: center;
}

table.gregorian-month-calendar th a,
table.gregorian-month-calendar th a:link,
table.gregorian-month-calendar th a:visited,
table.gregorian-month-calendar th a:hover,
table.gregorian-month-calendar td a,
table.gregorian-month-calendar td a:link,
table.gregorian-month-calendar td a:visited,
table.gregorian-month-calendar td a:hover {
    display: block;
    width: 35px;
    height: 20px;
    vertical-align: middle;
    text-align: center;
    border: ridge 1px #666666;
    border-radius: 3px;
    -webkit-border-radius: 3px;
    -moz-border-radius: 3px;
}

table.gregorian-month-calendar th a,
table.gregorian-month-calendar th a:link,
table.gregorian-month-calendar th a:visited {
    padding: 2px 2px 2px 2px;
    background-color: #979887;
    margin-bottom: 2px;
}

table.gregorian-month-calendar td a,
table.gregorian-month-calendar td a:link,
table.gregorian-month-calendar td a:visited,
table.gregorian-month-calendar td a:hover {
    color: #111111;
    padding: 7px 2px 7px 2px;
    background-color: #bbbbbb;
}

table.gregorian-month-calendar td.passed a,
table.gregorian-month-calendar td.passed a:link,
table.gregorian-month-calendar td.passed a:visited {
    color: #555555;
}

table.gregorian-month-calendar td.padding a,
table.gregorian-month-calendar td.padding a:link,
table.gregorian-month-calendar td.padding a:visited {
    color: #bbbbbb;
    background-color: #eeeeee;
    border: ridge 1px #bbbbbb;
}

table.gregorian-month-calendar td.sat a,
table.gregorian-month-calendar td.sat a:link,
table.gregorian-month-calendar td.sat a:visited,
table.gregorian-month-calendar td.sun a,
table.gregorian-month-calendar td.sun a:link,
table.gregorian-month-calendar td.sun a:visited {
    background-color: #eeee66;
}

table.gregorian-month-calendar td.today a,
table.gregorian-month-calendar td.today a:link,
table.gregorian-month-calendar td.today a:visited {
    color: #ff0077;
}

table.gregorian-month-calendar td.selected a,
table.gregorian-month-calendar td.selected a:link,
table.gregorian-month-calendar td.selected a:visited {
    color: #222222;
    background-color: #66ee66;
    border: ridge 1px #666666;
}

table.gregorian-month-calendar td.has-events a,
table.gregorian-month-calendar td.has-events a:link,
table.gregorian-month-calendar td.has-events a:visited {
    background-image: url('has-events.png');
    background-repeat: no-repeat;
}

table.gregorian-month-calendar td a:hover,
table.gregorian-month-calendar td.selected a:hover,
table.gregorian-month-calendar td.has-events a:hover,
table.gregorian-month-calendar td.sat a:hover,
table.gregorian-month-calendar td.sun a:hover,
table.gregorian-month-calendar td.padding a:hover {
    color: #222222;
    background-color: #44cc44;
    border: ridge 1px #666666;
}

/**
 * CSS for Gregorian day calendars
 * @author Stefan Wilhelm
 * @package de.atwillys.php.swlib.cal
*/

table.gregorian-day-calendar {
    width: 100%;
    border-collapse: collapse;
    border-spacing: 0px;
}

table.gregorian-day-calendar caption {
    margin: 0px 0px 0px 0px;
    padding: 0px 0px 0px 0px;
    width: 99.9%;
    background: #eeeeee;
    color: #111111;
    vertical-align: middle;
    text-align: center;
    font-size: 14pt;
    line-height: 18pt;
    margin-bottom: 2px;
    border: ridge 1px #666666;
    border-radius: 3px;
    -webkit-border-radius: 3px;
    -moz-border-radius: 3px;
}

table.gregorian-day-calendar td {
    vertical-align: middle;
    padding: 0px 0px 0px 0px;
    text-align: left;
    border-top: dotted 1px #aaaaaa;
    border-bottom: dotted 1px #aaaaaa;
}

table.gregorian-day-calendar td.time {
    text-align: right;
    width: 80px;
    height: 20px;
}

table.gregorian-day-calendar tr.full-hour td.time {
    border-top: solid 1px #000000;
}

table.gregorian-day-calendar tr.padding {
    background-color: #eeeeee;
}

table.gregorian-day-calendar tr td.space {
    width: 4px;
}

table.gregorian-day-calendar tr td.data.event {
    padding: 4px 4px 4px 4px;
    vertical-align: top;
    border: 0px;
    border-collapse: collapse;
    background: #aaaaff;
    box-shadow:0 0 1px #000000;
    -webkit-box-shadow:0 0 1px #000000;
    -moz-box-shadow:0 0 1px #000000;
    border-radius: 10px;
    -webkit-border-radius: 10px;
    -moz-border-radius: 10px;
}

table.gregorian-day-calendar tr td.event div {
}
Posted in PHP | Tagged , | Comments Off

Growl UDP notification class

Growl is a well known, powerful tool for the Mac (and now for other platforms as well), which displays small notification messages on your screen. Any program can register itself as a source of messages and then send notifications. Additionally, Growl can listen to a UDP port for the same purpose. The PHP class in this article uses this remote messaging protocol for notifying local or remote computers. The Growl protocol is not really complex, but here a short overview how “UDP-Growling” works:

  • Growl wants to know which program sent an incoming notification and which type of notification was received (a warning, error, new email, new Skype message …). This allows you to set up what to do with these messages in your Growl configuration panel.
  • Hence, there are two major message types in the UDP protocol: Registration and Notification. The registration packets register a new application and specify which types of notification are sent from this application. The notification messages contain the messages that you want to display.
  • The notification messages contain title, description, priority, and if they are sticky (keep being displayed until you click them)
  • For security reasons, Growl has a password protection for UDP remote registrations

Risk a glance at the sample source code so see how it works:

Sample source code

&lt;?php
require_once(dirname(&quot;GrowlNotifier.class.php&quot;);
print '&lt;html&gt;&lt;body&gt;&lt;pre&gt;';

// Sets defaults
GrowlNotifier::config(array(
    'server' =&gt; '127.0.0.1',            // default server ip (this computer)
    'password' =&gt; '',                   // default password
    'application' =&gt; 'PhpGrowler',      // default application identifier
    'register' =&gt; true,                 // send registrations?
    'notifications' =&gt; array(           // defaule registered notifications
        'message','warning', 'error',   // default enabled notifications
        'disabled notify' =&gt; false,     // explicity disabled notification
        'enabled notify' =&gt; 'true'      // explicity enabled notification
    )
));

// Instance using default config
$growl = new GrowlNotifier();
$growl-&gt;notify('Hello Growl', &quot;This is a message for you&quot;);

// Instance using config specified in constructor
$growl = new GrowlNotifier('localhost', '');
$growl-&gt;notify('Hello localhost', &quot;This is another message for you&quot;);

print '&lt;/pre&gt;&lt;/body&gt;&lt;/html&gt;';
?&gt;

Output

To see the output of this example “out of the box”, go to your Growl settings pane under “Network”, and tick the check boxes “Listen for incoming notifications” and “Allow remote application registration”. Leave the password field blank. Well – I assume that Growl is installed and your Apache server is running on your local machine. Probably a system message “Allow incoming network transfers for GrowlHelper” will be displayed – the best is to say “always allow”.

Class source code

&lt;?php
/**
 * Growl notification class
 * @package de.atwillys.php.swLib
 * @version 1.0
 * @author Stefan Wilhelm, 2010
 * @license GPL
 */
class GrowlNotifier {

    /**
     * Class configuration defaults
     * @ststicvar array
     */
    private static $config = array(
        'server' =&gt; '127.0.0.1',            // default server ip
        'password' =&gt; '',                   // default password
        'application' =&gt; 'PhpGrowler',      // default application identifier
        'register' =&gt; true,                 // send registrations?
        'notifications' =&gt; array(           // defaule registered notifications
            'message','warning', 'error',   // default enabled notifications
            //'something else' =&gt; false     // disabled notification
        )
    );

    /**
     * The version of the protocol
     * @const int
     */
    const PROTOCOL_VERSION = 1;

    /**
     * Packacke type registration
     * @const int
     */
    const TYPE_REGISTRATION = 0;

    /**
     * Packacke type notification
     * @const int
     */
    const TYPE_NOTIFICATION = 1;

    /**
     * Growl UDP port
     * @const int
     */
    const UDP_PORT = 9887;

    /**
     * The Growl server/computer IP address
     * @var string
     */
    private $server = '127.0.0.1';

    /**
     * The Growl server/computer password
     * @var string
     */
    private $password = '';

    /**
     * Application, which is sent to identify the message source
     * @var string
     */
    private $application = 'PhpGrowler';

    /**
     * Available notification types
     * @var array
     */
    private $registeredNotifications = array('message');

    /**
     * Helper to achieve that the registration is only sent once.
     * @var bool
     */
    private $hasRegistered = null;

    /**
     * Class configuration. Sets the specified config settings (merges
     * with the existing). Returns the actual configuration after the
     * new array has been merged to the defaults/previous settings.
     * @param array $config
     * @return array
     */
    public static final function config($config=array()) {
        if(!is_array($config)) {
            throw new Exception('GrowlNotifier config is no array');
        } else {
            self::$config = array_merge(self::$config, $config);
        }
        return self::$config;
    }

    /**
     * Sends an UDP application registration package to the specified growl
     * server/computer. The notifications array can be either associative
     * (key is the notification name, value is a boolean value that describes
     * that the notification is enabled) or numerical indexed (key is a number,
     * value is the notification, enabled by default true). $server is the IP
     * address of the server, $password the password of the server, $application
     * the name or slug or any string identifier of the application.
     * @param string $server
     * @param string $password
     * @param string $application
     * @param array $notifications
     */
    private static function sendUdpRegistration($server, $password, $application, array $notifications) {
        $application = utf8_encode($application);
        $encoded = $defaults = '';
        $ne = $nd = 0;
        foreach($notifications as $notification =&gt; $enabled) {
            $enabled = (bool) $enabled;
            $notification = utf8_encode(trim($notification));
            $encoded .= pack('n', strlen($notification)) . $notification;
            $ne++;
            if($enabled !== false) { $defaults .= pack('c', $ne-1); $nd++; }
        }
        $data = pack(
            'c2nc2', self::PROTOCOL_VERSION, self::TYPE_REGISTRATION,
            strlen($application), $ne, $nd
        ) . $application . $encoded . $defaults;
        $data .= pack('H32', md5($data . trim($password)));
        $sock = @socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
        if($sock == false) {
            throw new Exception('Sending registration failed: ' . socket_strerror(socket_last_error()));
        }
        if(@socket_sendto($sock, $data, strlen($data), MSG_EOF, $server, self::UDP_PORT) &lt; 0) {
            @socket_close($sock);
            throw new Exception('Sending registration failed: ' . socket_strerror(socket_last_error()));
        }
        @socket_close($sock);
    }

    /**
     * Sends an UDP application notification package to the specified growl
     * server/computer. $server is the IP address of the server, $password
     * the password of the server, $application the name or slug or any string
     * identifier of the application, $notification one of the registered
     * notification identifiers - rest of parameters are self explaining.
     * @param string $server
     * @param string $password
     * @param string $application
     * @param string $notification
     * @param string $title=''
     * @param string $description=''
     * @param int $priority=0
     * @param bool $sticky=false
     */
    private static function sendUdpNotification($server, $password, $application, $notification, $title='',
                                                $description='', $priority=0, $sticky=false) {
        $application = utf8_encode(trim($application));
        $notification = utf8_encode(trim($notification));
        $description = utf8_encode(trim($description));
        $title = utf8_encode(trim($title));
        $priority = intval($priority);
        $data = pack('c2n5', self::PROTOCOL_VERSION, self::TYPE_NOTIFICATION,
            (2*(intval($priority) &amp; 7)) | (intval($priority) &lt; 0 ? 8 : 0) | ($sticky==true ? 256 : 0),
            strlen($notification), strlen($title), strlen($description), strlen($application)
        ) . $notification . $title . $description . $application;
        $data .= pack('H32', md5($data . trim($password)));
        $sock = @socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
        if($sock == false) {
            throw new Exception('Sending notification failed: ' . socket_strerror(socket_last_error()));
        }
        if(@socket_sendto($sock, $data, strlen($data), MSG_EOF, $server, self::UDP_PORT) &lt; 0) {
            @socket_close($sock);
            throw new Exception('Sending notification failed: ' . socket_strerror(socket_last_error()));
        }
        @socket_close($sock);
    }

    /**
     * Constructor
     * @param string $server
     * @param string $password
     * @param array $registeredNotifications
     */
    public function __construct($server=null, $password=null, $application=null, $registeredNotifications=array()) {
        $this-&gt;setServer(trim($server) != '' ? $server : self::$config['server']);
        $this-&gt;setPassword(!empty($password) ? $password : self::$config['password']);
        $this-&gt;setApplication(trim($application) != '' ? $application : self::$config['application']);
        $this-&gt;setRegisteredNotifications(!empty($registeredNotifications) ? $registeredNotifications : self::$config['notifications']);
    }

    /**
     * Returns the IP address of the server to send registrations/notifications to.
     * @return string
     */
    public function getServer() {
        return $this-&gt;server;
    }

    /**
     * Sets the new server IP address
     * @param string $server
     */
    public function setServer($server) {
        $this-&gt;server = trim($server);
    }

    /**
     * Returns the password of the server to send registrations/notifications to.
     * @return string
     */
    public function getPassword() {
        return $this-&gt;password;
    }

    /**
     * Sets the new server password
     * @param string $password
     */
    public function setPassword($password) {
        $this-&gt;password = strval($password);
    }

    /**
     * Returns the application identifier, under which the messages are send.
     * @return string
     */
    public function getApplication() {
        return $this-&gt;application;
    }

    /**
     * Sets the new application identified, under which messages are send.
     * @param strig $application
     */
    public function setApplication($application) {
        $this-&gt;application = trim($application);
    }

    /**
     * Returns an assoc. array containing the registered notification types
     * (or the notifications to register). Keys a the notification names, values
     * are the enabled-status (bool).
     * @return array
     */
    public function getRegisteredNotifications() {
        return $this-&gt;registeredNotifications;
    }

    /**
     * Sets the new registerd notifications..
     * Wants an assoc. array containing the registered notification types
     * (or the notifications to register). Keys a the notification names, values
     * are the enabled-status (bool). The first registered notification is the
     * default one, which will be used if the corresponding argumrnt in notify()
     * is empty.
     * @param array $notifications
     */
    public function setRegisteredNotifications($notifications) {
        if(!is_array($notifications)) {
            throw new Exception('Notifications to register must be passed as array');
        } else if(empty($notifications)) {
            throw new Exception('No notifications defined to register');
        } else {
            $sanatizedNotifications = array();
            foreach($notifications as $notification =&gt; $enabled) {
                if(is_numeric($notification) &amp;&amp; !is_bool($enabled)) {
                    $sanatizedNotifications[trim($enabled)] = true;
                } else {
                    $sanatizedNotifications[trim($notification)] = $enabled;
                }
            }
            $this-&gt;registeredNotifications = $sanatizedNotifications;
        }
    }

    /**
     * Sends a notification to the configured server.
     * @param string $notification
     * @param string $title
     * @param string $description=''
     * @param int $priority=0
     * @param bool $sticky=false
     */
    public function notify($title, $description='', $notification=null, $priority=0, $sticky=false) {
        // Send the registration only once
        if($this-&gt;hasRegistered !== false) {
            if(is_null($this-&gt;hasRegistered)) {
                $this-&gt;hasRegistered = self::$config['register'] ? true : false;
            }
            if($this-&gt;hasRegistered) {
                try {
                    self::sendUdpRegistration($this-&gt;getServer(), $this-&gt;getPassword(),
                            $this-&gt;getApplication(), $this-&gt;getRegisteredNotifications());
                    $this-&gt;hasRegistered = true;
                } catch(Exception $e) {
                    throw new Exception('Notification registration failed:' . $e-&gt;getMessage());
                }
            }
        }

        // Default is the first registered one ...
        if(empty($notification)) {
            foreach($this-&gt;getRegisteredNotifications() as $notification =&gt; $enabled) {
                if($enabled) break;
            }
        }

        // send the notification
        self::sendUdpNotification($this-&gt;getServer(), $this-&gt;getPassword(), $this-&gt;getApplication(),
                $notification, $title, $description, $priority, $sticky);
    }

}
?&gt;

References

Posted in PHP | Tagged , , , | Comments Off