ali1 / cakephp-json-tools
CakePHP Plugin to help with configuring json responses from the controller, without templates
Installs: 57
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 2
Forks: 0
Open Issues: 0
Type:cakephp-plugin
Requires
- php: >=7.2
- cakephp/cakephp: ~4.0
- mockery/mockery: 0.9.*
Requires (Dev)
README
A CakePHP plugin to assist with creating Json responses from controllers.
Json Tools has been created to be used by traditional CakePHP projects which are mostly browser-based, but have a few AJAX or API methods. The Json Tools component makes creating these a breeze.
Features
- A component that quickly lets you set up ajax methods.
- Works with CakePHP's ResponseHandler so you don't have to
- Can be used in methods that sometimes output Html and other times Json depending on request headers, just like normal CakePHP behavior
Requirements
- Composer
- CakePHP 4.0+ (see releases for working 3.7+ release)
- PHP 7.2+
Installation
In your CakePHP root directory: run the following command:
composer require ali1/cakephp-json-tools
Then in your Application.php in your project root, add the following snippet:
// In project_root/Application.php: $this->addPlugin('JsonTools');
or you can use the following shell command to enable to plugin in your bootstrap.php automatically:
bin/cake plugin load JsonTools
Now add it to your src/AppController.php or to specific controllers
class AppController extends Controller
{
public function initialize(): void
{
parent::initialize();
$this->loadComponent('JsonTools.Json');
}
}
Usage (in controller methods that may need to output Json)
Understanding the boiler plate Json output
This component primes ResponseHandler to output something that looks like this:
[ 'error' => false, 'field_errors' => [], 'message' => '', '_redirect' => false, 'content' => null, ];
Which corresponds to a json output of:
{"error": false, "field_errors": {}, "message": "OK", "_redirect": false, "content": false }
Your controller method can then override these keys or add new ones easily using this component.
Priming the method with boiler-plate Json output
// All Json actions where you want to use this component should have one of the following lines /** * The most basic priming. Will set the boiler-plate variables (see below) that can be processed by ResponseHandler * should there be a json request. If the request is not XHR/JSON, then this method would not have an effect. */ $this->Json->prepareVars(); /** * Will return true if is Json and is POST/PUT, otherwise false * Can replace something like $this->getRequest()->is(['post', 'put']) that is often used to check if form is submitted. * You don't need to run prepareVars() if you use this line */ if($this->Json->isJsonSubmit()){} /** * Will force the output to be Json regardless of HTTP request headers * You don't need to run prepareVars() if you use this line */ $this->Json->forceJson(); // will force the output to be Json regardless of HTTP request headers /** * Throw exception if request is not Json or not POST/PUT * You don't need to run prepareVars() if you use this line */ $this->Json->requireJsonSubmit(); // throw exception if request is not Json or not POST/PUT
Setting the JSON output
Boiler plate output keys (see above) could be overwritten later in your method using one of these methods
$this->Json->set('data', $data); // add a new key called data $this->Json->set('field_errors', $errors); // replace a key $this->Json->setMessage('Great'); // shortcut to replace message $this->Json->setError('Bad input'); // sets error to true and message to 'Bad Input' (can be configured to set error to string 'Bad Input' rather than bool true $this->Json->redirect(['action' => 'index']); // sets _redirect key to a URL (for javascript client to handle the redirect) $this->Json->entityErrorVars($user); // change the Json output to error: true, and message: a list of validation errors as a string (e.g. Username: Too long, Email: Incorrect email address)
Example controller
// UsersController.php public function ajaxUpdateUser() { /* Json->requireJsonSubmit() will throw exception if not Json and a Post/Put request and also It will also prepare boiler plate variables that can be handled by RequestHandler 'error' => false, 'field_errors' => [], 'message' => '', '_redirect' => false, 'content' => null, In other words, the action output will be {"error": false, "field_errors": {}, "message": "OK", "_redirect": false, "content": false } All of these variables can be overridden in the action if errors do develop or example */ $this->Json->requireJsonSubmit(); if(!$user = $this->Users->save($this->getRequest()->getData()) { // Json->entityErrorVars($entity) will change the Json output to error: true, and message: a list of validation errors as a string (e.g. Username: Too long, Email: Incorrect email address) $this->Json->entityErrorVars($user); } else { // will make the Json output _redirect key into a URL. If you use this, your javascript needs to recognise this (see example javascript) $this->Flash->success("Saved"); $this->Json->redirect(['action' => 'view', $user->id]); } } public ajaxGetUser($user_id) { $user = $this->Users->get($user_id); $this->Json->forceJson(); // output will be Json. As of this line, the Json output will be the boilerplate output (error: false, message: OK etc.) $this->Json->set('data', $user); // the output will now have a data field containing the user object } public userCard($user_id) { $user = $this->Users->get($user_id); $this->Json->forceJson(); // output will be Json. As of this line, the Json output will be the boilerplate output (error: false, message: OK etc.) $this->set(compact('user')); // for use by the template. don't use $this->Json->set so that the user object does not get send in the output $this->Json->sendContent('element/Users/card'); // the Json output will have a 'content' key containing Html generated by the template } public otherExamples() { // Configuration $this->Json->setErrorMessageInErrorKey(true); // (default false) // true: if $this->Json->setError('error message') is called, the error key and the message key will contain the error message // false: if $this->Json->setError('error message') is called, the error message will be in the message key and the error key will be true and $this->Json->setHttpErrorStatusOnError(true); // (default false) // by default, the HTTP response is always 200 even in error situations $this->Json->setMessage('Great, all saved'); // shortcut to set the message key $this->Json->set('count', 5); // set any other json output keys you want to output }
Example AJAX form
This is an example form that corresponds to the ajaxUpdateUser method above.
templates/Users/edit.php
<?php
$this->Html->script('ajax_form', ['block' => true]);
// optionally, also install BlockUI and include it, for a loading indicator while ajax is running (http://malsup.com/jquery/block/)
?>
<?= $this->Form->create($user, ['url' => ['controller' => 'Users', 'action' => 'ajaxUpdateUser'], 'onsubmit' => 'return dynamic_submit(this);']) ?>
<fieldset>
<legend><?= __('Update User') ?></legend>
<?php
echo $this->Form->control('username');
echo $this->Form->control('name');
echo $this->Form->control('email');
?>
</fieldset>
<?= $this->Form->button(__('Submit')) ?>
<?= $this->Form->end() ?>
webroot/ajax_submit.js
/* Flexible Ajax Form Submission Function Usage: <form ... onsubmit="return ajax_submit(this);"> or <form ... onsubmit="return ajax_submit(this, {config});"> Will expect json response from server * If error: true, will alert error message and no further callbacks will occur * If success (error: false), what happens next depends on the success config * * (note if server return _redirect key in JSON, then this will take precedence and the page will be redirected) * * DEFAULT { success: true } By default, the page will just reload on success * * { success: false } If false is given, do nothing on success * * { success: function(data, form){} } If a function is given, the data will be passed to that callback function along with the form element. this callback function will be responsible for taking further action * * { success: $('.results') } If an object(element) is given, then HTML from the JSON content key will be loaded into the given element * * { success: '/url/to/success' } If a string is given, will redirect to this URL on success Other config: blockElement - if element given, only that element is blocked while loading, rather than the whole page */ window.ajax_submit = function(form, config){ config = config || {}; // config is optional config.success || (config.success = true); let mode; if (config.success === true) { // refresh mode mode = 'refresh'; } else if (typeof config.success == 'string') { // redirect mode mode = 'redirect'; } else if (typeof config.success === 'function') { // callback mode mode = 'callback'; } else if (typeof config.success === 'object') { // load HTML mode mode = 'html'; } else { // do nothing mode mode = ''; } config.blockElement || (config.blockElement = true); // true = whole page, false = none, element = block only element const ajaxOpts = { url: $(form).attr('action'), data: $(form).serialize(), context: form, method: 'post', headers: {}, dataType: 'json' }; if($(form).attr('method') && $(form).attr('method') === 'get') { ajaxOpts.method = 'get'; } if(typeof $.blockUI !== 'undefined') { if(config.blockElement === true){ $.blockUI({baseZ: 2000}); // modals are 1005 } else if (config.blockElement) { $(config.blockElement).block(); } } try{ $.ajax(ajaxOpts) .done(function(data, textStatus, jqXHR){ if(data.error) { $('.blockUI.blockOverlay').parent().unblock(); // take care of any blocked UI alert(data.message); } else { if (data._redirect) { window.location = data._redirect; } else if (mode === 'refresh') { location.reload(); } else if (mode === 'redirect') { window.location = config.success; } else if (mode === 'callback') { $('.blockUI.blockOverlay').parent().unblock(); // take care of any blocked UI config.success(data, this); // pass form back } else if (mode === 'html') { // load HTML mode $('.blockUI.blockOverlay').parent().unblock(); // take care of any blocked UI $(config.success).html(data); } else { // do nothing mode $('.blockUI.blockOverlay').parent().unblock(); // take care of any blocked UI } } }) .fail(function(jqXHR, textStatus, errorThrown) { $('.blockUI.blockOverlay').parent().unblock(); // take care of any blocked UI console.log(jqXHR); if (typeof jqXHR.responseJSON !== 'object' || typeof jqXHR.responseJSON.message !== 'string') { alert(errorThrown); } else { alert(errorThrown + ': ' + jqXHR.responseJSON.message); } }).always(function(data){ console.log(data); }); return false; }catch(err){ alert('An error occurred'); console.log(err); return false; } };