Overview

Packages

  • Components

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:      * Action params.
 98:      *
 99:      * Stored in case debugging is on.
100:      * @var array
101:      */
102:     private $_actionParams;
103: 
104:     /**
105:      * Install error and exception handlers.
106:      */
107:     public function init()
108:     {
109:         parent::init();
110: 
111:         // Install uncaught PHP error handler
112:         Yii::app()->attachEventHandler('onError', array($this, 'onError'));
113:         // Install uncaught exception handler
114:         Yii::app()->attachEventHandler('onException', array($this, 'onException'));
115:     }
116: 
117:     /**
118:      * Get request header.
119:      *
120:      * @param string $fieldName
121:      * @return string
122:      */
123:     protected function getHeader($fieldName)
124:     {
125:         return Yii::app()->request->getRequestHeader($fieldName);
126:     }
127: 
128:     /**
129:      * Print json and headers.
130:      *
131:      * If we in debug mode, output json is 'prettyfied' for human-readability which
132:      * eases debugging.
133:      *
134:      * @param string $responseObject
135:      */
136:     protected function renderJson($responseObject)
137:     {
138:         $responseObject = CBJsonModel::resolveObjectRecursively($responseObject, true);
139: 
140:         // Fix response content type
141:         header('Content-Type: application/json; charset=utf-8;');
142: 
143:         if ((defined('YII_DEBUG') && (constant('YII_DEBUG') === true))) {
144:             // Beautify
145:             $responseJson = CBJson::indent(json_encode($responseObject));
146:             echo($responseJson);
147:             Yii::log($_GET['r']."\n"
148:                     ."Request: ".print_r($this->_actionParams, true)."\n"
149:                     ."Response: ".$responseJson, CLogger::LEVEL_TRACE, 'application.RestController');
150:         } else {
151:             // Simple, compact result
152:             echo(json_encode($responseObject));
153:         }
154:     }
155: 
156:     /**
157:      * Gather mysql logs.
158:      * @return string
159:      */
160:     private function getLogs()
161:     {
162:         $origLogs = $this->displaySummary(Yii::getLogger()->getLogs('profile', 'system.db.CDbCommand.*'));
163:         $finalLogs = array();
164:         foreach ($origLogs as &$log) {
165:             $finalLogs[] = array(
166:                 'sql' => substr($log[0], strpos($log[0], '(') + 1, -1),
167:                 'count' => $log[1],
168:                 'totalMilli' => sprintf('%.1f', $log[4] * 1000.0),
169:             );
170:         }
171:         return $finalLogs;
172:     }
173: 
174:     public $groupByToken=true;
175: 
176:     /**
177:      * Displays the summary report of the profiling result.
178:      * @param array $logs list of logs
179:      */
180:     protected function displaySummary($logs)
181:     {
182:         $stack=array();
183:         foreach($logs as $log)
184:         {
185:             if($log[1]!==CLogger::LEVEL_PROFILE)
186:                 continue;
187:             $message=$log[0];
188:             if(!strncasecmp($message,'begin:',6))
189:             {
190:                 $log[0]=substr($message,6);
191:                 $stack[]=$log;
192:             }
193:             else if(!strncasecmp($message,'end:',4))
194:             {
195:                 $token=substr($message,4);
196:                 if(($last=array_pop($stack))!==null && $last[0]===$token)
197:                 {
198:                     $delta=$log[3]-$last[3];
199:                     if(!$this->groupByToken)
200:                         $token=$log[2];
201:                     if(isset($results[$token]))
202:                         $results[$token]=$this->aggregateResult($results[$token],$delta);
203:                     else
204:                         $results[$token]=array($token,1,$delta,$delta,$delta);
205:                 }
206:                 else
207:                     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.',
208:                         array('{token}'=>$token)));
209:             }
210:         }
211: 
212:         $now=microtime(true);
213:         while(($last=array_pop($stack))!==null)
214:         {
215:             $delta=$now-$last[3];
216:             $token=$this->groupByToken ? $last[0] : $last[2];
217:             if(isset($results[$token]))
218:                 $results[$token]=$this->aggregateResult($results[$token],$delta);
219:             else
220:                 $results[$token]=array($token,1,$delta,$delta,$delta);
221:         }
222: 
223:         $entries=array_values($results);
224:         $func=create_function('$a,$b','return $a[4]<$b[4]?1:0;');
225:         usort($entries,$func);
226: 
227:         return $entries;
228:     }
229: 
230:     /**
231:      * Aggregates the report result.
232:      * @param array $result log result for this code block
233:      * @param float $delta time spent for this code block
234:      * @return array
235:      */
236:     protected function aggregateResult($result,$delta)
237:     {
238:         list($token,$calls,$min,$max,$total)=$result;
239:         if($delta<$min)
240:             $min=$delta;
241:         else if($delta>$max)
242:             $max=$delta;
243:         $calls++;
244:         $total+=$delta;
245:         return array($token,$calls,$min,$max,$total);
246:     }
247: 
248:     /**
249:      * Runs the action after passing through all filters.
250:      *
251:      * This method is invoked by {@link runActionWithFilters} after all possible filters have been
252:      * executed and the action starts to run.
253:      *
254:      * The major difference from the parent method is that it does the rendering
255:      * instead of the actions themselves which just return objects.
256:      *
257:      * Also catches exceptions and prints them accordingly.
258:      *
259:      * @param CAction $action action to run
260:      */
261:     public function runAction($action)
262:     {
263:         // Retrieve action parameters
264:         $this->_actionParams = $this->getActionParams();
265: 
266:         if (!$this->beforeAction($action)) {
267:             // Validate request
268:             throw new CHttpException(403, 'Restful action execution forbidden.');
269:         }
270: 
271:         // Run action and get response
272:         $responseObject = $action->runWithParams($this->_actionParams);
273: 
274:         // Run post-action code
275:         $this->afterAction($action);
276: 
277:         // Render action response object
278:         $this->renderJson($responseObject);
279:     }
280: 
281:     /**
282:      * Creates the action instance based on the action name.
283:      *
284:      * The method differs from the parent in that it uses CBJsonInlineAction for inline actions.
285:      *
286:      * @param string $actionId ID of the action. If empty, the {@link defaultAction default action} will be used.
287:      * @return CAction the action instance, null if the action does not exist.
288:      * @see actions
289:      * @todo Implement External Actions as well.
290:      */
291:     public function createAction($actionId)
292:     {
293:         if ($actionId === '') {
294:             $actionId = $this->defaultAction;
295:         }
296: 
297:         if (method_exists($this, 'action'.$actionId) && strcasecmp($actionId, 's')) { // we have actions method
298:             return new CBJsonInlineAction($this, $actionId);
299:         } else {
300:             $action = $this->createActionFromMap($this->actions(), $actionId, $actionId);
301:             if ($action !== null && !method_exists($action, 'run'))
302:                 throw new CException(Yii::t('yii',
303:                         'Action class {class} must implement the "run" method.',
304:                         array('{class}' => get_class($action))));
305:             return $action;
306:         }
307:     }
308: 
309:     /**
310:      * Extract json input object.
311:      *
312:      * Jsin is short for "json input object". The jsin can be extracted in two ways:
313:      * <ol>
314:      * <li>From raw PUT/POST request body, if it's a PUT/POST request</li>
315:      * <li>From 'jsin' GET parameter, if it's a GET request and we're in test mode</li>
316:      * </ol>
317:      *
318:      * @return array
319:      */
320:     public function getActionParams()
321:     {
322:         // Get handly pointer
323:         $request = Yii::app()->request;
324: 
325:         switch ($request->getRequestType()) {
326:         case 'PUT':
327:         case 'POST':
328: 
329:             if (!empty($_POST)) {
330:                 $params = $_REQUEST;
331:             } else if ($request->getIsJsonRequest()) {
332:                 // Read js input object as a string first
333:                 $params = array(
334:                     'jsin' => $request->getRequestRawBody()
335:                 );
336:             } else {
337:                 $params = array();
338:             }
339:             break;
340: 
341:         default:
342:             $params = $_GET;
343:             break;
344:         }
345: 
346:         return $params;
347:     }
348: 
349:     /**
350:      * Handle uncaught exception.
351:      *
352:      * @param CExceptionEvent $event
353:      */
354:     public function onException($event)
355:     {
356:         $e = $event->exception;
357: 
358:         // Directly return an exception
359:         $this->renderJson($this->createErrorObject($e->getCode(), $e->getMessage(), $e->getTraceAsString(), get_class($e)));
360: 
361:         // Don't bubble up
362:         $event->handled = true;
363:     }
364: 
365:     /**
366:      * Handle uncaught PHP notice/warning/error.
367:      *
368:      * @param CErrorEvent $event
369:      */
370:     public function onError($event)
371:     {
372:         //
373:         // Extract backtrace
374:         //
375:         $trace=debug_backtrace();
376:         // skip the first 4 stacks as they do not tell the error position
377:         if(count($trace)>4)
378:             $trace=array_slice($trace,4);
379: 
380:         $traceString = "#0 ".$event->file."(".$event->line."): ";
381:         foreach($trace as $i=>$t)
382:         {
383:             if ($i !== 0) {
384:                 if(!isset($t['file']))
385:                     $trace[$i]['file']='unknown';
386: 
387:                 if(!isset($t['line']))
388:                     $trace[$i]['line']=0;
389: 
390:                 if(!isset($t['function']))
391:                     $trace[$i]['function']='unknown';
392: 
393:                 $traceString.="\n#$i {$trace[$i]['file']}({$trace[$i]['line']}): ";
394:             }
395:             if(isset($t['object']) && is_object($t['object']))
396:                 $traceString.=get_class($t['object']).'->';
397:             $traceString.="{$trace[$i]['function']}()";
398: 
399:             unset($trace[$i]['object']);
400:         }
401: 
402:         //
403:         // Directly return an exception
404:         //
405:         $this->renderJson($this->createErrorObject($event->code, $event->message, $traceString, 'PHP Error'));
406: 
407:         // Don't bubble up
408:         $event->handled = true;
409:     }
410: 
411:     /**
412:      * Output total millitime header.
413:      */
414:     protected function outputTotalMillitimeHeader()
415:     {
416:         // Total execution time in milliseconds
417:         header('Total-Millitime: '.sprintf('%.1f', 1000.0 * Yii::getLogger()->executionTime));
418:     }
419: 
420:     /**
421:      * Create a standard-form error object from passed details.
422:      *
423:      * This allows for all kinds of errors (exceptions, php errors, etc.) to be returned to
424:      * the service user in a standard form.
425:      *
426:      * If you wish to add further notification mechanisms you can override this method.
427:      *
428:      * @param integer $code
429:      * @param string $message
430:      * @param string $traceString
431:      * @param string $type
432:      * @return array
433:      */
434:     protected function createErrorObject($code, $message, $traceString, $type)
435:     {
436:         $errorObject = array(
437:             'message' => $message,
438:             'code' => $code,
439:             'type' => $type,
440:         );
441: 
442:         if ((defined('YII_DEBUG') && (constant('YII_DEBUG') === true))) {
443:             $errorObject['trace'] = explode("\n", $traceString);
444:         }
445: 
446:         return $errorObject;
447:     }
448: }
449: 
Bogo Yii Json Service API documentation generated by ApiGen 2.8.0