Archive for the ‘CakePHP’ Category
Breadcrumbs for your Cake (2.1 feature)
CakePHP 2.1+ (currently in beta) comes with an awesome new feature called view blocks and view extensions (official docs here). This allows you to modify views, append to them, and change them, based on the content that might come later on in your code.
Previously, for building a breadcrumb, there was a giant if/else tree for parsing the request object, and views and helpers all structured to allow for the flexibility. In short, a nightmare. Now with view blocks functionality, it reduces it down to a very simple set of elements, which can be called in meaningful views.
View blocks are built as, you guessed it, blocks. Each block has a name (you chose it, whatever is relevant, “sidebar”, “breadcrumb”). You define a block as such:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// This goes in a view file
$this->start('breadcrumb');
echo 'Hello';
$this->end();
// You can also append to a block:
$this->append('breadcrumb');
echo ' World';
$this->end();
// And later on you retrieve it:
echo $this->fetch('breadcrumb');
// Outputs "Hello World" |
Where do you place this? In a view file. For example, when a user goes to mysite.com/listings/add, it loads a view in Listings/add.ctp. Inside this file, at the top, you do can do:
|
1 2 3 4 5 6 |
// Listings/add.ctp
$this->start('breadcrumb');
echo "Home > Add Listing";
$this->end();
// Continue your view file |
This says that when this view (Listings/add.ctp) is loaded, then the breadcrumb should be “Home > Add Listing”. Of course this is simplified (I removed any HTML, etc, just to demonstrate).
Now that we have one view block, we need to pull this into the main layout. Since the breadcrumb shows up before any content, we will structure it using an element, which is pulled into the default layout file. I have an element, in Elements/breadcrumbs/base.ctp, which houses the basic structure of my breadcrumb. This element is called from my Layouts/default.ctp (also, super simplified):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// Elements/breadcrumbs/base.ctp
<ul class="breadcrumb">
< ?php
// There's nothing in this block, so output a default or base-level crumb
if(!$this->fetch('breadcrumb')) {
echo "Home";
} else {
echo $this->fetch('breadcrumb');
} ?>
</ul>
// Layouts/default.ctp
...
<body>
<div id="nav">
< ?php echo $this->element('breadcrumbs/base'); ?>
</div>
<div id="body">
< ?php echo $content_for_layout; ?>
</div>
...
</body> |
The $this->fetch(‘breadcrumb’) is getting a block by the name of breadcrumb. I check to see if that block exists; if it doesn’t, then I output a default item of “Home”. Otherwise, I output the contents of the “breadcrumb” block.
Then when you load the page, it will show Home, and if you go to Listings/add, it will show Home > Add. Pretty simple! In my views, instead of calling the start()/end(), I have an element, under Elements/breadcrumbs/single.ctp, that builds a single level breadcrumb:
|
1 2 3 4 5 6 7 8 9 |
// Elements/breadcrumbs/single.ctp:
$this->start('breadcrumb');
echo '<a href="/">Home</a> | <a href="'.$url.'">'.$title.'</a>';
$this->end();
// First line in a view, let's say Listings/add.ctp
echo $this->element('breadcrumbs/single', array(
'url' => 'listings/add', 'title' => 'Add a Listing'
)); |
You can clean this up even more by automating it from the $this->request, and using $title_for_layout. I also have another view, which takes an array as a parameter, to build multi-level breadcrumbs:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Elements/breadcrumbs/multi.ctp:
$this->start('breadcrumb');
echo "<li><a href='/'>Home</a></li>";
foreach($items as $i) {
echo "<li><a href='{$i[url]}'>{$i[title]}</a></li>";
}
$this->end();
// Use it in a view:
echo $this->element('breadcrumbs/multi', array('items' =>
array(
array('url' => '/level1', 'title' => 'Level 1'),
array('url' => '/level2', 'title' => 'Level 2'),
// ...
)
)); |
Hope this helps! The $scripts_for_layout has also been deprecated in favor of of the view-blocks feature, which create, since now Javascript file can be included on a page-by-page basis, using $this->append
Sphinx and CakePHP
For a project, I’ve decided to use the Sphinx search engine, and was looking for behaviors for CakePHP, to just make it much easier to implement. Since I’m using Cake 2.0, I could only find something that was for < Cake 1.3. So I decided to update it for use with Cake2.0, and it’s working beautifully with pagination.
It’s located in my github site:
https://github.com/nshahzad/Sphinx-CakePHP
The usage is exactly the same as the original (the link to it is above). The only thing is that it’s assuming you have the sphinxapi.php (which comes with the Sphinx source) extracted into Vendor/sphinxapi/sphinxapi.php (that’s where App::import() will look for it).
CakePHP Models – multiple columns to the same table
This one took me a few to figure out. On VACentral, there are schedules, which have an arrival and departure point. These points are all stored in one table, so one row in schedule refers to multiple entries in the airports table. It looks something like (ok, not something like, but exactly):
|
1 2 3 4 5 |
Schedules:
id | departure_icao | arrival_icao
Airports
id | icao |
So two ICAO columns in routes map to one same column in airports. The ICAO is a unique 4 character identifier, which is assigned to an airport. It’s quite simple actually, but took me a while to figure it it. First the Airports model:
|
1 2 3 4 5 6 |
class Airport extends AppModel
{
public $name = 'Airport';
public $primaryKey = 'id';
public $actAs = array('Containable');
} |
And then our Schedules model:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Schedule extends AppModel {
public $name = 'Schedule';
public $primaryKey = 'id';
public $actsAs = array('Containable');
public $belongsTo = array(
'DepartureAirport' => array(
'className' => 'Airport',
'foreignKey' => false,
'conditions' => 'DepartureAirport.icao = Schedule.departure_icao',
'fields' => '',
'order' => ''
),
'ArrivalAirport' => array(
'className' => 'Airport',
'foreignKey' => false,
'conditions' => 'ArrivalAirport.icao = Schedule.arrival_icao',
'fields' => '',
'order' => ''
)
);
} |
So we used the $belongTo relationship, and we will define two relationships – “DepartureAirport” and “ArrivalAirport”. We also select the class we will use (which IMO, should really be called “modelName” or “useModel”, that really tripped me up, but I digress). Next, we define the conditions – we’ll use the relationship name (DepartureAirport or ArrivalAirport), and the column name, along with the column name on the current table it should join on. And that’s pretty much it. You don’t really need a relationship on the “receiving” end (the Airports table), unless you will be querying airports, and finding out what schedules go there. I’ll leave that upto you ![]()
And then for the query itself:
|
1 2 3 4 5 6 |
$this->Schedule->contain('DepartureAirport', 'ArrivalAirport');
$schedule = $this->Schedule->find('first');
// Our Airport specific data will be contained in:
$schedule['DepartureAirport']
$schedule['ArrivalAirport'] |
Which will now return something like (etc fields ommitted):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
Array
(
[Schedule] => Array
(
[schedule_id] => 4178
[airline_id] => 2
[code] => AEA
[flightnum] => 6371
[depicao] => CYUL
[arricao] => KJFK
)
[DepartureAirport] => Array
(
[airport_id] => 597
[iata] => YUL
[icao] => CYUL
[name] => Montreal / Pierre Elliot Trudeau International Airport, Quebec
[timezone] => US/Eastern
[location] => Montreal QC Canada
[lat] => 45.470556
[lng] => -73.740833
)
[ArrivalAirport] => Array
(
[airport_id] => 268
[iata] => JFK
[icao] => KJFK
[name] => JFK Airport
[timezone] => US/Eastern
[location] => New York-Kennedy NY
[lat] => 40.6398262
[lng] => -73.7787443
)
) |
Note how it's using the Containable behavior; this is so it doesn't pull every relationship you've defined with that table (the schedules table above has many more relationships, but for brevity, I only pulled the relevant ones). Not specifying Containable() is REALLY expensive, especially when you don't need all those relationships to be included in every time! To speed it up even more, you should specify the actual field names to pull (the SQL * operator is expensive).