CakePHP 3: Access a method from one Behavior (or the Table class) in another Behavior

Please note that this post refers to CakePHP 3.

Short answer is, as long as you have added both Behaviors in the Table class, you can call a method from one Behavior in another as follows:

class SecondBehavior extends Behavior {
    public function secondBehaviorMethod() {
        return $this->_table->firstBehaviorMethod();
    }
}

I couldn’t find anything about this in the docs, so initially I hoped/presumed that, as long as I had added both Behaviors to my Table class, it was simply a case of calling the method as I would in a Table, so:

In MyTable.php:

public function initialize(array $config) {
    ...
    $this->addBehavior('FirstBehavior');
    $this->addBehavior('SecondBehavior');
}

In FirstBehavior.php:

class FirstBehavior extends Behavior {
    public function firstBehaviorMethod() {
        return "done";
    }
}

In SecondBehavior.php:

class SecondBehavior extends Behavior {
    public function secondBehaviourMethod() {
        return $this->firstBehaviorMethod();  //Gives "Call to undefined method" error
    }
}

However, this just results in a “Call to undefined method” error. However, Behaviors have the $_table property (see the Behavior API) that allows you to access other methods of the current Table. Therefore, just adding _table to the call to firstBehaviorMethod ($this->_table->firstBehaviorMethod()) fixes the issue. As well as accessing methods from other added Behaviors, you can also use find ($this->_table->find(…)), etc.

CakePHP 3: Date/Time Disappearing during Hydration

When generating a date/time for saving to a MySQL database, I’ve always tended to generate it with the PHP date method (date('Y-m-d H:i:s')). However, using CakePHP 3, with dateTime validation set on the field, the date/time would disappear during hydration of the entity, but without a validation error.

I still suspected that validation was the issue here, so started having a look at the validation method to try to work out what was going wrong. Before I got very far, I noticed that the datetime validation method validates any value that is a Cake DateTime object. Therefore, the simple solution is to create the date/time using Cake’s built-in Time class:

use Cake\I18n\Time;
$datetime = Time::now();

Cake then happily saves the DateTime in the standard MySQL format.

CakePHP 3 JSON Views: Setting _jsonOptions special parameter

CakpePHP 3 makes creating JSON and XML views of your data very easy, certainly easier than I remember finding it in Cake 2. However, I found the documentation didn’t make it particularly clear how/where to set the _jsonOptions parameter. All the docs (http://book.cakephp.org/3.0/en/views/json-and-xml-views.html) say is:

“The JsonView class supports the _jsonOptions variable that allows you to customize the bit-mask used to generate JSON. See the json_encode documentation for the valid values of this option.”

It’s actually pretty simple, just put this in your Controller action:

$this->set('_jsonOptions', JSON_HEX_TAG );

I was actually doing this to try to get rid of the JSON_HEX_TAG option, which seems to be set by default. So I actually did this, to unset all the JSON options:

$this->set('_jsonOptions', '');

PHP: Naturally sorting multidimension arrays by string values

This is just one of those things that I have to do often enough for it to be useful to have it close at hand, but not often enough that I actually remember how to do it.

I often run into this problem when using CakePHP, as results are returned from the database in a multidimensional array. Since MySQL doesn’t (as far as I’m aware) have an easy natural sort option, even if the results are sorted by name, strings that have numbers in can end up in the wrong order, e.g. “Item 10” before “Item 2”. Using a combination of usort() and strnatcasecmp(), it’s pretty easy to naturally sort an array by a value that appears in each subarray, like this:

usort($array, function($a, $b) {
    return strnatcasecmp($a['name'], $b['name']);
});

As an example, say I got the following array back from MySQL:

$array = array(
    array(
        'id' => 265,
        'name' => 'Drawer 1'
    ),
    array(
        'id' => 971,
        'name' => 'Drawer 10'
    ),
    array(
        'id' => 806,
        'name' => 'Drawer 2'
    ),
    array(
        'id' => 429,
        'name' => 'Drawer 20'
    )
);

“Drawer 10” comes before “Drawer 2”, which is obviously not what I want. Using strcasecmp() as the sort function on the above array wouldn’t change it, but using strnatcasecmp() gives the required result:

$array = array(
    array(
        'id' => 265,
        'name' => 'Drawer 1'
    ),
    array(
        'id' => 806,
        'name' => 'Drawer 2'
    ),
    array(
        'id' => 971,
        'name' => 'Drawer 10'
    ),
    array(
        'id' => 429,
        'name' => 'Drawer 20'
    )
);

CakePHP: Getting the full URL of the current page

In a CakePHP (2.3) project, I needed to echo the full (i.e. including scheme (http, https etc) and hostname) current URL in a view. I assumed this would be easy, and it is, but it wasn’t immediately obvious and took a bit of searching. The following examples assume we are on localhost, the project is called ‘project’ and the current page is /controller/action/1, so we would expect the full URL to be: http://localhost/project/controller/action/1.

The way to do it:

All we need to do is to echo a null url, with the ‘full’ option set to true, which means the scheme, hostname and project path are included. As well as null, any “falsey” value seems to work, e.g. false, 0, “”, and even array().

echo $this->Html->url(null, true);

Produces: http://localhost/project/controller/action/1

The ways not to do it:

echo $this->here;

Produces: /project/controller/action/1. This is missing the scheme and hostname.

echo $this->Html->url();

Produces: /project/controller/action/1. Again, this is missing the scheme and hostname.

echo $this->Html->url( $this->here, true );

Produces: http://localhost/project/project/controller/action/1. Here ‘project’ appears twice, once from $this->here and once as we have set the ‘full’ option to true.

XML views in CakePHP 2.x

In a CakePHP project, I needed to extract an XML string from the database and display it as an XML page. The docs suggested this would be easy with the XmlView, but I struggled to get it to work. However,  after a fair amount of fiddling around, I found an easy way to display an XML view.

Here’s what I had to do:

  • Enable parsing of .xml extensions in routes.php:
    Router::parseExtensions('xml');
  • Set the viewClass to Xml in the controller:
    $this->viewClass = 'Xml';
  • Create a simple xml view in Views/YourController/xml/xml.ctp, and echo $xml in this view:
    <?php echo $xml; ?>
  • Back in the controller, set the XML string to the view:
    $this->set('xml', $xmlString);
  • Finally, render using the xml view you have just created (this must be after the ‘set’ line above):
    $this->render('xml');

Unfortunately, this does not take advantage of the _serialize key, which would mean you don’t need to create a view file at all, but I just couldn’t get this to work with an XML string, despite trying various combinations of CakePHP’s Xml Class functions.

Authenticate (SSO) Vanilla users with a CakePHP 2 application using Vanilla’s jsConnect Plugin

Using your existing CakePHP website to authenticate users into a Vanilla forum (so they don’t have to setup another account) is fairly simple…once you know how:

In your Cake site:

  1. Download the PHP jsConnect client library as described here
  2. Create a ‘vanilla’ directory in app/Vendor and copy into it functions.jsconnect.php from the download in 1.
  3. In UsersController.php add:
    App::import(
     'Vendor',
     'FunctionsJsconnect',
     array('file' => 'vanilla' . DS . 'functions.jsconnect.php')
    );
  4. Paste index.php from the download in 1 into View/Users and rename it as vanilla_authenticate.ctp. Edit so that it contains only:
    // 1. Get your client ID and secret here. These must match those in your jsConnect settings.
    $clientID = "your_client_id_from_js_connect_plugin_settings_page";
    $secret = "your_secret_from_js_connect_plugin_settings_page";
    // 3. Fill in the user information in a way that Vanilla can understand.
    // CHANGE THESE FOUR LINES.
    $user['uniqueid'] = $user_data['your_cake_id_field'];
    $user['name'] = $user_data['your_cake_username_field'];
    $user['email'] = $user_data['your_cake_email_field'];
    $user['photourl'] = $user_data['your_cake_avatar_field'];
    // 4. Generate the jsConnect string.
    // This should be true unless you are testing. 
    // You can also use a hash name like md5, sha1 etc which must be the name as the connection settings in Vanilla.
    $secure = true; 
    WriteJsConnect($user, $_GET, $clientID, $secret, $secure);
  5. In UsersController.php, add a vanilla_authenticate action:
    public function vanilla_authenticate(){
        if ($this->Auth->loggedIn()){
             //user is logged in so pass necessary data through to view
             $this->layout = 'ajax';
             $user_data = $this->Auth->user();
             $this->set('user_data', $user_data);
             }
         }
  6. Again in UsersController.php, modify your login action:
    public function login() {
        if ($this->request->is('post')) {
            ...
            //Do your normal login business which includes the line
            $this->redirect($this->Auth->redirectUrl()); //after successful login
            ...
            }
        //add following to handle get request from jsConnect
        else{
            if(isset($this->request->query['source'])&&$this->request->query['source']=="vanilla"){
    	    //we need to redirect to vanilla after login so change redirectURL
    	    $this->Auth->redirectUrl('http://www.yourvanillasite.com/index.php?p=/entry/jsconnect&client_id=your_client_id_from_js_connect_plugin_settings_page&Target=http://www.yourvanillasite.com'); //big thanks to @hgtonight for this snippet
    	    }
    	}
        }

In your Vanilla forum:

  1. Download and enable the jsConnect plugin (at the moment, the advice is to use 1.03 as 1.4.1 is still buggy, reporting a ‘regex’ error);
  2. In the jsConnect plugin settings:
    • autogenerate (using the button at the bottom) a ClientID and Secret;
    • the Site Name: Your Cake Site – this will appear as ‘Sign in with {site name}’ when users go to the Vanilla forum
    • Authenticate URL:  http://www.yourcakesite.com/users/vanilla_authenticate
    • Sign In URL: http://www.yourcakesite.com/users/login?source=vanilla
    • Register URL: http://www.yourcakesite.com/users/register

Well, that’s what worked for me anyway….

Vanilla Community’s x00 tells me that you can avoid the complicated redirect in Cake Step 6 above (and replace with:

$this->Auth->redirectUrl('www.yourvanillasite.com');

if you use the jsConnect AutoSignIn Plugin…but I haven’t tried this yet.

Good luck.

Complete Idiots Guide to using Composer with CakePHP

I recently wanted to use Miles Johnson’s Forum Plugin for CakePHP in a project, and when I came to install it, saw that he recommends (insists on, in fact) using Composer to do this, to take care of ensuring all the dependencies are installed. Never having used Composer before, I took a look at his post on using Composer with CakePHP, which probably counts as an idiots guide for most people, but still wasn’t basic enough for me. Therefore, I thought I would write a complete idiots guide!

The steps I had to go through to get it working were as follows:

  • Install Composer – I am on Windows, so just downloaded and ran the Windows installer, which seemed to work fine
  • Ensure that the openssl extension is enabled in php.ini, i.e. uncomment this line: extension=php_openssl.dll
  • Create a composer.json file in your app directory and add the dependencies, e.g. for the Forum plugin:
    {
       "config": {
          "vendor-dir": "Vendor"
       },
       "require": {
          "mjohnson/forum": "5.*"
       }
    }
  • Run composer install in the app directory. This should install all of the dependencies (in the Vendor or Plugin directories of your app, as appropriate) and create an autoload.php file in the Vendor directory
  • Add the following at the start of your Config/core.php file to make use of Composer’s autoloading capabilities:
    require_once dirname(__DIR__) . '/Vendor/autoload.php';

That should deal with all of the dependencies, in this case for the Forum Plugin. It is still necessary to load the Plugins in the Config/bootstrap.php file. For the Forum Plugin, this is does as follows:

CakePlugin::load('Utility', array('bootstrap' => true, 'routes' => true));
CakePlugin::load('Admin', array('bootstrap' => true, 'routes' => true));
CakePlugin::load('Forum', array('bootstrap' => true, 'routes' => true));

Ajax form submission from a JQuery UI modal dialog in CakePHP

I just wanted to share with you two approaches to bringing up an add or edit form in a JQuery UI modal dialog and closing the dialog on a successful submit or keeping it open and displaying errors in case of validation or other save problems.

A non-Cake approach

Ingredients

CakePHP 2+
JQuery 1.9+
JQuery UI
JQuery Form Plugin

The Views

Main page

This is the page from which we want to launch our modal add or edit dialog:

<!-- overlayed element -->
<div id="dialogModal">
     <!-- the external content is loaded inside this tag -->
     <div class="contentWrap"></div>
</div>
...
<div class="actions">
    <ul>
        <li>
            <?php echo $this->Html->link(__('Add user', true), array("controller"=>"users", "action"=>"add"), array("class"=>"overlay", "title"=>"Add User"));
        </li>
    </ul>
</div>
...
<script>
$(document).ready(function() {
    //prepare the dialog
    $( "#dialogModal" ).dialog({
        autoOpen: false,
        show: {
            effect: "blind",
            duration: 500
            },
        hide: {
            effect: "blind",
            duration: 500
            },
        modal: true
        });
    //respond to click event on anything with 'overlay' class
    $(".overlay").click(function(event){
        event.preventDefault();
        $('#contentWrap').load($(this).attr("href"));  //load content from href of link
        $('#dialogModal').dialog('option', 'title', $(this).attr("title"));  //make dialog title that of link
        $('#dialogModal').dialog('open');  //open the dialog
        });
    });
</script>

Our add or edit form

This is shown in the dialog

echo $this->Form->create('User');
echo $this->Form->input('name');
echo $this->Form->end('Submit');
<script>
$('#UserAddForm').ajaxForm({ 
    target: '#contentWrap', 
    resetForm: false, 
    beforeSubmit: function() { 
        $('#contentWrap').html('Loading...'); 
        }, 
    success: function(response) { 
        if (response=="saved")) { 
            $('#dialogModal').dialog('close');  //close containing dialog    
            location.reload();  //if you want to reload parent page to show updated user
            } 
        } 
    });
</script>

Controller

For our add or edit action

function add() {
    ...
    if (!empty($this->request->data)) {
        $this->User->create();
        if ($this->User->save($this->request->data)){ 
            return "saved"; 
            } 
        }
    ...
    }

 

A more Cakey approach

With thanks to RXC on StackOverflow

Ingredients

CakePHP 2+
JQuery 1.9+
JQuery UI

The Views

Main page

This is the same as above

Our add or edit form

echo $this->Form->create('User');
echo $this->Form->input('name');
echo $this->Js->submit('Save', array(  //create 'ajax' save button
    'update' => '#contentWrap'  //id of DOM element to update with selector
    ));
if (false != $saved){ //will only be true if saved OK in controller from ajax save above
    echo "<script>
        $('#dialogModal').dialog('close');  //close containing dialog         
        location.reload();  //if you want to reload parent page to show updated user
    </script>";
    }
echo $this->Form->end();
echo $this->Js->writeBuffer(); //assuming this view is rendered without the default layout, make sure you write out the JS buffer at the bottom of the page

Controller

For our add or edit action

function add() {
    ...
    $this->set('saved', false); //false by default - controls closure of overlay in which this is opened
    if (!empty($this->request->data)) {
        $this->User->create();
        if ($this->User->save($this->request->data)){ 
            $this->set('saved', true); //only set true if data saves OK
            } 
        }
    ...
    }

Hope that’s helpful. Would be very interested in alternatives that have worked for you…and corrections to inevitable errors above.