CakePHP 2.x to CakePHP 3.x

Converting to the new version of CakePHP was supposed to be a breeze I thought.
Not really. They changed a lot of stuff regarding Models and database manipulation, so we have to rewrite a lot of code.
Most notable changes are:

General

  • The folder structure obviously, every important MVC part is now hidden in the src/ folder instead of the App/ folder.
  • The usage of namespaces, finally!

Model

  • Models are no longer used to access data from a database. This is now done through the tables, the Model\Table classes. A Model\Table class represents a database Table. Models as we knew them are no longer used, and instead the logic will need to be moved to the Model\Table classes.
  • Model\Table query results no longer return Arrays. Instead they return a list of Entity objects.
  • Entity objects are a representation of 1 database row. As such they can be manipulated from the object itself.
  • To save a change to any Entity you will need to pass the complete Entity to the Model\Table class’ save function; Model\Table::save(Entity);
  • Model associations are saved automatically when you add the associated Entities to the Entity you wish to add them to, and save the Entity;
    // Load weekschedule entity
    $currentLoadedWeekschedule = $this->Weekschedules->get(34);
    
    // Create new Timetable entity
    $newTimetable = $this->Timetables->newEntity(['name' => 'test', 'desc' => 'test2']);
    
    // Add related Timetable to weekschedule
    $currentLoadedWeekschedule->timetables = $newTimetable;
    
    // Eventually, save this weekschedule and its related Timetable
    $this->Weekschedules->save($currentLoadedWeekschedule);
    
  • All time fields from the database are now returned as a Cake\I18n\Time object instead of as a string. You can easily manipulate these object to view as a string again though:
    echo $period['startdatetime']->i18nFormat("YYYY-MM-dd HH:mm:ss");
    
  • Same goes for saving the DATETIME fields, create a new Time() object for that field.
    use Cake\I18n\Time;
    $entity['startdatetime'] = new Time($data['startdate'] . ' 00:00:00');
    
  • Because you get an object with entities returned now, there are now longer big arrays you need to unwind, things like $var[‘Table’][‘TableLinked’][‘LinkedTable2’][‘data’]; are no longer needed!
  • Support for cascading deletes! If the Model associations are set correctly and to ‘dependent’ and ‘cascadeCallbacks’, you will automagically remove all associated underlying entities.
    PeriodsTable:
    $this->hasMany('Weekschedules', [
        'foreignKey' => 'period_id',
        'dependent' => true,
        'cascadeCallbacks' => true
    ]);
    
    WeekschedulesTable:
    $this->hasMany('Timetables', [
        'foreignKey' => 'weekschedule_id',
        'dependent' => true,
        'cascadeCallbacks' => true
    ]);
    
    AgendasController:
    $this->loadModel('Periods');
    $deletedPeriod = $this->Periods->get($this->request->data['data']['Period']['id']);
    
    $this->Periods->delete($deletedPeriod);
    
  • Copying Entities with child-entities is a breeze! Where as we previously needed a script to;
    create a copy of the parent entity, submit that, get its id, use that to save the new children with the parents id, and save each child.

    We can now easily copy a complete entity and its children with the following lines (in the Model);

    public function copy($weekschedule_id){
        // Find original weekschedule
        $originalWeekschedule = $this->get($weekschedule_id, ['contain' => 'Timetables']);
    
        // Create new weekschedule entity with the same properties 
        // and timetables as the original one
        $copiedWeekschedule = $this->newEntity($originalWeekschedule->toArray());
        $copiedWeekschedule->set('name', __('Copy of ') . $copiedWeekschedule['name']);
    
        // Save this new weekschedule
        $newWeekschedule = $this->save($copiedWeekschedule);
    
        return $newWeekschedule;
    }
    

Views / Helpers

  • The FormHelper has been rewritten to use the new ORM. The FormHelper action ‘end()’ no longer creates a submit button. Instead, call the following:
    // Cake\View\Helper\FormHelper\submit(string $caption, array $options);
    echo $this->Form->submit(__('Rearrange weekschedules'), ['class' => 'btn']);
    
    // Dont forget to close your Form or it will include all other input fields on the page
    echo $this->Form->end();
    

There will be more changes, but these are the ones I found important and most changing for us for now.

CakePHP & MySQL Workbench relations

To start designing databases for CakePHP application firstly you need to be aware of distinctions between database relation types in MySQL  and model associations in CakePHP.

MySQL tables relationship CakePHP objects association
one-to-one non-identifying relationship hasOne
one-to-many non-identifying relationship hasMany, belongsTo
one-to-one identifying relationship not supported*
one-to-many identifying relationship not supported*
many-to-many identifying relationship hasAndBelongsToMany

* As there is no multiple-column primary key support in CakePHP it can be said that there is no support for 1:1 identifying and 1:n identifying relationships. The only exception is associated model with one column primary key which is relation foreign key at the same time

Screenshot below presents simple EER diagram (I have highlighted the Workbench relationships creator area in red):

Relationship description MySQL tables relationship CakePHP objects association
Each user may have only one profile page.
Each profile page is associated with one user.
users <-> profile_pages
(one-to-one non-identifying relationship)
User hasOne ProfilePage
Each article can be commented many times.
Each comment is created by a User.
Each user can create many comments.
articles <-> comments
(one-to-many non-identifying relationship)

users <-> comments
(one-to-many non-identifying relationship)

Article hasMany Comment,
Comment belongsTo Article,

User hasMany Comment,
Comment belongsTo User

Each user can like many articles.
Each article can be liked by many users.
users <-> users_articles <-> articles
(many-to-many identifying relationship)
User hasAndBelongsToMany Article

As I mentioned before it’s worth to use CakePHP naming convention during the design process. Once you’ve got proper database schema you can easily generate plain source code of your application. In other words MySQL Workbench + CakePHP code generator can be used as ORM (Object-relational mapping) design tool.

Via Rocketmill

CakePHP uploads linked to Models

How to upload files such as images for a certain Model in CakePHP, you ask?

Get the Upload plugin

Simply download the following plugin to your /app/Plugin/ folder, and make sure the new folder in there is called ‘Upload’.
Enable the plugin in your /app/Config/bootstrap.php;

CakePlugin::load('Upload');

You can also just follow the Installation instructions on the Plugin website.

Use the plugin in your Model of choice

In the model you want to have uploads, create a database field called ‘photo’ (varchar 255) and add the following code to your Model;

    public $actsAs = array(
        'Upload.Upload' => array(
            'photo' => array(
                'thumbnailSizes' => array(
                    'vga' => '285x285',
                    'thumb' => '80x80'
                ),
                'thumbnailMethod'  => 'php',
                'fields' => array(
                	'photo' => 'photo',
                	'dir' => 'uploads',
                	'type' => 'photo_type'
                )
            )
        )
    );

Then in the View of the add/edit page, add the following code:

echo $this->Form->create('Note', array('class' => 'form-horizontal', 'type' => 'file'));
echo $this->Form->input('Note.photo', array('type' => 'file', 'label' => 'Foto'));
echo $this->Form->input('message', array('label' => __('Bericht')));
echo $this->Form->end(); 

That’s it!

By default, the new uploads are saved in the /app/webroot/files/{modelname}/photo/{model_entry_id}. You can change this in the options in the Model.
More about those options here.

Happy coding!

Fat Models, skinny Controllers

I think that’s quite a lovely design pattern!

The controller isn’t there to do everything. It is there to determine what needs to be done, and have other parts of code do it.

You keep the overview in the Controller, so that the Controller only needs to do small alterations to posted data or direct the data to the Model, which can handle it accordingly.

The Controller basically is used as a manager..

Thanks for the data View, handle it Model!

Small example of the functions M & C should do:

Controller

  • Handle GET & POST data
  • Logical alterations of the data provided by either the Model or View
  • Passing data from View to Model
  • Passing data from Model to View

Model

  • Save data to the database
  • Retrieve data from the database
  • Handle caching
  • Handle custom functions to manipulate database

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!

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!