An object oriented approach to building a monthly calendar.
This code assumes a database table with event dates specified in a datetime field. It also helps to have a CSS file that defines a few table classes. you can see my example below.
The controller:
function cal($y,$m)
{
$this->load->helper('url');
$this->load->model('Month_model');
$this->Month_model->set_event_table('Events');
$this->Month_model->set_event_field('datetime_start');
$p_arr = array('year','month','month_name_long','month_prev','month_next','year_in_month_prev','year_in_month_next');
$data['month_array'] = $this->Month_model->db_month_box($m,$y);
foreach ($p_arr as $p)
{
$data[$p] = $this->Month_model->$p;
}
$this->load->view('month_table',$data);
}
The model:
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class Month_model extends Model {
var $month;
var $month_prev;
var $month_next;
var $month_name_long;
var $month_name_short;
var $year;
var $year_in_month_prev;
var $year_in_month_next;
var $month_arr;
var $db_table_with_dates;
var $db_field_with_date;
function Month_model()
{
parent::Model();
}
// set the db and field
// where events are
function set_event_table($tbl_name)
{
$this->db_table_with_dates = $tbl_name;
}
function set_event_field($fld_name)
{
$this->db_field_with_date = $fld_name;
}
// send month and year
// build month box
// and then hang the events
// fetched from the specified db
function db_month_box($m,$y)
{
// get the raw month structure
$arr = $this->month_box($m,$y);
// hang some objects onto the month
$arr = $this->hang_events();
// chunk out weekly arrays, keep keys
$ca = array_chunk($arr,7,TRUE);
return $ca;
}
// return an array with Ymd format
// dates as keys, calendar month with
// padded entries to make a fully
// rectangular month calendar
function month_box($m,$y)
{
if ($m > 12) $m = 12;
if ($m < 1) $m = 1;
$this->month = $m;
$this->year = $y;
$this->month_arr = array();
// get timestamp of the first
// day of requested month and year
$ts_start = mktime(12, 0, 0, $this->month, 1, $this->year);
$days_in_month = date('t',$ts_start);
// use noon so we don't get messed up
$ts_end = mktime(12, 0, 0, $this->month, $days_in_month, $this->year);
$pms = strtotime('-1 month',$ts_start);
$pme = strtotime('-1 day',$ts_start);
$nms = strtotime('+1 day',$ts_end);
// have to do extra to get prev/next month params
$this->month_next = date('m',$nms);
$this->year_in_month_next = date('Y',$nms);
$this->month_prev = date('m',$pms);
$this->year_in_month_prev = date('Y',$pms);
$days_in_month_nm = date('t',$nms);
$nme = mktime(12, 0, 0, $this->month_next, $days_in_month_nm, $this->year_in_month_next);
$prev_month_start = date('Ymd', $pms);
$prev_month_end = date('Ymd', $pme);
$next_month_start = date('Ymd', $nms);
$next_month_end = date('Ymd', $nme);
$start_day = date('Ymd',$ts_start);
$end_day = date('Ymd',$ts_end);
// generate array from the
// number of days in month
$arr_tm = range($start_day,$end_day);
$arr_pm = range($prev_month_start,$prev_month_end);
$arr_nm = range($next_month_start,$next_month_end);
// get the day num of the
// first/last day of the month
$day_of_week_first = date('w', $ts_start);
$day_of_week_last = date('w', $ts_end);
// pad the beginning of the array
// based on day_of_week, negative keys
$pad_in = ($day_of_week_first) ? array_slice($arr_pm,-$day_of_week_first) : array();
// now the lead-out days
$day_mod = 6-$day_of_week_last;
$pad_out = ($day_mod)?array_slice($arr_nm,0,$day_mod):array();
// put the lead-in/lead-out days on
$arr = array_merge($pad_in,$arr_tm,$pad_out);
$arr = array_flip($arr);
// normalize every value
// do this to avoid making
// a day class right now
// just use stdClass
$today_Ymd = date('Ymd');
$this->month_name_long = date('F',$ts_start);
$this->month_name_short = date('M',$ts_start);
foreach($arr as $k => $v)
{
$o = new stdClass;
// remove the spaces in the sscanf() format string
list($year,$month,$day) = sscanf($k, "% 4d% 2d% 2d");
// store the timestamp to enable
// access to any date formatting
// via date() function in view
$o->ts = strtotime($month.'/'.$day.'/'.$year);
$o->day_of_month = $day;
$o->day_of_year = date('z',$o->ts);
$o->day_ordinal = date('jS',$o->ts);
$o->day_name_long = date('D',$o->ts);
$o->day_name_short = substr($o->day_name_long,0,3);
$o->day_name_initial = substr($o->day_name_short,0,1);
$o->not_in_month = ($month!=$this->month);
$o->today = ($today_Ymd==$k);
// start with event FALSE,
// add events in other function
$o->has_event = FALSE;
$o->data = array();
$this->month_arr[$k] = $o;
}
return $this->month_arr;
}
function hang_events()
{
$f = $this->db_field_with_date;
// define some calculated fields in SQL
$this->db->select("*, DATE_FORMAT($f, '%m') as month, DATE_FORMAT($f, '%d') as day, DATE_FORMAT($f, '%Y%m%d') AS Ymd", FALSE);
// get everything in the month (any year)
$this->db->where("MONTH($f)",$this->month);
$this->db->order_by($f);
$q = $this->db->get($this->db_table_with_dates);
$ev = $q->result();
foreach ($ev as $e)
{
$month_date = $this->year.$e->month.$e->day;
// check for exact match first then annual
// these should be separated to handle single and
// repeating dates differently
if ( array_key_exists($e->Ymd, $this->month_arr) OR array_key_exists($month_date, $this->month_arr) )
{
$this->month_arr[$month_date]->has_event = TRUE;
$this->month_arr[$month_date]->data[] = $e;
}
}
return $this->month_arr;
}
}
// EOF
and the view:
<table id="calendar" cellspacing="0" cellpadding="0" summary="This month's calendar">
<caption>
<?php echo anchor('example/cal/'.$year_in_month_prev.'/'.$month_prev, '«', array('title'=>"previous month", 'class'=>"nav")); ?>
<?php echo $month_name_long; ?> <?php echo $year; ?>
<?php echo anchor('example/cal/'.$year_in_month_next.'/'.$month_next, '»', array('title'=>"next month", 'class'=>"nav")); ?>
</caption>
<tr>
<?php foreach($month_array[0] as $head): ?>
<th scope="col"><?php echo $head->day_name_initial; ?></th>
<?php endforeach; ?>
</tr>
<?php foreach($month_array as $week): ?>
<tr>
<?php foreach($week as $day): ?>
<td class=
<?php if($day->today){echo '"today"';}
elseif($day->not_in_month){echo '"not_in_month"';}
else{echo '"day"';} ?>
>
<?php echo ($day->has_event) ? anchor('example/cal/'.$year.'/'.$month.'/'.$day->day_of_month, $day->day_of_month) : $day->day_of_month; ?>
<!--
<ul>
<?php foreach($day->data as $event): ?>
<li><?php echo $event->id; ?>: <?php echo $event->event_name; ?></li>
<?php endforeach; ?>
</ul>
-->
</td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</table>
The CSS for the table (needs a gif file):
body {
font: normal 12px/20px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;
color: #616B76;
}
a {
color: #DF9496;
}
#calendar {
width: 141px;
padding: 0;
margin: 0;
border-left: 1px solid #A2ADBC;
font: normal 12px/20px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;
color: #616B76;
text-align: center;
background-color: #fff;
}
.nav, .nav a {
font: bold 18px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;
color: #fff;
text-align: center;
text-decoration: none;
}
caption {
margin: 0;
padding: 0;
width: 141px;
background: #A2ADBC;
color: #fff;
font: bold 12px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;
text-align: center;
}
th {
font: bold 11px/20px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;
color: #616B76;
background: #D9E2E1;
border-right: 1px solid #A2ADBC;
border-bottom: 1px solid #A2ADBC;
border-top: 1px solid #A2ADBC;
}
.today, td.today a, td.today a:link, td.today a:visited {
color: #F6F4DA;
font-weight: bold;
background: #DF9496;
}
.not_in_month, td.not_in_month a, td.not_in_month a:link, td.not_in_month a:visited {
color: #CCCCCC;
font-weight: normal;
background: #EEEEEE;
}
td.day {
border-right: 1px solid #A2ADBC;
border-bottom: 1px solid #A2ADBC;
width: 20px;
height: 20px;
text-align: center;
background: url(../img/bg_calendar.gif) no-repeat right bottom;
}
td.day a {
text-decoration: none;
font-weight: bold;
display: block;
}
td.day a:link, td.day a:visited {
color: #B88546;
background: url(../img/bg_calendar.gif) no-repeat;
}
td.day a:hover, td.day a:active {
color: #6aa3ae;
background: url(../img/bg_calendar.gif) no-repeat right top;
}
