CakePHP 3 – Modelless Forms replace $useTable = false

Written by James McDonald

March 28, 2016

I wanted to learn about using AJAX to submit a form by following http://miftyisbored.com/a-complete-tutorial-on-cakephp-and-ajax-forms-using-jquery/ and I couldn’t get the examples to work because CakePHP 3 has some significant (and better) changes.

Below is my attempt at the above linked tutorial for CakePHP. I have made a number of changes from the original:

  • I have moved the timezone return code to the src/Form/TimezoneForm.php ( this is because apparently you want thin controllers and fat models )
  • Removed deprecated CakePHP 2.x code
  • Added inList validation to check the cities exist

I installed CakePHP 3 with composer and then ran it using bin/cake server

This is what the end result looked like after I made all the edits to bring it forward to CakePHP 3

error_update

This is when invalid data is submitted

 

loading_showing

This is when a valid entry is submitted. It also shows the ‘loading’ spinner

 

In CakePHP 2.x you if a model didn’t have an underlying database table associated you did the following to tell the Model it was sans table.

<?php

App::uses('AppModel', 'Model');

class Timezone extends AppModel {

   // cake 2.x
   $useTable = false;

In CakePHP 3 if there is no backing database table then this is known as having a “Modelless Form

 

Layout

<?php
// src/Template/Layout/timezone.ctp
use Cake\Core\Configure;

$cakeDescription = "My Cake Description";
$cakeVersion = Configure::version();

?>
<!DOCTYPE html>
<html>
    <head>
        <?php echo $this->Html->charset(); ?>
        <title>
            <?php echo $cakeDescription ?>:
            <?php echo $title_for_layout; ?>
        </title>
        <?php
        echo $this->Html->meta('icon');

        // echo $this->Html->css('cake.generic');
        ?>
        <?= $this->Html->css('base.css') ?>
        <?= $this->Html->css('cake.css') ?>
        <?php
        echo $this->fetch('meta');
        echo $this->fetch('css');
        echo $this->Html->script([
            'https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js',
            'global'
            ], [
                'block' => 'scriptBottom'
                ]
                );
        echo $this->Html->css('//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css');
        echo $this->Html->css('//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css');
        ?>
        
    </head>
    <body>
        <div id="container">
            <div id="content">

<?= $this->Flash->render() ?>

                <?= $this->fetch('content') ?>
                <?php
                // Append into the 'script' block.
                $this->Html->scriptStart(['block' => true]);
                echo "console.log('I am in the JavaScript');";
                $this->Html->scriptEnd();
                ?>
            </div>

        </div>
        <div style="margin-left: 40px;">

            <footer>

                <?php
                echo $this->Html->link(
                        $this->Html->image('cake.power.gif', array('alt' => $cakeDescription, 'border' => '0')), 'http://www.cakephp.org/', array('target' => '_blank', 'escape' => false, 'id' => 'cake-powered')
                );
                ?>

                <span style="font-size: .7em;"><?php echo $cakeVersion; ?></span>

            </footer>
        </div>
<?php
// echo $this->Js->writeBuffer(); // Write cached scripts
echo $this->fetch('scriptBottom');  // fetch our scritBottom
echo $this->fetch('script');
?>
    </body>
</html>

TimezoneForm

<?php

// src/Form/TimezoneForm.php

namespace App\Form;

use Cake\Form\Form;
use Cake\Form\Schema;
use Cake\Validation\Validator;
use Cake\Utility\Hash;
use Cake\Log\LogTrait;
use Cake\Utility\Inflector;

class TimezoneForm extends Form {

    use LogTrait;

    protected $cities = [
        [
            'city' => 'London',
            'tz' => 'Europe/London'
        ],
        [
            'city' => 'Montreal',
            'tz' => 'America/Toronto'
        ],
        [
            'city' => 'Mumbai',
            'tz' => 'Asia/Calcutta'
        ],
        [
            'city' => 'New York',
            'tz' => 'America/New_York'
        ],
        [
            'city' => 'Paris',
            'tz' => 'Europe/Paris'
        ],
//        [
//            'city' => 'Sydney',
//             'tz' => 'Australia/Sydney'
//            ],
        [
            'city' => 'Toronto',
            'tz' => 'America/Toronto'
        ],
        [
            'city' => 'Melbourne',
            'tz' => 'Australia/Melbourne'
        ]
    ];

    protected function _buildSchema(Schema $schema) {
        return $schema->addField('city', 'string');
//            ->addField('email', ['type' => 'string'])
//            ->addField('body', ['type' => 'text']);
    }

    public function get_cities() {
        $this->log($this->cities);
        return Hash::extract($this->cities, '{n}.city');
    }

    public function test_func() {

        return "my jimmy";
    }

    public function return_tz($city) {

        foreach ($this->cities as $k => $v) {
            if (strcasecmp($city, $v['city']) == 0) {
                date_default_timezone_set($v['tz']);
                $content = '<div class="alert alert-success" role="alert">The time in ' . $v['city'] . ' is : <strong>' . date('D M d Y - H:i:s ', time()) . '</strong></div>';
                return $content;
            }
        }

        $ret = $form->test_func();
        $content = '<div class="alert alert-danger" role="alert">An Unexpected ' . $ret . ' Error Occured </div>';

        return $content;
    }

    protected function _buildValidator(Validator $validator) {

        return $validator->add('city', 'inList', [
                    'rule' => ['inList', $this->get_cities(), true],
                    'message' => 'Not in list'
        ]);
    }

    protected function _execute(array $data) {
        // Send an email.
        return true;
    }

}

TimezoneController

<?php
// src/Controller/TimezoneController.php
namespace App\Controller;

use App\Controller\AppController;
use Cake\Event\Event;
use App\Form\TimezoneForm; // specify your form class here otherwise it won't be found
//use Cake\Log\Log;
use Cake\Utility\Hash;

class TimezoneController extends AppController {

    public function beforeFilter(Event $event) {
        parent::beforeFilter($event);

        // Change layout for Ajax requests
        if ($this->request->is('ajax')) {
            $this->viewBuilder()->layout('ajax'); // src/Template/Layout/ajax.ctp
        }
        $this->viewBuilder()->layout('timezone'); // src/Template/Layout/timezone.ctp
    }

    public function getTime() {

        // no longer model $useTable = false;
        // model-less forms use src/Form/TimezoneForm.php
        $timezone = new TimezoneForm();
        $city_list = $timezone->get_cities();
        $this->set(compact('timezone', 'city_list'));
    }

    public function ajaxGetTime() {
        $this->request->allowMethod('ajax'); // No direct access via browser URL

        $content = '<div class="alert alert-warning" role="alert">Something unexpected occured</div>';
        $form = new TimezoneForm();
        $this->log($this->request->data);
        if ($this->request->is('post')) {
            if ($form->validate($this->request->data)) {
                $city = $this->request->data['city'];
                $content = $form->return_tz($city);
            } else {
                $errors = $form->errors();

                $flatErrors = Hash::flatten($errors); 
                // flatten the error array so you can loop through it easier
                // 
                // $errors is:
                //                (
                //                [city] => Array
                //                (
                //                [inList] => Not in list
                //                )
                //
                //                )

                // $errors becomes:
                //                (
                //                [city.inList] => Not in list
                //                )


                if (count($errors) > 0) {
                    $content = '<div class="alert alert-danger" role="alert">Could not get timezone. The following errors occurred: ';
                    $content .= '<ul>';
                    foreach ($flatErrors as $key => $value) {
                        $content .= '<li><strong>' . $value . '</strong></li>';
                    }
                    $content .= '</ul>';
                    $content .= '</div>';
                }
            }
        }

        //set current date as content to show in view
        $this->set(compact('content'));

    }

}

View

<!-- src/Template/Timezone/get_time.ctp -->

<h2>CakePHP City Time Retriever</h2>
<p>The form below allows you to enter the name of a city and get its current local time through AJAX using jQuery. 
    You can view the tutorial for the setup of CakePHP 2.x <a href="http://miftyisbored.com/a-complete-tutorial-on-cakephp-and-ajax-forms-using-jquery/">here </a> </p>

<div id="loading" style="display: none;">
<div class="alert alert-info" role="alert">
    <i class="fa fa-spinner fa-spin"></i> Please wait...</div>
</div>

<div id="cityUpdate"></div>
<?php echo $this->Form->create(
        $timezone, 
        [
            'url' => [
                'action' => 'ajax_get_time'
                ],
            'name' => 'TimezoneForm',
            'data-update' => 'cityUpdate', // the id of the div we want to return data to
            'data-loading' => 'loading' // the id of the div we want to display the loading spinner
            ]); 
echo $this->Form->input('city', array('label' => 'Enter the city name', 'placeholder' => 'Enter the name of the city' )); 
echo $this->Form->submit('Get City Time', array('title' => 'Get Time', 'id'=>'citySubmit') );  
echo $this->Form->end(); ?>
<p></p>
<p>Enter any of the following cities in the text-field above to get their current local time: 
<?php echo $this->Html->nestedList($city_list); ?>
</p>

<?php //debug($timezone); ?>

JQuery

// webroot/js/global.js

$( document ).ready(function() {
    console.log( "ready!" );
  
    $('form').submit( function(e) {
        e.preventDefault();        
        var form = $(this);
        var submit_button = $(form).find('input[type=submit]');
        var loading = form.attr('data-loading');
        var update = form.attr('data-update');
        var form_data = form.serialize();
        var url = form.attr('action');
        console.log(form_data);
        $.ajax({
        url: url,
        type: 'POST',
        dataType: 'json',
        data: form_data,
        beforeSend: function(){
           $('#loading').fadeIn();
           submit_button.attr('disabled','disabled');
           $('input[type=text]').blur();
        },
        complete: function(){
           $('#' + loading).fadeOut();
           submit_button.removeAttr('disabled');
        },
        success: function(data) {
            console.log("cityUpdate");
            $('#' + update ).fadeIn().html(data.content);
                 },
        error: function(data){
            console.log(data);
        }
        });
    });
});

 

Change the default route to hook / to the get_time action:

// config/routes.php

Router::scope('/', function (RouteBuilder $routes) {
    
    # connect / to /timezone/get_time
    $routes->connect('/', array('controller' => 'timezone', 'action' => 'get_time'));
    /**
     * Here, we are connecting '/' (base path) to a controller called 'Pages',
     * its action called 'display', and we pass a param to select the view file
     * to use (in this case, src/Template/Pages/home.ctp)...
     */
    # comment this 
    # $routes->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);

2 Comments

  1. gerry

    Whats the global.js for? its not used in the views?

    Reply
    • James Mcdonald

      Global.js replaces the default post action with an ajax post and and takes the reply and updates the page. In this case I included it from template.ctp

      Reply

Leave a Reply to James Mcdonald Cancel reply

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

The reCAPTCHA verification period has expired. Please reload the page.

You May Also Like…

Clear HSTS Settings in CHrome

Open chrome://net-internals/#hsts enter the domain in the query field and click Query to confirm it has HSTS settings...

Ubuntu on Hyper-v

It boils town to installing linux-azure # as root or sudo apt-get update apt-get install linux-azure...