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

Submit a Comment

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

You May Also Like…

E-Commerce Learnings

I have a client who had two Wordpress Woocommerce Stores hosted for $20 each a month on cPanel Servers On inspecting...