Overview

Packages

  • Components
  • Internals
    • AR
  • RestApi
    • Objects
    • Services

Classes

  • CBHttpRequest
  • CBJson
  • CBJsonController
  • CBJsonInlineAction
  • CBJsonModel
  • Overview
  • Package
  • Class
  • Tree
  • Todo
  1: <?php
  2: /**
  3:  * Yii controllers exchanging JSON objects.
  4:  *
  5:  * This base controller class allows developers to write controller actions which expect
  6:  * object parameters and return objects instead of printing/rendering them. It also has a
  7:  * uniform error handling mechanism which converts exceptions and PHP errors to standard-form
  8:  * jsons. The client application can be tuned to properly handle such exceptions.
  9:  *
 10:  * This approach, apart from making writing actions as if they were naturally called by the
 11:  * client/consumer, allows for proper out-of-the-box documentation (i.e. through apigen).
 12:  *
 13:  * <p>
 14:  * <h2>Action signatures</h2>
 15:  * All restful action <b>signatures</b>:
 16:  * <ul>
 17:  * <li>Accept at most one <b>input parameter</b> object. The input parameter name can be
 18:  * anything since there's at most one parameter.</li>
 19:  * <li>Return exactly one <b>output</b> object.</li>
 20:  * </ul>
 21:  * Here is an example of a restful action expecting an object and returning another:
 22:  * <pre>
 23:  *  public function actionGetAll(NotificationQueryJson $notificationQuery)
 24:  *  {
 25:  *      return new NotificationJson();
 26:  *  }
 27:  * </pre>
 28:  *
 29:  * <p>
 30:  * <h2>Request/Input Objects</h2>
 31:  * <p>
 32:  * <b>Input</b> parameters are fetched in two different ways:
 33:  * <ul>
 34:  * <li><b>Production mode</b>: From the POST body as raw json object.</li>
 35:  * <li><b>Development mode</b>: From the 'jsin' GET parameter as raw json object.</li>
 36:  * </ul>
 37:  *
 38:  * <p>
 39:  * <b>Input</b> object types of a restful action can be:
 40:  * <ul>
 41:  * <li>Scalars</li>
 42:  * <li>Objects of a CBJsonModel subtype</li>
 43:  * </ul>
 44:  *
 45:  *
 46:  * <p>
 47:  * <h2>Request/Input Headers</h2>
 48:  * <p>
 49:  * Further input can be passed to a restful action through <b>request headers</b>:
 50:  * <ul>
 51:  * <li><b>Production mode</b>: Headers of the form 'this-is-some-header' are accessed as
 52:  * 'thisIsSomeHeader':
 53:  * <pre>
 54:  * this-is-some-header: someValue\r\n
 55:  * this-is-another-header: anotherValue\r\n
 56:  * </pre>
 57:  * </li>
 58:  * <li><b>Development mode</b>: From the 'header' GET parameter as a hash:
 59:  * <pre>
 60:  * http://.../jsin=...&header[thisIsSomeHeader]=someValue&header[thisIsAnotherHeader]=anotherValue
 61:  * </pre>
 62:  * </li>
 63:  * </ul>
 64:  *
 65:  * <p>
 66:  * <h2>Response/Output objects</h2>
 67:  * <p>
 68:  * <b>Output</b> object types of a restful action can be:
 69:  * <ul>
 70:  * <li>Scalars</li>
 71:  * <li>Objects of a CBJsonModel subtype</li>
 72:  * <li>Arrays of the above two element types</li>
 73:  * </ul>
 74:  * Output objects are always automatically converted to json. Appropriate content-type headers
 75:  * are also automatically sent.
 76:  *
 77:  * <p>
 78:  * <h2>Response/Output errors</h2>
 79:  * <p>
 80:  * <b>Errors</b> are uniformly output using the createErrorObject method.
 81:  *
 82:  * <p>
 83:  * <h2>Examples</h2>
 84:  * <p>
 85:  * Here is an example GET request (for development mode) with both header and json:
 86:  * <pre>
 87:  * http://.../index-test.php?r=controller/action&header[applicationId]=1&jsin={"id":34034,"name":"John Doe"}
 88:  * </pre>
 89:  *
 90:  * @since 1.0
 91:  * @package Components
 92:  * @author Konstantinos Filios <konfilios@gmail.com>
 93:  */
 94: class CBJsonController extends CController
 95: {
 96:     /**
 97:      * Active action.
 98:      * @var CAction
 99:      */
100:     private $_action;
101: 
102:     /**
103:      * Action params.
104:      *
105:      * Stored in case debugging is on.
106:      * @var array
107:      */
108:     private $_actionParams;
109: 
110:     /**
111:      * Install error and exception handlers.
112:      */
113:     public function init()
114:     {
115:         parent::init();
116: 
117:         // Install uncaught PHP error handler
118:         Yii::app()->attachEventHandler('onError', array($this, 'onError'));
119:         // Install uncaught exception handler
120:         Yii::app()->attachEventHandler('onException', array($this, 'onException'));
121:     }
122: 
123:     /**
124:      * Get request header.
125:      *
126:      * @param string $fieldName
127:      * @return string
128:      */
129:     protected function getHeader($fieldName)
130:     {
131:         return Yii::app()->request->getRequestHeader($fieldName);
132:     }
133: 
134:     /**
135:      * Print json and headers.
136:      *
137:      * If we in debug mode, output json is 'prettyfied' for human-readability which
138:      * eases debugging.
139:      *
140:      * @param string $responseObject
141:      */
142:     protected function renderJson($responseObject)
143:     {
144:         $responseObject = CBJsonModel::resolveObjectRecursively($responseObject, true);
145: 
146:         // Fix response content type
147:         header('Content-Type: application/json; charset=utf-8;');
148: 
149:         if ((defined('YII_DEBUG') && (constant('YII_DEBUG') === true))) {
150:             // Beautify
151:             $responseJson = CBJson::indent(json_encode($responseObject));
152:             echo($responseJson);
153:             Yii::log((!empty($this->_action) ? $this->_action->id : 'Unknown action')."\n"
154:                     ."Request Params: ".print_r($this->_actionParams, true)."\n"
155:                     ."Response Object: ".$responseJson, CLogger::LEVEL_TRACE, 'application.CBRestController');
156:         } else {
157:             // Simple, compact result
158:             echo(json_encode($responseObject));
159:         }
160:     }
161: 
162:     /**
163:      * Gather mysql logs.
164:      * @return string
165:      */
166:     private function getLogs()
167:     {
168:         $origLogs = $this->displaySummary(Yii::getLogger()->getLogs('profile', 'system.db.CDbCommand.*'));
169:         $finalLogs = array();
170:         foreach ($origLogs as &$log) {
171:             $finalLogs[] = array(
172:                 'sql' => substr($log[0], strpos($log[0], '(') + 1, -1),
173:                 'count' => $log[1],
174:                 'totalMilli' => sprintf('%.1f', $log[4] * 1000.0),
175:             );
176:         }
177:         return $finalLogs;
178:     }
179: 
180:     public $groupByToken=true;
181: 
182:     /**
183:      * Displays the summary report of the profiling result.
184:      * @param array $logs list of logs
185:      */
186:     protected function displaySummary($logs)
187:     {
188:         $stack=array();
189:         foreach($logs as $log)
190:         {
191:             if($log[1]!==CLogger::LEVEL_PROFILE)
192:                 continue;
193:             $message=$log[0];
194:             if(!strncasecmp($message,'begin:',6))
195:             {
196:                 $log[0]=substr($message,6);
197:                 $stack[]=$log;
198:             }
199:             else if(!strncasecmp($message,'end:',4))
200:             {
201:                 $token=substr($message,4);
202:                 if(($last=array_pop($stack))!==null && $last[0]===$token)
203:                 {
204:                     $delta=$log[3]-$last[3];
205:                     if(!$this->groupByToken)
206:                         $token=$log[2];
207:                     if(isset($results[$token]))
208:                         $results[$token]=$this->aggregateResult($results[$token],$delta);
209:                     else
210:                         $results[$token]=array($token,1,$delta,$delta,$delta);
211:                 }
212:                 else
213:                     throw new CException(Yii::t('yii','CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.',
214:                         array('{token}'=>$token)));
215:             }
216:         }
217: 
218:         $now=microtime(true);
219:         while(($last=array_pop($stack))!==null)
220:         {
221:             $delta=$now-$last[3];
222:             $token=$this->groupByToken ? $last[0] : $last[2];
223:             if(isset($results[$token]))
224:                 $results[$token]=$this->aggregateResult($results[$token],$delta);
225:             else
226:                 $results[$token]=array($token,1,$delta,$delta,$delta);
227:         }
228: 
229:         $entries=array_values($results);
230:         $func=create_function('$a,$b','return $a[4]<$b[4]?1:0;');
231:         usort($entries,$func);
232: 
233:         return $entries;
234:     }
235: 
236:     /**
237:      * Aggregates the report result.
238:      * @param array $result log result for this code block
239:      * @param float $delta time spent for this code block
240:      * @return array
241:      */
242:     protected function aggregateResult($result,$delta)
243:     {
244:         list($token,$calls,$min,$max,$total)=$result;
245:         if($delta<$min)
246:             $min=$delta;
247:         else if($delta>$max)
248:             $max=$delta;
249:         $calls++;
250:         $total+=$delta;
251:         return array($token,$calls,$min,$max,$total);
252:     }
253: 
254:     /**
255:      * Runs the action after passing through all filters.
256:      *
257:      * This method is invoked by {@link runActionWithFilters} after all possible filters have been
258:      * executed and the action starts to run.
259:      *
260:      * The major difference from the parent method is that it does the rendering
261:      * instead of the actions themselves which just return objects.
262:      *
263:      * Also catches exceptions and prints them accordingly.
264:      *
265:      * @param CAction $action action to run
266:      */
267:     public function runAction($action)
268:     {
269:         // Store action for debugging
270:         $this->_action = $action;
271: 
272:         // Retrieve action parameters
273:         $this->_actionParams = $this->getActionParams();
274: 
275:         if (!$this->beforeAction($action)) {
276:             // Validate request
277:             throw new CHttpException(403, 'Restful action execution forbidden.');
278:         }
279: 
280:         // Run action and get response
281:         $responseObject = $action->runWithParams($this->_actionParams);
282: 
283:         // Run post-action code
284:         $this->afterAction($action);
285: 
286:         // Render action response object
287:         $this->renderJson($responseObject);
288:     }
289: 
290:     /**
291:      * Creates the action instance based on the action name.
292:      *
293:      * The method differs from the parent in that it uses CBJsonInlineAction for inline actions.
294:      *
295:      * @param string $actionId ID of the action. If empty, the {@link defaultAction default action} will be used.
296:      * @return CAction the action instance, null if the action does not exist.
297:      * @see actions
298:      * @todo Implement External Actions as well.
299:      */
300:     public function createAction($actionId)
301:     {
302:         if ($actionId === '') {
303:             $actionId = $this->defaultAction;
304:         }
305: 
306:         if (method_exists($this, 'action'.$actionId) && strcasecmp($actionId, 's')) { // we have actions method
307:             return new CBJsonInlineAction($this, $actionId);
308:         } else {
309:             $action = $this->createActionFromMap($this->actions(), $actionId, $actionId);
310:             if ($action !== null && !method_exists($action, 'run'))
311:                 throw new CException(Yii::t('yii',
312:                         'Action class {class} must implement the "run" method.',
313:                         array('{class}' => get_class($action))));
314:             return $action;
315:         }
316:     }
317: 
318:     /**
319:      * Extract json input object.
320:      *
321:      * Jsin is short for "json input object". The jsin can be extracted in two ways:
322:      * <ol>
323:      * <li>From raw PUT/POST request body, if it's a PUT/POST request</li>
324:      * <li>From 'jsin' GET parameter, if it's a GET request and we're in test mode</li>
325:      * </ol>
326:      *
327:      * @return array
328:      */
329:     public function getActionParams()
330:     {
331:         // Get handly pointer
332:         $request = Yii::app()->request;
333: 
334:         switch ($request->getRequestType()) {
335:         case 'PUT':
336:         case 'POST':
337: 
338:             if (!empty($_POST)) {
339:                 $params = $_REQUEST;
340:             } else if ($request->getIsJsonRequest()) {
341:                 // Read js input object as a string first
342:                 $params = array(
343:                     'jsin' => $request->getRequestRawBody()
344:                 );
345:             } else {
346:                 $params = array();
347:             }
348:             break;
349: 
350:         default:
351:             $params = $_GET;
352:             break;
353:         }
354: 
355:         return $params;
356:     }
357: 
358:     /**
359:      * Handle uncaught exception.
360:      *
361:      * @param CExceptionEvent $event
362:      */
363:     public function onException($event)
364:     {
365:         $e = $event->exception;
366: 
367:         // Directly return an exception
368:         $this->renderJson($this->createErrorObject($e->getCode(), $e->getMessage(), $e->getTraceAsString(), get_class($e)));
369: 
370:         // Don't bubble up
371:         $event->handled = true;
372:     }
373: 
374:     /**
375:      * Handle uncaught PHP notice/warning/error.
376:      *
377:      * @param CErrorEvent $event
378:      */
379:     public function onError($event)
380:     {
381:         //
382:         // Extract backtrace
383:         //
384:         $trace=debug_backtrace();
385:         // skip the first 4 stacks as they do not tell the error position
386:         if(count($trace)>4)
387:             $trace=array_slice($trace,4);
388: 
389:         $traceString = "#0 ".$event->file."(".$event->line."): ";
390:         foreach($trace as $i=>$t)
391:         {
392:             if ($i !== 0) {
393:                 if(!isset($t['file']))
394:                     $trace[$i]['file']='unknown';
395: 
396:                 if(!isset($t['line']))
397:                     $trace[$i]['line']=0;
398: 
399:                 if(!isset($t['function']))
400:                     $trace[$i]['function']='unknown';
401: 
402:                 $traceString.="\n#$i {$trace[$i]['file']}({$trace[$i]['line']}): ";
403:             }
404:             if(isset($t['object']) && is_object($t['object']))
405:                 $traceString.=get_class($t['object']).'->';
406:             $traceString.="{$trace[$i]['function']}()";
407: 
408:             unset($trace[$i]['object']);
409:         }
410: 
411:         //
412:         // Directly return an exception
413:         //
414:         $this->renderJson($this->createErrorObject($event->code, $event->message, $traceString, 'PHP Error'));
415: 
416:         // Don't bubble up
417:         $event->handled = true;
418:     }
419: 
420:     /**
421:      * Output total millitime header.
422:      */
423:     protected function outputTotalMillitimeHeader()
424:     {
425:         // Total execution time in milliseconds
426:         header('Total-Millitime: '.sprintf('%.1f', 1000.0 * Yii::getLogger()->executionTime));
427:     }
428: 
429:     /**
430:      * Create a standard-form error object from passed details.
431:      *
432:      * This allows for all kinds of errors (exceptions, php errors, etc.) to be returned to
433:      * the service user in a standard form.
434:      *
435:      * If you wish to add further notification mechanisms you can override this method.
436:      *
437:      * @param integer $code
438:      * @param string $message
439:      * @param string $traceString
440:      * @param string $type
441:      * @return array
442:      */
443:     protected function createErrorObject($code, $message, $traceString, $type)
444:     {
445:         $errorObject = array(
446:             'message' => $message,
447:             'code' => $code,
448:             'type' => $type,
449:         );
450: 
451:         if ((defined('YII_DEBUG') && (constant('YII_DEBUG') === true))) {
452:             $errorObject['trace'] = explode("\n", $traceString);
453:         }
454: 
455:         return $errorObject;
456:     }
457: }
458: 
Bogo Yii Json Service API documentation generated by ApiGen 2.8.0