debuggable

 
Contact Us
 
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30

Lookupable Behavior

Posted on 30/8/08 by Felix Geisendörfer

Hey folks,

this is post #11 of my 30 day challenge.

If you ever had to work with a fairly normalized database where for example the statuses of some tables have their own table then you probably know how annoying it can be do keep those status tables in sync with everybody during development. Whenever you create a new status record you need to tell everybody about it. Then you need a good approach to refer to this status in your application and keep your code readable.

So here is what I came up with:

class LookupableBehavior extends ModelBehavior {
  function lookup(&$model, $conditions, $field = 'id', $create = true) {
    if (!is_array($conditions)) {
      $conditions = array ($model->displayField => $conditions);
    }

    if (!empty($field)) {
      $fieldValue = $model->field($field, $conditions);
    } else {
      $fieldValue = $model->find($conditions);
    }
    if ($fieldValue !== false) {
      return $fieldValue;
    }
    if (!$create) {
      return false;
    }
    $model->create($conditions);
    if (!$model->save()) {
      return false;
    }
    $conditions[$model->primaryKey] = $model->id;
    if (empty($field)) {
      return $model->read();
    }
    return $model->field($field, $conditions);
  }
}

The basic idea is to use this behavior with primitive tables that only have an id and a name / title / whatever column. So lets say you have a post_statuses table and you want to assign your current post the status 'Published', things would be as easy as:

$this->Post->set('post_status_id', $this->Post->PostStatus->lookup('Published'));

This causes the behavior to search your post_statuses table for a record named 'Published'. If it is found then the records id is returned. If it is not found, then the record is created and the new id is returned. This way you never have to worry about keeping your status tables in sync as everything is created on the fly and it is dead simple to expand available status options this way. Of course this method is not going to protect you from typos or people going nuts with adding new statuses, but with a little internal policy on how to use this behavior you should be able to save yourself some time and regain some of that hair you lost because of internet explorer : ).

The latest version of this code along with installation instructions is published in the debuggable scraps repository as always.

Feedback is most welcome! Please leave a comment.

-- Felix Geisendörfer aka the_undefined

 

Normalizing CakePHP model records

Posted on 29/8/08 by Felix Geisendörfer

Hey folks,

here we go with post #10 of my 30 day challenge from sunny Atlanta.

one problem I often face when writing model functions is that I got to handle different ways of passing a model record. Imagine a simple Post hasMany Comment association that you are looping through and calling a function on.

$out = array();
foreach ($posts as $post) {
  $out[] = sprintf('<h1>%s</h1>', $post['Post']['title']);
  foreach ($post['Comment'] as $comment) {
    $out[] = sprintf('<img src="%s">', Comment::gravatarUrl($comment));
  }
}
echo join("\n", $out);

If you were now to code the Comment::gravatarUrl function and you wanted it to be fairly flexible as to what the $comment parameter is (an array, either with or without the 'Comment' key included or the $id of the record), you'd have to always code something like this:

class Comment extends AppModel{
  static function gravatarUrl($comment) {
    if (!is_array($comment))) {
      $comment = ClassRegistry::init('Comment')->findById($comment);
    }
    if (empty($comment)) {
      return false;
    }
    if (!isset($comment['Comment'])) {
      $comment = array('Comment' => $comment);
    }
    return sprintf('http://www.gravatar.com/avatar/%s.jpg', md5($comment['Comment']['author_email']));
  }
}

So in order to avoid duplicating this kind of logic throughout my apps, I decided to abstract it into a generic AppModel::normalize function that looks like this:

class AppModel extends Model{
  static function normalize($model, $id, $query = array()) {
    if (empty($id)) {
      return false;
    }
    if (is_array($id)) {
      $record = $id;
      if (!isset($record[$model])) {
        $record = array($model => $record);
      }
    }
    if (!isset($record)) {
      $Model = ClassRegistry::init($model);
      $record = $Model->find('first', am(array(
        'conditions' => array($model.'.'.$model->primaryKey => $id),
        'contain' => false,
      ), $query));
    }
    if (empty($record)) {
      return false;
    }
    return $record;
  }
}

Usage is fairly simple. Applying the function to the behavior above will make the function look like this:

class Comment extends AppModel{
  static function gravatarUrl($comment) {
    $comment = AppModel::normalize('Comment', $comment);
    return ($comment)
      ? sprintf('http://www.gravatar.com/avatar/%s.jpg', md5($comment['Comment']['author_email']))
      : false;
  }
}

And as you can see there is also a 3rd parameter called $query that you can use to create more complex normalization rules if an $id is passed in instead of an array. Now of course this isn't perfect yet. It doesn't verify complex association structures and I spend a lot of thought on how to possibly do that. But at some point I kind of concluded that the performance implications probably wouldn't be worth it. Another thing I'd really love is if I was able to detect the name of the class the static function was called upon inside the normalize function, but so far I think PHP simply can't do it. What I mean is that I'd like to use Comment::normalize / Post::normalize etc. without having to re-define the function in each model, so if anybody has an idea I'd love to hear it!

Feedback as always is very welcome! Please comment!

HTH,
-- Felix Geisendörfer

 

HDD surgery for my MacBook Pro

Posted on 28/8/08 by Felix Geisendörfer

Hey folks,

here we go with post #9 of my 30 day challenge.

This isn't exactly a CakePHP / JS tip, but I feel like blogging about it ; ). For several weeks my MacBook Pro's HDD was maxed out. I had a 7200 RPM 160 GB HDD and since I have a ton (!) of music, pictures and a bootcamp partition this is close to nothing. When I finally had to spend 5-10 minutes every day just to keep the system running (OSX hates < 2GB of free HDD) I had to do something.

Since I didn't want to delete any of my data, I checked out additional hard drives. However, I was a little bit unhappy that there didn't seem to be any fast (7200 RPM) 2.5" harddrives with 300 GB or more. So eventually I decided that I had to sacrifice the fast speed if I wanted to go for big. And big I got:

What you see below is the Samsung SpinPoint M60S HM500LI, a 500 GB HDD with 5400 RPM and a 8mb buffer.

Here is a size comparison to the MacBook Pro:

So far so good. However, after I ordered the HDD it was laying around for quite a bit. Why? I was scared. Looking at the ifixit guide I knew the replacement would be quite involved. I mean now that I done it I know its nothing but taking out a couple of screws and cables and putting them back together. But boy did my hands shake while working on the internals of my little work buddy : ).

The beginning was easy. You take your Mac and take a before picture just in case you'll never see it again:

Then you turn it off and start by removing the battery:

Next you take off the little cover where the memory is located (I had already done that once before when putting 4GB Ram in):

Then you basically take out screws. Lots of them. It really helps to have the right tools as indicated in the ifixit guide. Especially the torx screw driver and spudger are things you don't want to miss.

Once you got all the screws out, you crack the case open. This is when it gets scary and your every move feels like you could destroy the little machine that is at the mercy of your hands:

In order to take the keyboard and top part of the case of you have to disconnect its wire. The trick is to carefully loose the yellow tape used to fixate the cable without knocking of any capacitors or shit. Once that is accomplished you are rewarded with this stunning picture:

The hard drive itself is located at the bottom left, and you need to lift up two yellow tapes again as well as disconnect the HDD:

Here is a close-up shot of the old drive that was in my MacBook:

And this is the HDD connector including the yellow tape fixating it:

Once you got all the cables and stuff off, you can take off the hdd. It has 4 bumpers and placeholders screwed to it which you need to transfer to the new drive. After that you basically just put all the stuff back in in reverse order and pray for the best. While I put my new drive in I suddenly had a little component hanging from one of the cables which I accidently pulled out. It turned out this simply was just placed in a little compartment left to the drive and I didn't rip anything. But boy was I scared ; ).

Anyway, after putting everything back together and booting up with the Leopard Install DVD, I was very happy to discover the surgery was a success! I formatted the new disk and recovered my data from a time machine Backup and things couldn't have been much smoother. The only thing I noticed is that I lost my mysql my.cnf file. It was located in /etc/private/my.cnf which unfortunately is excluded from the backup. Oh well, no biggie - after all I was just glad to not have killed my machine before leaving for the US today ; ).

Alright, hope some of you guys enjoyed the pictures. As far as I can tell there isn't much of a performance hit because of the slower RPM speed yet, but that's just my initial impression.

Oh and before I forget. From tomorrow on my time zone is EST since I'm in Atlanta. So my posts will be published by 9am EST not CET anymore.

-- Felix Geisendörfer aka the_undefined

 

Recent surgery

Posted on 28/8/08 by Felix Geisendörfer

Hey folks,

this is a little heads up that my post for today will be delayed as I'm traveling to Atlanta today and there simply wasn't any time so far this morning to finish my post. However, I wil finish it at the airport (after hacking the wifi ; p).

Meanwhile here is a little teaser for the upcoming post, enjoy:

-- Felix Geisendörfer aka the_undefined

 

Passing controller variables to your JavaScript

Posted on 27/8/08 by Felix Geisendörfer

Hey folks,

this is post #8 of my 30 day challenge. One week done, yeah!

If your application requires JavaScript in order to work than you have probably been looking for an efficient way to pass CakePHP controller variables to your scripts. I already mentioned this technique in my talk at CakeFest this year, but here is the full explanation.

Essentially we started by adding a new function to the AppController which we call 'setJson':

class AppController extends Controller{
  function setJson($one, $two = null) {
    if (!is_array($one)) {
      return $this->viewVars['jsonVars'][$one] = $two;
    }
    foreach ($one as $key => $val) {
      $this->viewVars['jsonVars'][$key] = $val;
    }
  }
}

After you added this to your AppController, you will also need this in the <head> section of your app/views/layouts/default.ctp file:

if (isset($jsonVars)) {
  echo $javascript->codeBlock('window.jsonVars = '.$javascript->object($jsonVars).';');
}

So lets say your application is all ajax / js and you want to render a couple of widgets fetched from the db, this is what you would do:

app/controllers/widgets_controller.php

class WidgetsController extends AppController{
  function index() {
    $widgets = $this->Widget->find('all');
    $this->setJson(compact('widgets'));
  }
}

app/views/webroot/js/my_include.js

$(function() {
  $.each(window.jsonVars['widgets'], function() {
    $.renderWidget(this);
  });
})

So essentially this provides you with a quick and dirty way to make your PHP arrays directly accessible in JavaScript. Often times you may want to avoid such techniques for accessibility reasons, but in other cases this is just exactly what makes sense.

Feel free to drop a comment if you like this approach or have your own solution to the problem.

HTH,
-- Felix Geisendörfer aka the_undefined

 
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30