The power of Twitter Bootstrap

I knew Bootstrap was a great tool in the right hands, but damn that stuff is powerful!

To give a recent example; I always had like a default navigation code that would look great on desktops. Until I started looking at it on mobile. It was shit, the menu covered almost half of the screen and you couldn’t click it away.. :

The new Bootstrapped navigation

But following a few small steps from the official Bootstrap documentation on Navbar’s, it got that much nicer on mobile, complete with expanding menu’s and stuff, awesome! :

Ho-ly-shit!

CakePHP searchable HABTM relations

Today I had a moment where I had to create a simple search function, but extending over multiple models. Let me start by explaining the idea;

I have 2 models to use in the search; Note and Category.
A Note can have multiple Categories, and a Category can be linked to multiple Notes. Meaning it has to be a many-to-many relation.
In CakePHP that is called a HasAndBelongsToMany relation, or HABTM.

The 2 models are then linked via a link-table; notes_categories, which only contains a note_id and a category_id field.

There is a list of Notes which needs to be searchable and filterable. There are 2 filters and 1 search-box.

  • a Type filter (a note can have either type A or type B)
  • a Category filter (a note can have one or more categories)
  • a message search field (a note has a message in it, who’s text must be searchable)

The Type filter and search field are quite simple, as those searchables are both saved in the Note model.

// Handle search queries
$query = array();
if (isset($_GET['q'])) {
    $query[] = array('Note.message LIKE "%'.$_GET['q'].'%"');
    $this->set('q', $_GET['q']);
}

if (isset($_GET['type']) && $_GET['type'] != 'all') {
    $query[] = array('Note.type' => $_GET['type']);
    $this->set('type', $_GET['type']);
}

$this->set('notes', $this->Paginator->paginate($query));

The Category however is a bit more complex, and required some thought.
After trying some alternative methods (such as containing and such) the solution eventually was presented in this 2009 article on the CakePHP Bakery.

First, we join the link-table, find all instances of a Note there.
Then we connect to the Categories through the link-table, where the category_id is the same as the category the filtering happens on.

if (isset($_GET['cat']) && $_GET['cat'] != 'all') {
    $this->paginate = array(
        'joins' => array( 
            array( 
                'table' => 'notes_categories', 
                'alias' => 'NotesCategories', 
                'type' => 'inner', 
                'foreignKey' => false, 
                'conditions'=> array('NotesCategories.note_id = Note.id') 
            ), 
            array( 
                'table' => 'categories', 
                'alias' => 'Category', 
                'type' => 'inner', 
                'foreignKey' => false, 
                'conditions'=> array( 
                    'Category.id = NotesCategories.category_id', 
                    'Category.id' => $_GET['cat'] 
                ) 
            ) 
        )
    );
    
    $this->set('cat', $_GET['cat']);
}

Well that pretty much helped me write a simple search function over multiple Models!

Javascript date objects in different browsers

So we use a tool called dhtmlxscheduler for filling our timetables. It works nice! We just drag and drop the times we want and press save to edit the current weekschedule in PHP.

However..

Internet Explorer had a weird bug where it set all the times to 01:00 – 01:00.
So I started investigating why this could be, and it turned out that the time was send to PHP correctly, but PHP could not interpret the string returned from the date object of IE. The other major browsers seemed to have no issues with this whatsoever..

It turns out the date string outputted in IE was different from other browsers.

The solution was to format the date to a general string before sending it to PHP;

// Format the date to be used in a general way, IE had some issues with this, of course..
var tmpdate = null;
var tmptime = null;
var hours = null;
var minutes = null;
var seconds = null;

tmpdate = $.datepicker.formatDate('yy-mm-dd', eventdata[attribute]);
// Add leading zero's if needed
hours = eventdata[attribute].getHours(); 
if(hours < 10){ hours = "0"+hours; }

minutes = eventdata[attribute].getMinutes(); 
if(minutes < 10){ minutes = "0"+minutes; }

seconds = eventdata[attribute].getSeconds(); 
if(seconds < 10){ seconds = "0"+seconds; }

tmptime = hours+':'+minutes+':'+seconds;
$newHiddenElement.val(tmpdate +' '+ tmptime);

Talking to other frames in Javascript

Want to talk between iframes?
Tested in Safari 8.0.2 (10600.2.5), Firefox 35.0 and Chrome 40.0.2214.91 (64-bit) on MacOS

Communication between a parent and a child frame

Parent: “Hi there kiddo, was your content by any chance changed?”
Child : ‘Yes it was father!’
Parent: “Okay, let’s not close this tab then, but instead show your regular change reminder!”

OR

Parent: “Hi there kiddo, was your content by any chance changed?”
Child : ‘No it was not.’
Parent: “Okay, let’s close this tab then!”

Actual code

Parent:

/**
* Remove a tab and tab-content
*/
function closeTab(tabId){
	var test = document.getElementById('content_'+tabId).contentWindow.hasWeekscheduleChangedIframe();
		
	// If the test was true, something changed in child, escape from function
	// otherwise just continue and close the tab
	if(true === test){
		return;
	}

.. 
actually close tab.
}

Child:

// Communication with parent page
function hasWeekscheduleChangedIframe(){
	if(weekschedule_changed){
		// View popup with options to discard changes, save changes or continue without action
		$('#changedWeekschedulePopup').modal({backdrop: 'static'});
		window.parent.parent.scrollTo(0, scrollTopLocation);
		return true;
	}
	return false;
}

Conclusion

In other words, to call a function within a child iframe, use the following code:

document.getElementById('elementId').contentWindow.customfunction();

Of course assuming that elementId is the id of an iframe.

Courtesy of this StackOverflow thread.

Translate your CakePHP Website

Translate

So, creating a translated website. I thought that was supposed to be easy with CakePHP.
Well actually, it is!

There are however some small steps you need to remember, especially regarding cache.

Use translatable tags

First off, you need to start with using the language tags for each text you want to translate. For the most convenient translating you will need to use English tags as base.

Just replace all text in your views and controllers with the gettext method, which is

echo __('text');

So instead of using

<strong>This is dog!</strong>

you would have to use

<strong><?php echo __('This is dog!'); ?></strong>

Create a POT file

Next up is to create a POT file, which actually is a source language file from which you translate to other languages.

The POT file is created with the Cake i18n command:

./cake i18n

Simply press E and follow the instructions, basically you can press enter until it is done.

Create a translation with POedit

First off, install POedit and open it.
Then click the option ‘Create new translation’ and select the ‘default.pot’ file saved by the cake i18n command. It is located in your project folder under [projectname]/app/Locale/.

POedit will ask you what language this translation is going to be in, so you can then pick the new language you want to create. Save this translation as [projectname]/app/Locale/[language]/LC_MESSAGES/default.po, where [language] is the short version for the language, conform the ISO 639-2 standards.

Note: Be sure to save the file as default.po. not as [language].po!

Informing CakePHP about using translations

Hi Cake, I want to use translations!

In your AppController’s beforeFilter function:

// Set language view and controller vars
if ($this->Session->check('Config.language')) {
    $lang = $this->Session->read('Config.language');
    Configure::write('Config.language', $lang);
} else {
    $lang = 'nl';
    $this->Session->write('Config.language', $lang);
    Configure::write('Config.language', $lang);
}

$this->userlanguage = $lang;
$this->set('userlanguage', $lang);

Restart your webserver

Small step that took me some time to figure out; Apparently the gettext function (that is used for translations in CakePHP) does caching…
So, to clear the cache, simply restart your webserver and all should be fine.

Changes in the project, new language tags?

You might need to change/add/delete some of the source tags.
If the files in your project changed simply create the .pot file again.

Now open your .po files located in the language folders; [projectname]/app/Locale/[language]/LC_MESSAGES/default.po, not the POT file in the Locale folder. Open the files with POedit and go to: “Catalog”->”Update from POT file”. It will compare your PO file with the new POT file to check what text has been added, or has been deleted.

BONUS: set language function

If you wish to use a system in which you can click on a flag or some other link to change the language, you can add the following function to the PagesController, or any other controller of choice:

public function set_language($lang){
	$this->Session->write('Config.language', $lang);
	Configure::write('Config.language', $lang);
	
	$this->redirect($this->referer());
}

Then simply add a route:

// Language
Router::connect('/setlanguage/:lang', array('controller' => 'pages', 'action' => 'set_language'), array('pass' => array('lang')));

And call the action with a link:

<a href="/setlanguage/eng"><?php if($userlanguage == 'eng'){ echo '<font style="color:#FFF">ENG</font>'; } else { echo 'ENG'; } ?></a>
<a href="/setlanguage/nl"><?php if($userlanguage == 'nl'){ echo '<font style="color:#FFF">NL</font>'; } else { echo 'NL'; } ?></a>

Thats all folks, thanks for reading!