Daves dev warblings

For all the things I never remember

Filtering dates using CakeDC/Search

Scenario

I needed to do a search on a date field in my database which was stored as a DATE, but only filtering by month.

Solution

So I had already implemented the CakeDC/Search into my project, so all I needed to do was create a custom method to return the right conditions for the query.

My model already had the filterArgs setup.

1
2
3
4
5
6
7
8
9
/**
 * Setup default search filters
 *
 * @var array
 */
  public $filterArgs = [
      'day_type_id' => ['type' => 'value'],
        'month' => ['type' => 'query', 'method' => 'filterByMonth']
  ];

So I just added a type of query and passed in a method.

1
2
3
4
5
6
7
8
9
10
11
/**
 * Filter the pagination by month
 *
 * @param array $data
 * @return array
 **/
    public function filterByMonth(array $data = array()) {
        return [
            "DATE_FORMAT({$this->alias}.date, '%c')" => (int)$data['month']
        ];
    }

The $data array will contain all the fields setup in your model along with their values. So for me it looked like

1
2
3
4
[
    'day_type_id' => 3,
    'month' => '08' // Note the string type, hence why I cast to int
]

Done!

Make a brew!

Custom Pagination Helper links

Scenario

You are paginating a set of records but you want to use a custom url for that specific filter. For me this was pagination a set of news articles by category.

The url I wanted to use was /news/category/daves-awesome-category and then paginating results on /news/category/daves-awesome-category/page:3. However the Paginator helper didn’t want to play ball.

I had already created my route.

1
Router::connect('/news/category/:category/*', array('controller' => 'news_articles', 'action' => 'index'), array('category' => '[a-z0-9-]+', 'pass' => array('category')));

But instead of the expected links above, I was getting /news/daves-awesome-category/page:2 missing out my keyword from the url.

Solution

The Paginator helper options array to the rescue! You can actually configure the options of the helper right in the view. Such a simple fix.

Here is my pagination including the fix to adjust the url if a category is set.

1
2
3
4
5
6
if (isset($category)) {
  $this->Paginator->options['url'] = array('controller' => 'news_articles', 'action' => 'index', 'category' => $category['NewsCategory']['slug']);
}
echo $this->Paginator->prev('< ' . __('previous'), array(), null, array('class' => 'prev disabled'));
echo $this->Paginator->numbers(array('separator' => ''));
echo $this->Paginator->next(__('next') . ' >', array(), null, array('class' => 'next disabled'));

Done!

Make a brew, and probably have a biscuit too. Why not eh? You’ve earned it.

Dynamic validation in CakePHP

The scenario

You have two different forms which both submit data for the same model. Each of these forms has different fields, but both need to be validated before the data can be saved.

If you setup your model validation normally, both forms will fail to validate as they will be missing fields.

Solution

The method that I use to solve this is very simple. You can create two validation arrays and dynamically merge them together as and when you need them.

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?php
// Model/Post.php

/**
 * Setup the default rules here, these rules should be common to both forms 
 *
 * @var array $validate
 */
  public $validate = array(
      'title' => array(
          'one' => array(
              'rule' => 'notEmpty',
              'message' => 'Please enter a title',
              'required' => true
          ),
          'two' => array(
              'rule' => array('minLength', 10),
              'message' => 'Title must be more than 10 characters'
          )
      )
  );

/**
 * Validation rules for when editing a post
 * 
 * @var array $validatePost
 */
  public $validatePost = array(
      'content' => array(
          'one' => array(
              'rule' => 'notEmpty',
              'message' => 'Please enter some content',
              'required' => true
          )
      )
  );

/**
 * Validate Author
 * 
 * @var array $validateAuthor
 */
  public $validateAuthor = array(
      'author_id' => array(
          'one' => array(
              'rule' => 'notEmpty',
              'message' => 'Please select an Author',
              'required' => true
          )
      ),
  );

/**
 * Here we can check some conditions to see what validation we need
 * 
 * @return boolean
 */
  public function beforeValidate() {
      // We might want to check data
      if (isset($this->data['Post']['author_id'])) {
          $this->validate = array_merge($this->validate, $this->validateAuthor);
      }

      // Maybe only on an edit action?
      // We know it's edit because there is an id
      if (isset($this->data['Post']['id'])) {
          $this->validate = array_merge($this->validate, $this->validatePost);
      }

      // Perhaps we want to add a single new rule for add using the validator?
      // We know it's add because there is no id
      if (!isset($this->data['Post']['id'])) {
          $this->validator()->add('pubDate', array(
                  'one' => array(
                      'rule' => array('datetime', 'ymd'),
                      'message' => 'Publish date must be ymd'
                  )
              )
          )
      }

      return true;
  }

So now our validation array will contain a dynamic selection of rules based on conditions we choose. I prefer to use the array_merge() method because I find the validation rules easier to read and maintain.

You can find out more about the validator() in the book. Dynamically change validation rules

Success

Go and make a brew, you’ve just done some dynamic validation. Right in the model too, exactly where it should be. ggwp.

Merging Admin specific helpers

The scenario

You have a bunch of helpers which you are only using in the /admin section of your website. We don’t need to load these on the front-end part of the site.

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
// app/AppController.php
  public $helpers = array(
      'Html',
      'Form',
  );

  public $adminHelpers = array(
        'NiceAdmin.Actions',
        'NiceAdmin.Boolean',
  );

public function beforeFilter() {
  if (isset($this->request->params['admin'])) {
      $this->layout = 'admin';
      $this->helpers = array_merge($this->helpers, $this->adminHelpers);
  }
}

Success!

Make a brew.

How to create hasManyThrough multi-selects

Updated 6th November 2013

The scenario

In your CMS you have a HABTM relationship between two models, something like Post and Tag. However you want to store some extra data about the Tag, such as who created it. This means that the relationship between the two models will in fact need to be a hasManyThrough and use a join model. You can read more about this type of relationship in the CakePHP CookBook.

You want to be able to select and save multiple tags when creating a Post. So the logical step here is to load a list of Tag items and display them in a multi-select element in our view.

The alternative scenario is using checkboxes to add a hasMany relation to a parent record.

The problem

When creating a multi-select there is no form field configuration which will allow the data to be formatted in a way which is compatible with any of the save() methods.

For example, Setting up your form using a multi-select such as $this->Form->input('tag_id', array('type' => 'select', 'multiple' => true)) will not allow you to numerically index your fields. This means that your data will look like the following.

1
2
3
4
5
6
array (size=1)
  'tag_id' => 
    array (size=3)
      0 => string '28' (length=2)
      1 => string '29' (length=2)
      2 => string '30' (length=2)

However this isn’t compatible, and must be transformed into a numerically indexed form such as this.

1
2
3
4
5
6
7
8
9
10
array (size=3)
  0 => 
    array (size=1)
      'tag_id' => string '28' (length=2)
  1 => 
    array (size=1)
      'tag_id' => string '29' (length=2)
  2 => 
    array (size=1)
      'tag_id' => string '30' (length=2)

This new data format can now be assigned to your join model and saved using saveAll(). However the dependency isn’t respected and as such, you need the hack for deleting the existing join model records as you can see below.

Solution

The models

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
<?php

// Model/Post.php
$hasMany = array(
  'PostsTag' => array(
      'className' => 'PostsTag',
      'foreignKey' => 'post_id'
  )
);

// Model/Tag.php
$hasMany = array(
  'PostsTag' => array(
      'className' => 'PostsTag',
      'foreignKey' => 'tag_id'
  )
);

// Model/PostsTag.php
$belongsTo = array(
  'Post' => array(
      'className' => 'Post',
      'foreignKey' => 'post_id'
  ),
  'Tag' => array(
      'className' => 'Tag',
      'foreignKey' => 'tag_id'
  ),
);

The form in the view

If you want to use a multi-select field here you can use the following.

1
2
3
4
5
6
7
8
9
<?php
echo $this->Form->input(
  'PostsTag.tag_id',
  array(
      'type' => 'select',
      'multiple' => true,
      'selected' => Hash::extract($this->request->data['PostsTag'], '{n}.tag_id')
  )
);

If you would like to use checkboxes instead you can change the Form Helper to use checkboxes.

Important note
If you are using checkboxes and want to validate your data you will need to ensure that the hiddenField option is not false. Otherwise the data will not appear in the data array and you will not be able to validate it.

1
2
3
4
5
6
7
8
<?php
echo $this->Form->input(
  'Post.tag_id',
  array(
      'multiple' => 'checkbox',
      'options' => $tags // A list of tags fetched with $this->Post->Tag->find('list')
  )
);

The controller except

Controller method excerpt to show the usage of the function

1
2
3
4
5
<?php
// Here we are massaging the data in order to transform it
$this->request->data['PostsTag'] = $this->Post->PostsTag->massageHasManyForSaveAll($this->request->data['PostsTag'], 'tag_id', $this->request->data['Post']['id']);

$this->Post->saveAll($this->request->data;

The AppModel hack

In the AppModel we need to implement a hack to massage the data. You’ll notice that we call it on the join model, as above. This is important as the join model will have the correct relationships beteween the two models. We can use this to our advantage to find out the related keys for the delete.

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
<?php
/**
* Transform a set of hasMany multi-select data into a format which can be saved
* using saveAll in the controller
* 
* @param array $data
* @param str $fieldToSave
* @param int $deleteId
* @return array
*/
public function massageHasManyForSaveAll($data, $fieldToSave, $deleteId = null) {
  foreach ($this->belongsTo as $model => $relationship) {
      if ($relationship['foreignKey'] != $fieldToSave) {
          $relatedModel = $model;
          $relatedModelPrimaryKey = $this->{$model}->primaryKey;
          $relatedForeignKey = $relationship['foreignKey'];
      }
  }

  if ($deleteId !== null) {
      $this->deleteAll(array(
          $this->alias .'.'. $relatedForeignKey => $deleteId
      ));
  }

  if (is_array($data[$fieldToSave])) {
      foreach ($data[$fieldToSave] as $packageId) {
          $return[] = array($fieldToSave => $packageId);
      }
      
      return $return;
  }

  return $data;
}

Success

Make a brew!

How to use a Component to listen to Cake events

I wanted a sleek way to inject SEO meta tags into my project without a large overhead and lots of complicated finds and model joints. Thanks to jose_zap’s suggestion, I decided to learn about the Cake Events system.

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php
/**
 * Component to find and load seo data and inject it into the view
 *
 * @author David Yell <neon1024@gmail.com>
 */

App::uses('CakeEventListener', 'Event');
App::uses('CakeEvent', 'Event');

class SeoComponent extends Component implements CakeEventListener {
  
/**
 * Setup the component
 * Called after the Controller::beforeFilter() and before the controller action
 * 
 * @param Controller $controller
 */
  public function startup(Controller $controller) {
      parent::startup($controller);
      $controller->getEventManager()->attach($this);
  }

/**
 * List of callable functions which are attached to system events
 * 
 * @return array
 */
  public function implementedEvents() {
      return array(
          'View.beforeLayout' => 'writeSeo'
      );
  }

/**
 * Inject the seo data into the view
 * 
 * @param CakeEvent $event
 * @return void
 */
  public function writeSeo(CakeEvent $event) {
      // Looking for vars set to the view isn't especially robust! This should probably call a behaviour method which goes and looks up data
      if (!empty($event->subject()->viewVars['content']['Content']['seo_title'])) {
          $event->subject()->viewVars['title_for_layout'] = $event->subject()->viewVars['content']['Content']['seo_title'];
      }
      
      if (!empty($event->subject()->viewVars['content']['Content']['seo_description'])) {
          $event->subject()->Html->meta('description', $event->subject()->viewVars['content']['Content']['seo_description'], array('block' => 'meta'));
      }
      
      if (!empty($event->subject()->viewVars['content']['Content']['seo_keywords'])) {
          $event->subject()->Html->meta('keywords', $event->subject()->viewVars['content']['Content']['seo_keywords'], array('block' => 'meta'));
      }
  }
}

Success

Make a brew!

Third party libraries with CakePHP

I always struggle to include third party library files in my Cake project as they very often do not adhere to any coding standard such as PSR0.

Where to put the files?

Third party files should always go into your Vendor folder inside your project.

How to load them?

Usually you will want to use the App class to load your third party classes into your application using App::import(). The parameters of this in the api do not match the usage, which apparently is for backwards compatibility.

The App class in the cookbook.

The code will look like the following.

1
2
3
4
5
6
7
8
<?php
App::import('Vendor', 'TheNameOfMyClass', array('file' => 'DavesLibrary'.DS.'DavesClass.php'));

// DavesLibrary/DavesClass.php
class TheNameOfMyClass { }

// Importing a Vendor library from a plugin? No problem, use plugin.notation
App::import('Vendor', 'DavesPlugin.TheNameOfMyClass', array('file' => 'DavesLibrary'.DS.'DavesClass.php'));

Success

Make a brew!

CakePHP config for Nginx

I always have trouble configuring Nginx with CakePHP as I’m forever trying to add extra things to the config.

Here is an example config template.

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
server {
    listen   80;
    server_name example.dev;

    # root directive should be global
    root /Users/david/Sites/example_website/webroot;
    # If you are using a project with the `/app` folder, you'll need to uncomment the following
    # root /Users/david/Sites/example_website/app/webroot;
    access_log     /usr/local/var/log/nginx/example.access_log;
    error_log      /usr/local/var/log/nginx/example.error_log debug;
    # debug will logs lots, don't use this in production

    location / {
        index  index.php index.html index.htm;
        try_files $uri $uri/ /index.php?$uri&$args;
    }

    location ~ \.php(/|$) {
        fastcgi_pass  127.0.0.1:9001;
        # I use port 9001 as I have XDebug on port 9000, but php-fpm uses 9000 as default
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    }
}

Different finds in CakePHP

I had a reference for how to do different types of finds in CakePHP using the manual join method and the Containable or Linkable behaviours.