Overview

Packages

  • Phery

Classes

  • Phery
  • PheryFunction
  • PheryResponse

Exceptions

  • PheryException
  • Overview
  • Package
  • Class
   1: <?php
   2: /**
   3:  * The MIT License (MIT)
   4:  *
   5:  * Copyright © 2010-2013 Paulo Cesar, http://phery-php-ajax.net/
   6:  *
   7:  * Permission is hereby granted, free of charge, to any person
   8:  * obtaining a copy of this software and associated documentation
   9:  * files (the “Software”), to deal in the Software without restriction,
  10:  * including without limitation the rights to use, copy, modify, merge,
  11:  * publish, distribute, sublicense, and/or sell copies of the Software,
  12:  * and to permit persons to whom the Software is furnished to do so,
  13:  * subject to the following conditions:
  14:  *
  15:  * The above copyright notice and this permission notice shall be included
  16:  * in all copies or substantial portions of the Software.
  17:  *
  18:  * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS
  19:  * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  20:  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  21:  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
  22:  * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
  23:  * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  24:  * OTHER DEALINGS IN THE SOFTWARE.
  25:  *
  26:  * @link       http://phery-php-ajax.net/
  27:  * @author     Paulo Cesar
  28:  * @version    2.6.1
  29:  * @license    http://opensource.org/licenses/MIT MIT License
  30:  */
  31: 
  32: /**
  33:  * Main class for Phery.js
  34:  *
  35:  * @package    Phery
  36:  */
  37: class Phery implements ArrayAccess {
  38: 
  39:     /**
  40:      * Exception on callback() function
  41:      * @see callback()
  42:      * @type int
  43:      */
  44:     const ERROR_CALLBACK = 0;
  45:     /**
  46:      * Exception on process() function
  47:      * @see process()
  48:      */
  49:     const ERROR_PROCESS = 1;
  50:     /**
  51:      * Exception on set() function
  52:      * @see set()
  53:      */
  54:     const ERROR_SET = 2;
  55:     /**
  56:      * Exception when the CSRF is invalid
  57:      * @see process()
  58:      */
  59:     const ERROR_CSRF = 4;
  60:     /**
  61:      * Exception on static functions
  62:      * @see link_to()
  63:      * @see select_for()
  64:      * @see form_for()
  65:      */
  66:     const ERROR_TO = 3;
  67:     /**
  68:      * Default encoding for your application
  69:      * @var string
  70:      */
  71:     public static $encoding = 'UTF-8';
  72:     /**
  73:      * Expose the paths on PheryResponse exceptions
  74:      * @var bool
  75:      */
  76:     public static $expose_paths = false;
  77:     /**
  78:      * The functions registered
  79:      * @var array
  80:      */
  81:     protected $functions = array();
  82:     /**
  83:      * The callbacks registered
  84:      * @var array
  85:      */
  86:     protected $callbacks = array();
  87:     /**
  88:      * The callback data to be passed to callbacks and responses
  89:      * @var array
  90:      */
  91:     protected $data = array();
  92:     /**
  93:      * Static instance for singleton
  94:      * @var Phery
  95:      * @static
  96:      */
  97:     protected static $instance = null;
  98:     /**
  99:      * Render view function
 100:      * @var array
 101:      */
 102:     protected $views = array();
 103:     /**
 104:      * Config
 105:      *
 106:      * <code>
 107:      * 'exit_allowed' (boolean)
 108:      * 'exceptions' (boolean)
 109:      * 'return' (boolean)
 110:      * 'error_reporting' (int)
 111:      * 'csrf' (boolean)
 112:      * 'set_always_available' (boolean)
 113:      * </code>
 114:      * @var array
 115:      *
 116:      * @see config()
 117:      */
 118:     protected $config = array();
 119:     /**
 120:      * If the class was just initiated
 121:      * @var bool
 122:      */
 123:     private $init = true;
 124: 
 125:     /**
 126:      * Construct the new Phery instance
 127:      * @param array $config Config array
 128:      */
 129:     public function __construct(array $config = array())
 130:     {
 131:         $this->callbacks = array(
 132:             'before' => array(),
 133:             'after' => array()
 134:         );
 135: 
 136:         $config = array_replace(
 137:             array(
 138:                 'exit_allowed' => true,
 139:                 'exceptions' => false,
 140:                 'return' => false,
 141:                 'csrf' => false,
 142:                 'set_always_available' => false,
 143:                 'error_reporting' => false
 144:             ), $config
 145:         );
 146: 
 147:         $this->config($config);
 148:     }
 149: 
 150:     /**
 151:      * Set callbacks for before and after filters.
 152:      * Callbacks are useful for example, if you have 2 or more AJAX functions, and you need to perform
 153:      * the same data manipulation, like removing an 'id' from the $_POST['args'], or to check for potential
 154:      * CSRF or SQL injection attempts on all the functions, clean data or perform START TRANSACTION for database, etc
 155:      *
 156:      * @param array $callbacks The callbacks
 157:      *
 158:      * <pre>
 159:      * array(
 160:      *
 161:      *     // Set a function to be called BEFORE
 162:      *     // processing the request, if it's an
 163:      *     // AJAX to be processed request, can be
 164:      *     // an array of callbacks
 165:      *
 166:      *     'before' => array|function,
 167:      *
 168:      *     // Set a function to be called AFTER
 169:      *     // processing the request, if it's an AJAX
 170:      *     // processed request, can be an array of
 171:      *     // callbacks
 172:      *
 173:      *     'after' => array|function
 174:      * );
 175:      * </pre>
 176:      *
 177:      * The callback function should be
 178:      *
 179:      * <pre>
 180:      *
 181:      * // $additional_args is passed using the callback_data() function,
 182:      * // in this case, a before callback
 183:      *
 184:      * function before_callback($ajax_data, $internal_data){
 185:      *   // Do stuff
 186:      *   $_POST['args']['id'] = $additional_args['id'];
 187:      *   return true;
 188:      * }
 189:      *
 190:      * // after callback would be to save the data perhaps? Just to keep the code D.R.Y.
 191:      *
 192:      * function after_callback($ajax_data, $internal_data, $PheryResponse){
 193:      *   $this->database->save();
 194:      *   $PheryResponse->merge(PheryResponse::factory('#loading')->fadeOut());
 195:      *   return true;
 196:      * }
 197:      * </pre>
 198:      *
 199:      * Returning false on the callback will make the process() phase to RETURN, but won't exit.
 200:      * You may manually exit on the after callback if desired
 201:      * Any data that should be modified will be inside $_POST['args'] (can be accessed freely on 'before',
 202:      * will be passed to the AJAX function)
 203:      *
 204:      * @return Phery
 205:      */
 206:     public function callback(array $callbacks)
 207:     {
 208:         if (isset($callbacks['before']))
 209:         {
 210:             if (is_array($callbacks['before']) && !is_callable($callbacks['before']))
 211:             {
 212:                 foreach ($callbacks['before'] as $func)
 213:                 {
 214:                     if (is_callable($func))
 215:                     {
 216:                         $this->callbacks['before'][] = $func;
 217:                     }
 218:                     else
 219:                     {
 220:                         self::exception($this, "The provided before callback function isn't callable", self::ERROR_CALLBACK);
 221:                     }
 222:                 }
 223:             }
 224:             else
 225:             {
 226:                 if (is_callable($callbacks['before']))
 227:                 {
 228:                     $this->callbacks['before'][] = $callbacks['before'];
 229:                 }
 230:                 else
 231:                 {
 232:                     self::exception($this, "The provided before callback function isn't callable", self::ERROR_CALLBACK);
 233:                 }
 234:             }
 235:         }
 236: 
 237:         if (isset($callbacks['after']))
 238:         {
 239:             if (is_array($callbacks['after']) && !is_callable($callbacks['after']))
 240:             {
 241: 
 242:                 foreach ($callbacks['after'] as $func)
 243:                 {
 244:                     if (is_callable($func))
 245:                     {
 246:                         $this->callbacks['after'][] = $func;
 247:                     }
 248:                     else
 249:                     {
 250:                         self::exception($this, "The provided after callback function isn't callable", self::ERROR_CALLBACK);
 251:                     }
 252:                 }
 253:             }
 254:             else
 255:             {
 256:                 if (is_callable($callbacks['after']))
 257:                 {
 258:                     $this->callbacks['after'][] = $callbacks['after'];
 259:                 }
 260:                 else
 261:                 {
 262:                     self::exception($this, "The provided after callback function isn't callable", self::ERROR_CALLBACK);
 263:                 }
 264:             }
 265:         }
 266: 
 267:         return $this;
 268:     }
 269: 
 270:     /**
 271:      * Throw an exception if enabled
 272:      *
 273:      * @param Phery   $phery Instance
 274:      * @param string  $exception
 275:      * @param integer $code
 276:      *
 277:      * @throws PheryException
 278:      * @return boolean
 279:      */
 280:     protected static function exception($phery, $exception, $code)
 281:     {
 282:         if ($phery instanceof Phery && $phery->config['exceptions'] === true)
 283:         {
 284:             throw new PheryException($exception, $code);
 285:         }
 286: 
 287:         return false;
 288:     }
 289: 
 290: 
 291: 
 292:     /**
 293:      * Set any data to pass to the callbacks
 294:      *
 295:      * @param mixed $args,... Parameters, can be anything
 296:      *
 297:      * @return Phery
 298:      */
 299:     public function data($args)
 300:     {
 301:         foreach (func_get_args() as $arg)
 302:         {
 303:             if (is_array($arg))
 304:             {
 305:                 $this->data = array_merge_recursive($arg, $this->data);
 306:             }
 307:             else
 308:             {
 309:                 $this->data[] = $arg;
 310:             }
 311:         }
 312: 
 313:         return $this;
 314:     }
 315: 
 316:     /**
 317:      * Encode PHP code to put inside data-phery-args, usually for updating the data there
 318:      *
 319:      * @param array  $data     Any data that can be converted using json_encode
 320:      * @param string $encoding Encoding for the arguments
 321:      *
 322:      * @return string Return json_encode'd and htmlentities'd string
 323:      */
 324:     public static function args(array $data, $encoding = 'UTF-8')
 325:     {
 326:         return htmlentities(json_encode($data), ENT_COMPAT, $encoding, false);
 327:     }
 328: 
 329:     /**
 330:      * Output the meta HTML with the token.
 331:      * This method needs to use sessions through session_start
 332:      *
 333:      * @param bool $check Check if the current token is valid
 334:      * @param bool $force It will renew the current hash every call
 335:      * @return string|bool
 336:      */
 337:     public function csrf($check = false, $force = false)
 338:     {
 339:         if ($this->config['csrf'] !== true)
 340:         {
 341:             return !empty($check) ? true : '';
 342:         }
 343: 
 344:         if (session_id() == '')
 345:         {
 346:             @session_start();
 347:         }
 348: 
 349:         if ($check === false)
 350:         {
 351:             if ((!empty($_SESSION['phery']['csrf']) && $force) || empty($_SESSION['phery']['csrf']))
 352:             {
 353:                 $token = sha1(uniqid(microtime(true), true));
 354: 
 355:                 $_SESSION['phery'] = array(
 356:                     'csrf' => $token
 357:                 );
 358: 
 359:                 $token = base64_encode($token);
 360:             }
 361:             else
 362:             {
 363:                 $token = base64_encode($_SESSION['phery']['csrf']);
 364:             }
 365: 
 366:             return "<meta id=\"csrf-token\" name=\"csrf-token\" content=\"{$token}\" />\n";
 367:         }
 368:         else
 369:         {
 370:             if (empty($_SESSION['phery']['csrf']))
 371:             {
 372:                 return false;
 373:             }
 374: 
 375:             return $_SESSION['phery']['csrf'] === base64_decode($check, true);
 376:         }
 377:     }
 378: 
 379:     /**
 380:      * Check if the current call is an ajax call
 381:      *
 382:      * @param bool $is_phery Check if is an ajax call and a phery specific call
 383:      *
 384:      * @static
 385:      * @return bool
 386:      */
 387:     public static function is_ajax($is_phery = false)
 388:     {
 389:         switch ($is_phery)
 390:         {
 391:             case true:
 392:                 return (bool)(!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
 393:                 strcasecmp($_SERVER['HTTP_X_REQUESTED_WITH'], 'XMLHttpRequest') === 0 &&
 394:                 strtoupper($_SERVER['REQUEST_METHOD']) === 'POST' &&
 395:                 !empty($_SERVER['HTTP_X_PHERY']));
 396:             case false:
 397:                 return (bool)(!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
 398:                 strcasecmp($_SERVER['HTTP_X_REQUESTED_WITH'], 'XMLHttpRequest') === 0);
 399:         }
 400:         return false;
 401:     }
 402: 
 403:     /**
 404:      * Strip slashes recursive
 405:      *
 406:      * @param array|string $variable
 407:      * @return array|string
 408:      */
 409:     private function stripslashes_recursive($variable)
 410:     {
 411:         if (!empty($variable) && is_string($variable))
 412:         {
 413:             return stripslashes($variable);
 414:         }
 415: 
 416:         if (!empty($variable) && is_array($variable))
 417:         {
 418:             foreach ($variable as $i => $value)
 419:             {
 420:                 $variable[$i] = $this->stripslashes_recursive($value);
 421:             }
 422:         }
 423: 
 424:         return $variable;
 425:     }
 426: 
 427:     /**
 428:      * Flush loop
 429:      *
 430:      * @param bool $clean Discard buffers
 431:      */
 432:     private static function flush($clean = false)
 433:     {
 434:         while (ob_get_level() > 0)
 435:         {
 436:             $clean ? ob_end_clean() : ob_end_flush();
 437:         }
 438:     }
 439: 
 440:     /**
 441:      * Default error handler
 442:      *
 443:      * @param int $errno
 444:      * @param string $errstr
 445:      * @param string $errfile
 446:      * @param int $errline
 447:      */
 448:     public static function error_handler($errno, $errstr, $errfile, $errline)
 449:     {
 450:         self::flush(true);
 451: 
 452:         $response = PheryResponse::factory()->exception($errstr, array(
 453:             'code' => $errno,
 454:             'file' => Phery::$expose_paths ? $errfile : pathinfo($errfile, PATHINFO_BASENAME),
 455:             'line' => $errline
 456:         ));
 457: 
 458:         self::respond($response);
 459:         self::shutdown_handler(false, true);
 460:     }
 461: 
 462:     /**
 463:      * Default shutdown handler
 464:      *
 465:      * @param bool $errors
 466:      * @param bool $handled
 467:      */
 468:     public static function shutdown_handler($errors = false, $handled = false)
 469:     {
 470:         if ($handled)
 471:         {
 472:             self::flush();
 473:         }
 474: 
 475:         if ($errors === true && ($error = error_get_last()) && !$handled)
 476:         {
 477:             self::error_handler($error["type"], $error["message"], $error["file"], $error["line"]);
 478:         }
 479: 
 480:         if (!$handled)
 481:         {
 482:             self::flush();
 483:         }
 484: 
 485:         if (!session_id())
 486:         {
 487:             session_write_close();
 488:         }
 489: 
 490:         exit;
 491:     }
 492: 
 493:     /**
 494:      * Helper function to properly output the headers for a PheryResponse in case you need
 495:      * to manually return it (like when following a redirect)
 496:      *
 497:      * @param string|PheryResponse $response The response or a string
 498:      * @param bool                 $echo     Echo the response
 499:      *
 500:      * @return string
 501:      */
 502:     public static function respond($response, $echo = true)
 503:     {
 504:         if ($response instanceof PheryResponse)
 505:         {
 506:             if (!headers_sent())
 507:             {
 508:                 session_write_close();
 509: 
 510:                 header('Cache-Control: no-cache, must-revalidate', true);
 511:                 header('Expires: Sat, 26 Jul 1997 05:00:00 GMT', true);
 512:                 header('Content-Type: application/json; charset='.(strtolower(Phery::$encoding)), true);
 513:                 header('Connection: close', true);
 514:             }
 515:         }
 516: 
 517:         if ($response)
 518:         {
 519:             $response = "{$response}";
 520:         }
 521: 
 522:         if ($echo === true)
 523:         {
 524:             echo $response;
 525:         }
 526: 
 527:         return $response;
 528:     }
 529: 
 530:     /**
 531:      * Set the callback for view portions, as defined in Phery.view()
 532:      *
 533:      * @param array $views Array consisting of array('#id_of_view' => callback)
 534:      *                     The callback is like a normal phery callback, but the second parameter
 535:      *                     receives different data. But it MUST always return a PheryResponse with
 536:      *                     render_view(). You can do any manipulation like you would in regular
 537:      *                     callbacks. If you want to manipulate the DOM AFTER it was rendered, do it
 538:      *                     javascript side, using the afterHtml callback when setting up the views.
 539:      *
 540:      * <pre>
 541:      * Phery::instance()->views(array(
 542:      *     'section#container' => function($data, $params){
 543:      *          return
 544:      *              PheryResponse::factory()
 545:      *              ->render_view('html', array('extra data like titles, menus, etc'));
 546:      *      }
 547:      * ));
 548:      * </pre>
 549:      *
 550:      * @return Phery
 551:      */
 552:     public function views(array $views)
 553:     {
 554:         foreach ($views as $container => $callback)
 555:         {
 556:             if (is_callable($callback))
 557:             {
 558:                 $this->views[$container] = $callback;
 559:             }
 560:         }
 561: 
 562:         return $this;
 563:     }
 564: 
 565:     /**
 566:      * Initialize stuff before calling the AJAX function
 567:      *
 568:      * @return void
 569:      */
 570:     protected function before_user_func()
 571:     {
 572:         if ($this->config['error_reporting'] !== false)
 573:         {
 574:             set_error_handler('Phery::error_handler', $this->config['error_reporting']);
 575:         }
 576: 
 577:         if (empty($_POST['phery']['csrf']))
 578:         {
 579:             $_POST['phery']['csrf'] = '';
 580:         }
 581: 
 582:         if ($this->csrf($_POST['phery']['csrf']) === false)
 583:         {
 584:             self::exception($this, 'Invalid CSRF token', self::ERROR_CSRF);
 585:         }
 586:     }
 587: 
 588:     /**
 589:      * Process the requests if any
 590:      *
 591:      * @param boolean $last_call
 592:      *
 593:      * @return boolean
 594:      */
 595:     private function process_data($last_call)
 596:     {
 597:         $response = null;
 598:         $error = null;
 599:         $view = false;
 600: 
 601:         if (empty($_POST['phery']))
 602:         {
 603:             return self::exception($this, 'Non-Phery AJAX request', self::ERROR_PROCESS);
 604:         }
 605: 
 606:         if (!empty($_GET['_']))
 607:         {
 608:             $this->data['requested'] = (int)$_GET['_'];
 609:             unset($_GET['_']);
 610:         }
 611: 
 612:         if (isset($_GET['_try_count']))
 613:         {
 614:             $this->data['retries'] = (int)$_GET['_try_count'];
 615:             unset($_GET['_try_count']);
 616:         }
 617: 
 618:         $args = array();
 619:         $remote = false;
 620: 
 621:         if (!empty($_POST['phery']['remote']))
 622:         {
 623:             $remote = $_POST['phery']['remote'];
 624:         }
 625: 
 626:         if (!empty($_POST['phery']['submit_id']))
 627:         {
 628:             $this->data['submit_id'] = "#{$_POST['phery']['submit_id']}";
 629:         }
 630: 
 631:         if ($remote !== false)
 632:         {
 633:             $this->data['remote'] = $remote;
 634:         }
 635: 
 636:         if (!empty($_POST['args']))
 637:         {
 638:             $args = get_magic_quotes_gpc() ? $this->stripslashes_recursive($_POST['args']) : $_POST['args'];
 639: 
 640:             if ($last_call === true)
 641:             {
 642:                 unset($_POST['args']);
 643:             }
 644:         }
 645: 
 646:         foreach ($_POST['phery'] as $name => $post)
 647:         {
 648:             if (!isset($this->data[$name]))
 649:             {
 650:                 $this->data[$name] = $post;
 651:             }
 652:         }
 653: 
 654:         if (count($this->callbacks['before']))
 655:         {
 656:             foreach ($this->callbacks['before'] as $func)
 657:             {
 658:                 if (($args = call_user_func($func, $args, $this->data, $this)) === false)
 659:                 {
 660:                     return false;
 661:                 }
 662:             }
 663:         }
 664: 
 665:         if (!empty($_POST['phery']['view']))
 666:         {
 667:             $this->data['view'] = $_POST['phery']['view'];
 668:         }
 669: 
 670:         if ($remote !== false)
 671:         {
 672:             if (isset($this->functions[$remote]))
 673:             {
 674:                 if (isset($_POST['phery']['remote']))
 675:                 {
 676:                     unset($_POST['phery']['remote']);
 677:                 }
 678: 
 679:                 $this->before_user_func();
 680: 
 681:                 $response = call_user_func($this->functions[$remote], $args, $this->data, $this);
 682: 
 683:                 foreach ($this->callbacks['after'] as $func)
 684:                 {
 685:                     if (call_user_func($func, $args, $this->data, $response, $this) === false)
 686:                     {
 687:                         return false;
 688:                     }
 689:                 }
 690: 
 691:                 if (($response = self::respond($response, false)) === null)
 692:                 {
 693:                     $error = 'Response was void for function "'. htmlentities($remote, ENT_COMPAT, null, false). '"';
 694:                 }
 695: 
 696:                 $_POST['phery']['remote'] = $remote;
 697:             }
 698:             else
 699:             {
 700:                 if ($last_call)
 701:                 {
 702:                     self::exception($this, 'The function provided "' . htmlentities($remote, ENT_COMPAT, null, false) . '" isn\'t set', self::ERROR_PROCESS);
 703:                 }
 704:             }
 705:         }
 706:         else
 707:         {
 708:             if (!empty($this->data['view']) && isset($this->views[$this->data['view']]))
 709:             {
 710:                 $view = $this->data['view'];
 711: 
 712:                 $this->before_user_func();
 713: 
 714:                 $response = call_user_func($this->views[$this->data['view']], $args, $this->data, $this);
 715: 
 716:                 foreach ($this->callbacks['after'] as $func)
 717:                 {
 718:                     if (call_user_func($func, $args, $this->data, $response, $this) === false)
 719:                     {
 720:                         return false;
 721:                     }
 722:                 }
 723: 
 724:                 if (($response = self::respond($response, false)) === null)
 725:                 {
 726:                     $error = 'Response was void for view "'. htmlentities($this->data['view'], ENT_COMPAT, null, false) . '"';
 727:                 }
 728:             }
 729:             else
 730:             {
 731:                 if ($last_call)
 732:                 {
 733:                     if (!empty($this->data['view']))
 734:                     {
 735:                         self::exception($this, 'The provided view "' . htmlentities($this->data['view'], ENT_COMPAT, null, false) . '" isn\'t set', self::ERROR_PROCESS);
 736:                     }
 737:                     else
 738:                     {
 739:                         self::exception($this, 'Empty request', self::ERROR_PROCESS);
 740:                     }
 741:                 }
 742:             }
 743:         }
 744: 
 745:         if ($error !== null)
 746:         {
 747:             self::error_handler(E_NOTICE, $error, '', 0);
 748:         }
 749:         elseif ($response === null && $last_call & !$view)
 750:         {
 751:             $response = PheryResponse::factory();
 752:         }
 753:         elseif ($response !== null)
 754:         {
 755:             ob_start();
 756: 
 757:             if (!$this->config['return'])
 758:             {
 759:                 echo $response;
 760:             }
 761:         }
 762: 
 763:         if (!$this->config['return'] && $this->config['exit_allowed'] === true)
 764:         {
 765:             if ($last_call || $response !== null)
 766:             {
 767:                 exit;
 768:             }
 769:         }
 770:         elseif ($this->config['return'])
 771:         {
 772:             self::flush(true);
 773:         }
 774: 
 775:         if ($this->config['error_reporting'] !== false)
 776:         {
 777:             restore_error_handler();
 778:         }
 779: 
 780:         return $response;
 781:     }
 782: 
 783:     /**
 784:      * Process the AJAX requests if any.
 785:      *
 786:      * @param bool $last_call Set this to false if any other further calls
 787:      *                        to process() will happen, otherwise it will exit
 788:      *
 789:      * @throws PheryException
 790:      * @return boolean Return false if any error happened
 791:      */
 792:     public function process($last_call = true)
 793:     {
 794:         if (self::is_ajax(true))
 795:         {
 796:             // AJAX call
 797:             return $this->process_data($last_call);
 798:         }
 799:         return true;
 800:     }
 801: 
 802:     /**
 803:      * Config the current instance of Phery
 804:      *
 805:      * <code>
 806:      * array(
 807:      *     // Defaults to true, stop further script execution
 808:      *     'exit_allowed' => true|false,
 809:      *
 810:      *     // Throw exceptions on errors
 811:      *     'exceptions' => true|false,
 812:      *
 813:      *     // Return the responses in the process() call instead of echo'ing
 814:      *     'return' => true|false,
 815:      *
 816:      *     // Error reporting temporarily using error_reporting(). 'false' disables
 817:      *     // the error_reporting and wont try to catch any error.
 818:      *     // Anything else than false will throw a PheryResponse->exception() with
 819:      *     // the message
 820:      *     'error_reporting' => false|E_ALL|E_DEPRECATED|...
 821:      *
 822:      *     // By default, the function Phery::instance()->set() will only
 823:      *     // register functions when the current request is an AJAX call,
 824:      *     // to save resources. In order to use Phery::instance()->get_function()
 825:      *     // anytime, you need to set this config value to true
 826:      *     'set_always_available' => false|true
 827:      * );
 828:      * </code>
 829:      *
 830:      * If you pass a string, it will return the current config for the key specified
 831:      * Anything else, will output the current config as associative array
 832:      *
 833:      * @param string|array $config Associative array containing the following options
 834:      *
 835:      * @return Phery|string|array
 836:      */
 837:     public function config($config = null)
 838:     {
 839:         $register_function = false;
 840: 
 841:         if (!empty($config))
 842:         {
 843:             if (is_array($config))
 844:             {
 845:                 if (isset($config['exit_allowed']))
 846:                 {
 847:                     $this->config['exit_allowed'] = (bool)$config['exit_allowed'];
 848:                 }
 849: 
 850:                 if (isset($config['return']))
 851:                 {
 852:                     $this->config['return'] = (bool)$config['return'];
 853:                 }
 854: 
 855:                 if (isset($config['set_always_available']))
 856:                 {
 857:                     $this->config['set_always_available'] = (bool)$config['set_always_available'];
 858:                 }
 859: 
 860:                 if (isset($config['exceptions']))
 861:                 {
 862:                     $this->config['exceptions'] = (bool)$config['exceptions'];
 863:                 }
 864: 
 865:                 if (isset($config['csrf']))
 866:                 {
 867:                     $this->config['csrf'] = (bool)$config['csrf'];
 868:                 }
 869: 
 870:                 if (isset($config['error_reporting']))
 871:                 {
 872:                     if ($config['error_reporting'] !== false)
 873:                     {
 874:                         $this->config['error_reporting'] = (int)$config['error_reporting'];
 875:                     }
 876:                     else
 877:                     {
 878:                         $this->config['error_reporting'] = false;
 879:                     }
 880: 
 881:                     $register_function = true;
 882:                 }
 883: 
 884:                 if ($register_function || $this->init)
 885:                 {
 886:                     register_shutdown_function('Phery::shutdown_handler', $this->config['error_reporting'] !== false);
 887:                     $this->init = false;
 888:                 }
 889: 
 890:                 return $this;
 891:             }
 892:             elseif (!empty($config) && is_string($config) && isset($this->config[$config]))
 893:             {
 894:                 return $this->config[$config];
 895:             }
 896:         }
 897: 
 898:         return $this->config;
 899:     }
 900: 
 901:     /**
 902:      * Generates just one instance. Useful to use in many included files. Chainable
 903:      *
 904:      * @param array $config Associative config array
 905:      *
 906:      * @see __construct()
 907:      * @see config()
 908:      * @static
 909:      * @return Phery
 910:      */
 911:     public static function instance(array $config = array())
 912:     {
 913:         if (!(self::$instance instanceof Phery))
 914:         {
 915:             self::$instance = new Phery($config);
 916:         }
 917:         else if ($config)
 918:         {
 919:             self::$instance->config($config);
 920:         }
 921: 
 922:         return self::$instance;
 923:     }
 924: 
 925:     /**
 926:      * Sets the functions to respond to the ajax call.
 927:      * For security reasons, these functions should not be reacheable through POST/GET requests.
 928:      * These will be set only for AJAX requests as it will only be set in case of an ajax request,
 929:      * to save resources.
 930:      *
 931:      * You may set the config option "set_always_available" to true to always register the functions
 932:      * regardless of if it's an AJAX function or not going on.
 933:      *
 934:      * The answer/process function, should have the following structure:
 935:      *
 936:      * <code>
 937:      * function func($ajax_data, $callback_data, $phery){
 938:      *   $r = new PheryResponse; // or PheryResponse::factory();
 939:      *
 940:      *   // Sometimes the $callback_data will have an item called 'submit_id',
 941:      *   // is the ID of the calling DOM element.
 942:      *   // if (isset($callback_data['submit_id'])) {  }
 943:      *   // $phery will be the current phery instance that called this callback
 944:      *
 945:      *   $r->jquery('#id')->animate(...);
 946:      *   return $r; //Should always return the PheryResponse unless you are dealing with plain text
 947:      * }
 948:      * </code>
 949:      *
 950:      * @param array $functions An array of functions to register to the instance.
 951:      * <pre>
 952:      * array(
 953:      *   'function1' => 'function',
 954:      *   'function2' => array($this, 'method'),
 955:      *   'function3' => 'StaticClass::name',
 956:      *   'function4' => array(new ClassName, 'method'),
 957:      *   'function5' => function($data){}
 958:      * );
 959:      * </pre>
 960:      * @return Phery
 961:      */
 962:     public function set(array $functions)
 963:     {
 964:         if ($this->config['set_always_available'] === false && !self::is_ajax(true))
 965:         {
 966:             return $this;
 967:         }
 968: 
 969:         if (isset($functions) && is_array($functions))
 970:         {
 971:             foreach ($functions as $name => $func)
 972:             {
 973:                 if (is_callable($func))
 974:                 {
 975:                     $this->functions[$name] = $func;
 976:                 }
 977:                 else
 978:                 {
 979:                     self::exception($this, 'Provided function "' . $name . '" isnt a valid function or method', self::ERROR_SET);
 980:                 }
 981:             }
 982:         }
 983:         else
 984:         {
 985:             self::exception($this, 'Call to "set" must be provided an array', self::ERROR_SET);
 986:         }
 987: 
 988:         return $this;
 989:     }
 990: 
 991:     /**
 992:      * Unset a function previously set with set()
 993:      *
 994:      * @param string $name Name of the function
 995:      * @see set()
 996:      * @return Phery
 997:      */
 998:     public function unset_function($name)
 999:     {
1000:         if (isset($this->functions[$name]))
1001:         {
1002:             unset($this->functions[$name]);
1003:         }
1004:         return $this;
1005:     }
1006: 
1007:     /**
1008:      * Get previously function set with set() method
1009:      * If you pass aditional arguments, the function will be executed
1010:      * and this function will return the PheryResponse associated with
1011:      * that function
1012:      *
1013:      * <pre>
1014:      * Phery::get_function('render', ['<html></html>'])->appendTo('body');
1015:      * </pre>
1016:      *
1017:      * @param string $function_name The name of the function registed with set
1018:      * @param array $args Any arguments to pass to the function
1019:      * @see Phery::set()
1020:      * @return callable|array|string|PheryResponse|null
1021:      */
1022:     public function get_function($function_name, array $args = array())
1023:     {
1024:         if (isset($this->functions[$function_name]))
1025:         {
1026:             if (count($args))
1027:             {
1028:                 return call_user_func_array($this->functions[$function_name], $args);
1029:             }
1030: 
1031:             return $this->functions[$function_name];
1032:         }
1033:         return null;
1034:     }
1035: 
1036:     /**
1037:      * Create a new instance of Phery that can be chained, without the need of assigning it to a variable
1038:      *
1039:      * @param array $config Associative config array
1040:      *
1041:      * @see config()
1042:      * @static
1043:      * @return Phery
1044:      */
1045:     public static function factory(array $config = array())
1046:     {
1047:         return new Phery($config);
1048:     }
1049: 
1050:     /**
1051:      * Common check for all static factories
1052:      *
1053:      * @param array $attributes
1054:      * @param bool $include_method
1055:      *
1056:      * @return string
1057:      */
1058:     protected static function common_check(&$attributes, $include_method = true)
1059:     {
1060:         if (!empty($attributes['args']))
1061:         {
1062:             $attributes['data-phery-args'] = json_encode($attributes['args']);
1063:             unset($attributes['args']);
1064:         }
1065: 
1066:         if (!empty($attributes['confirm']))
1067:         {
1068:             $attributes['data-phery-confirm'] = $attributes['confirm'];
1069:             unset($attributes['confirm']);
1070:         }
1071: 
1072:         if (!empty($attributes['cache']))
1073:         {
1074:             $attributes['data-phery-cache'] = "1";
1075:             unset($attributes['cache']);
1076:         }
1077: 
1078:         if (!empty($attributes['target']))
1079:         {
1080:             $attributes['data-phery-target'] = $attributes['target'];
1081:             unset($attributes['target']);
1082:         }
1083: 
1084:         if (!empty($attributes['related']))
1085:         {
1086:             $attributes['data-phery-related'] = $attributes['related'];
1087:             unset($attributes['related']);
1088:         }
1089: 
1090:         if (!empty($attributes['phery-type']))
1091:         {
1092:             $attributes['data-phery-type'] = $attributes['phery-type'];
1093:             unset($attributes['phery-type']);
1094:         }
1095: 
1096:         if (!empty($attributes['only']))
1097:         {
1098:             $attributes['data-phery-only'] = $attributes['only'];
1099:             unset($attributes['only']);
1100:         }
1101: 
1102:         if (isset($attributes['clickable']))
1103:         {
1104:             $attributes['data-phery-clickable'] = "1";
1105:             unset($attributes['clickable']);
1106:         }
1107: 
1108:         if ($include_method)
1109:         {
1110:             if (isset($attributes['method']))
1111:             {
1112:                 $attributes['data-phery-method'] = $attributes['method'];
1113:                 unset($attributes['method']);
1114:             }
1115:         }
1116: 
1117:         $encoding = 'UTF-8';
1118:         if (isset($attributes['encoding']))
1119:         {
1120:             $encoding = $attributes['encoding'];
1121:             unset($attributes['encoding']);
1122:         }
1123: 
1124:         return $encoding;
1125:     }
1126: 
1127:     /**
1128:      * Helper function that generates an ajax link, defaults to "A" tag
1129:      *
1130:      * @param string $content    The content of the link. This is ignored for self closing tags, img, input, iframe
1131:      * @param string $function   The PHP function assigned name on Phery::set()
1132:      * @param array  $attributes Extra attributes that can be passed to the link, like class, style, etc
1133:      * <pre>
1134:      * array(
1135:      *     // Display confirmation on click
1136:      *     'confirm' => 'Are you sure?',
1137:      *
1138:      *     // The tag for the item, defaults to a. If the tag is set to img, the
1139:      *     // 'src' must be set in attributes parameter
1140:      *     'tag' => 'a',
1141:      *
1142:      *     // Define another URI for the AJAX call, this defines the HREF of A
1143:      *     'href' => '/path/to/url',
1144:      *
1145:      *     // Extra arguments to pass to the AJAX function, will be stored
1146:      *     // in the data-phery-args attribute as a JSON notation
1147:      *     'args' => array(1, "a"),
1148:      *
1149:      *     // Set the "href" attribute for non-anchor (a) AJAX tags (like buttons or spans).
1150:      *     // Works for A links too, but it won't function without javascript, through data-phery-target
1151:      *     'target' => '/default/ajax/controller',
1152:      *
1153:      *     // Define the data-phery-type for the expected response, json, xml, text, etc
1154:      *     'phery-type' => 'json',
1155:      *
1156:      *     // Enable clicking on structural HTML, like DIV, HEADER, HGROUP, etc
1157:      *     'clickable' => true,
1158:      *
1159:      *     // Force cache of the response
1160:      *     'cache' => true,
1161:      *
1162:      *     // Aggregate data from other DOM elements, can be forms, inputs (textarea, selects),
1163:      *     // pass multiple selectors, like "#input1,#form1,~ input:hidden,select.job"
1164:      *     // that are searched in this order:
1165:      *     // - $(this).find(related)
1166:      *     // - $(related)
1167:      *     // So you can use sibling, children selectors, like ~, +, >, :parent
1168:      *     // You can also, through Javascript, append a jQuery object to the related, using
1169:      *     // $('#element').phery('data', 'related', $('#other_element'));
1170:      *     'related' => true,
1171:      *
1172:      *     // Disables the AJAX on element while the last action is not completed
1173:      *     'only' => true,
1174:      *
1175:      *     // Set the encoding of the data, defaults to UTF-8
1176:      *     'encoding' => 'UTF-8',
1177:      *
1178:      *     // Set the method (for restful responses)
1179:      *     'method' => 'PUT'
1180:      * );
1181:      * </pre>
1182:      *
1183:      * @param Phery  $phery      Pass the current instance of phery, so it can check if the
1184:      *                           functions are defined, and throw exceptions
1185:      * @param boolean $no_close  Don't close the tag, useful if you want to create an AJAX DIV with a lot of content inside,
1186:      *                           but the DIV itself isn't clikable
1187:      *
1188:      * <pre>
1189:      *   <?php echo Phery::link_to('', 'remote', array('target' => '/another-url', 'args' => array('id' => 1), 'class' => 'ajaxified'), null, true); ?>
1190:      *     <p>This new content</p>
1191:      *     <div class="result></div>
1192:      *   </div>
1193:      *   <?php echo Phery::link_to('', 'remote', array('target' => '/another-url', 'args' => array('id' => 2), 'class' => 'ajaxified'), null, true); ?>
1194:      *     <p>Another content will have div result filled</p>
1195:      *     <div class="result></div>
1196:      *   </div>
1197:      *
1198:      *   <script>
1199:      *     $('.ajaxified').phery('remote');
1200:      *   </script>
1201:      * </pre>
1202:      *
1203:      * @static
1204:      * @return string The mounted HTML tag
1205:      */
1206:     public static function link_to($content, $function, array $attributes = array(), Phery $phery = null, $no_close = false)
1207:     {
1208:         if ($phery && !isset($phery->functions[$function]))
1209:         {
1210:             self::exception($phery, 'The function "' . $function . '" provided in "link_to" hasnt been set', self::ERROR_TO);
1211:         }
1212: 
1213:         $tag = 'a';
1214:         if (isset($attributes['tag']))
1215:         {
1216:             $tag = $attributes['tag'];
1217:             unset($attributes['tag']);
1218:         }
1219: 
1220:         $encoding = self::common_check($attributes);
1221: 
1222:         if ($function)
1223:         {
1224:             $attributes['data-phery-remote'] = $function;
1225:         }
1226: 
1227:         $ret = array();
1228:         $ret[] = "<{$tag}";
1229:         foreach ($attributes as $attribute => $value)
1230:         {
1231:             $ret[] = "{$attribute}=\"" . htmlentities($value, ENT_COMPAT, $encoding, false) . "\"";
1232:         }
1233: 
1234:         if (!in_array(strtolower($tag), array('img', 'input', 'iframe', 'hr', 'area', 'embed', 'keygen')))
1235:         {
1236:             $ret[] = ">{$content}";
1237:             if (!$no_close)
1238:             {
1239:                 $ret[] = "</{$tag}>";
1240:             }
1241:         }
1242:         else
1243:         {
1244:             $ret[] = "/>";
1245:         }
1246: 
1247:         return join(' ', $ret);
1248:     }
1249: 
1250:     /**
1251:      * Create a <form> tag with ajax enabled. Must be closed manually with </form>
1252:      *
1253:      * @param string $action   where to go, can be empty
1254:      * @param string $function Registered function name
1255:      * @param array  $attributes Configuration of the element plus any HTML attributes
1256:      *
1257:      * <pre>
1258:      * array(
1259:      *     //Confirmation dialog
1260:      *     'confirm' => 'Are you sure?',
1261:      *
1262:      *     // Type of call, defaults to JSON (to use PheryResponse)
1263:      *     'phery-type' => 'json',
1264:      *
1265:      *     // 'all' submits all elements on the form, even empty ones
1266:      *     // 'disabled' enables submitting disabled elements
1267:      *     'submit' => array('all' => true, 'disabled' => true),
1268:      *
1269:      *     // Disables the AJAX on element while the last action is not completed
1270:      *     'only' => true,
1271:      *
1272:      *     // Set the encoding of the data, defaults to UTF-8
1273:      *     'encoding' => 'UTF-8',
1274:      * );
1275:      * </pre>
1276:      *
1277:      * @param Phery  $phery    Pass the current instance of phery, so it can check if the functions are defined, and throw exceptions
1278:      *
1279:      * @static
1280:      * @return string The mounted &lt;form&gt; HTML tag
1281:      */
1282:     public static function form_for($action, $function, array $attributes = array(), Phery $phery = null)
1283:     {
1284:         if (!$function)
1285:         {
1286:             self::exception($phery, 'The "function" argument must be provided to "form_for"', self::ERROR_TO);
1287: 
1288:             return '';
1289:         }
1290: 
1291:         if ($phery && !isset($phery->functions[$function]))
1292:         {
1293:             self::exception($phery, 'The function "' . $function . '" provided in "form_for" hasnt been set', self::ERROR_TO);
1294:         }
1295: 
1296:         $encoding = self::common_check($attributes, false);
1297: 
1298:         if (isset($attributes['submit']))
1299:         {
1300:             $attributes['data-phery-submit'] = json_encode($attributes['submit']);
1301:             unset($attributes['submit']);
1302:         }
1303: 
1304:         $ret = array();
1305:         $ret[] = '<form method="POST" action="' . $action . '" data-phery-remote="' . $function . '"';
1306:         foreach ($attributes as $attribute => $value)
1307:         {
1308:             $ret[] = "{$attribute}=\"" . htmlentities($value, ENT_COMPAT, $encoding, false) . "\"";
1309:         }
1310:         $ret[] = '>';
1311: 
1312:         return join(' ', $ret);
1313:     }
1314: 
1315:     /**
1316:      * Create a <select> element with ajax enabled "onchange" event.
1317:      *
1318:      * @param string $function Registered function name
1319:      * @param array  $items    Options for the select, 'value' => 'text' representation
1320:      * @param array  $attributes Configuration of the element plus any HTML attributes
1321:      *
1322:      * <pre>
1323:      * array(
1324:      *     // Confirmation dialog
1325:      *     'confirm' => 'Are you sure?',
1326:      *
1327:      *     // Type of call, defaults to JSON (to use PheryResponse)
1328:      *     'phery-type' => 'json',
1329:      *
1330:      *     // The URL where it should call, translates to data-phery-target
1331:      *     'target' => '/path/to/php',
1332:      *
1333:      *     // Extra arguments to pass to the AJAX function, will be stored
1334:      *     // in the args attribute as a JSON notation, translates to data-phery-args
1335:      *     'args' => array(1, "a"),
1336:      *
1337:      *     // Set the encoding of the data, defaults to UTF-8
1338:      *     'encoding' => 'UTF-8',
1339:      *
1340:      *     // Disables the AJAX on element while the last action is not completed
1341:      *     'only' => true,
1342:      *
1343:      *     // The current selected value, or array(1,2) for multiple
1344:      *     'selected' => 1
1345:      *
1346:      *     // Set the method (for restful responses)
1347:      *     'method' => 'PUT'
1348:      * );
1349:      * </pre>
1350:      *
1351:      * @param Phery  $phery    Pass the current instance of phery, so it can check if the functions are defined, and throw exceptions
1352:      *
1353:      * @static
1354:      * @return string The mounted &lt;select&gt; with &lt;option&gt;s inside
1355:      */
1356:     public static function select_for($function, array $items, array $attributes = array(), Phery $phery = null)
1357:     {
1358:         if ($phery && !isset($phery->functions[$function]))
1359:         {
1360:             self::exception($phery, 'The function "' . $function . '" provided in "select_for" hasnt been set', self::ERROR_TO);
1361:         }
1362: 
1363:         $encoding = self::common_check($attributes);
1364: 
1365:         $selected = array();
1366:         if (isset($attributes['selected']))
1367:         {
1368:             if (is_array($attributes['selected']))
1369:             {
1370:                 // multiple select
1371:                 $selected = $attributes['selected'];
1372:             }
1373:             else
1374:             {
1375:                 // single select
1376:                 $selected = array($attributes['selected']);
1377:             }
1378:             unset($attributes['selected']);
1379:         }
1380: 
1381:         if (isset($attributes['multiple']))
1382:         {
1383:             $attributes['multiple'] = 'multiple';
1384:         }
1385: 
1386:         $ret = array();
1387:         $ret[] = '<select '.($function ? 'data-phery-remote="' . $function . '"' : '');
1388:         foreach ($attributes as $attribute => $value)
1389:         {
1390:             $ret[] = "{$attribute}=\"" . htmlentities($value, ENT_COMPAT, $encoding, false) . "\"";
1391:         }
1392:         $ret[] = '>';
1393: 
1394:         foreach ($items as $value => $text)
1395:         {
1396:             $_value = 'value="' . htmlentities($value, ENT_COMPAT, $encoding, false) . '"';
1397:             if (in_array($value, $selected))
1398:             {
1399:                 $_value .= ' selected="selected"';
1400:             }
1401:             $ret[] = "<option " . ($_value) . ">{$text}</option>\n";
1402:         }
1403:         $ret[] = '</select>';
1404: 
1405:         return join(' ', $ret);
1406:     }
1407: 
1408:     /**
1409:      * OffsetExists
1410:      *
1411:      * @param mixed $offset
1412:      *
1413:      * @return bool
1414:      */
1415:     public function offsetExists($offset)
1416:     {
1417:         return isset($this->data[$offset]);
1418:     }
1419: 
1420:     /**
1421:      * OffsetUnset
1422:      *
1423:      * @param mixed $offset
1424:      */
1425:     public function offsetUnset($offset)
1426:     {
1427:         if (isset($this->data[$offset]))
1428:         {
1429:             unset($this->data[$offset]);
1430:         }
1431:     }
1432: 
1433:     /**
1434:      * OffsetGet
1435:      *
1436:      * @param mixed $offset
1437:      *
1438:      * @return mixed|null
1439:      */
1440:     public function offsetGet($offset)
1441:     {
1442:         if (isset($this->data[$offset]))
1443:         {
1444:             return $this->data[$offset];
1445:         }
1446: 
1447:         return null;
1448:     }
1449: 
1450:     /**
1451:      * offsetSet
1452:      *
1453:      * @param mixed $offset
1454:      * @param mixed $value
1455:      */
1456:     public function offsetSet($offset, $value)
1457:     {
1458:         $this->data[$offset] = $value;
1459:     }
1460: 
1461:     /**
1462:      * Set shared data
1463:      * @param string $name
1464:      * @param mixed $value
1465:      */
1466:     public function __set($name, $value)
1467:     {
1468:         $this->data[$name] = $value;
1469:     }
1470: 
1471:     /**
1472:      * Get shared data
1473:      *
1474:      * @param string $name
1475:      *
1476:      * @return mixed
1477:      */
1478:     public function __get($name)
1479:     {
1480:         if (isset($this->data[$name]))
1481:         {
1482:             return $this->data[$name];
1483:         }
1484: 
1485:         return null;
1486:     }
1487: 
1488:     /**
1489:      * Utility function taken from MYSQL.
1490:      * To not raise any E_NOTICES (if enabled in your error reporting), call it with @ before
1491:      * the variables. Eg.: Phery::coalesce(@$var1, @$var['asdf']);
1492:      *
1493:      * @param mixed $args,... Any number of arguments
1494:      *
1495:      * @return mixed
1496:      */
1497:     public static function coalesce($args)
1498:     {
1499:         $args = func_get_args();
1500:         foreach ($args as &$arg)
1501:         {
1502:             if (isset($arg) && !empty($arg))
1503:             {
1504:                 return $arg;
1505:             }
1506:         }
1507: 
1508:         return null;
1509:     }
1510: }
1511: 
1512: /**
1513:  * Standard response for the json parser
1514:  * @package    Phery
1515:  *
1516:  * @method PheryResponse ajax(string $url, array $settings = null) Perform an asynchronous HTTP (Ajax) request.
1517:  * @method PheryResponse ajaxSetup(array $obj) Set default values for future Ajax requests.
1518:  * @method PheryResponse post(string $url, PheryFunction $success = null) Load data from the server using a HTTP POST request.
1519:  * @method PheryResponse get(string $url, PheryFunction $success = null) Load data from the server using a HTTP GET request.
1520:  * @method PheryResponse getJSON(string $url, PheryFunction $success = null) Load JSON-encoded data from the server using a GET HTTP request.
1521:  * @method PheryResponse getScript(string $url, PheryFunction $success = null) Load a JavaScript file from the server using a GET HTTP request, then execute it.
1522:  * @method PheryResponse detach() Detach a DOM element retaining the events attached to it
1523:  * @method PheryResponse prependTo(string $target) Prepend DOM element to target
1524:  * @method PheryResponse appendTo(string $target) Append DOM element to target
1525:  * @method PheryResponse replaceWith(string $newContent) The content to insert. May be an HTML string, DOM element, or jQuery object.
1526:  * @method PheryResponse css(string $propertyName, mixed $value = null) propertyName: A CSS property name. value: A value to set for the property.
1527:  * @method PheryResponse toggle($duration_or_array_of_options, PheryFunction $complete = null)  Display or hide the matched elements.
1528:  * @method PheryResponse is(string $selector) Check the current matched set of elements against a selector, element, or jQuery object and return true if at least one of these elements matches the given arguments.
1529:  * @method PheryResponse hide(string $speed = 0) Hide an object, can be animated with 'fast', 'slow', 'normal'
1530:  * @method PheryResponse show(string $speed = 0) Show an object, can be animated with 'fast', 'slow', 'normal'
1531:  * @method PheryResponse toggleClass(string $className) Add/Remove a class from an element
1532:  * @method PheryResponse data(string $name, mixed $data) Add data to element
1533:  * @method PheryResponse addClass(string $className) Add a class from an element
1534:  * @method PheryResponse removeClass(string $className) Remove a class from an element
1535:  * @method PheryResponse animate(array $prop, int $dur, string $easing = null, PheryFunction $cb = null) Perform a custom animation of a set of CSS properties.
1536:  * @method PheryResponse trigger(string $eventName, array $args = null) Trigger an event
1537:  * @method PheryResponse triggerHandler(string $eventType, array $extraParameters = null) Execute all handlers attached to an element for an event.
1538:  * @method PheryResponse fadeIn(string $speed) Fade in an element
1539:  * @method PheryResponse filter(string $selector) Reduce the set of matched elements to those that match the selector or pass the function's test.
1540:  * @method PheryResponse fadeTo(int $dur, float $opacity) Fade an element to opacity
1541:  * @method PheryResponse fadeOut(string $speed) Fade out an element
1542:  * @method PheryResponse slideUp(int $dur, PheryFunction $cb = null) Hide with slide up animation
1543:  * @method PheryResponse slideDown(int $dur, PheryFunction $cb = null) Show with slide down animation
1544:  * @method PheryResponse slideToggle(int $dur, PheryFunction $cb = null) Toggle show/hide the element, using slide animation
1545:  * @method PheryResponse unbind(string $name) Unbind an event from an element
1546:  * @method PheryResponse undelegate() Remove a handler from the event for all elements which match the current selector, now or in the future, based upon a specific set of root elements.
1547:  * @method PheryResponse stop() Stop animation on elements
1548:  * @method PheryResponse val(string $content) Set the value of an element
1549:  * @method PheryResponse removeData(string $name) Remove element data added with data()
1550:  * @method PheryResponse removeAttr(string $name) Remove an attribute from an element
1551:  * @method PheryResponse scrollTop(int $val) Set the scroll from the top
1552:  * @method PheryResponse scrollLeft(int $val) Set the scroll from the left
1553:  * @method PheryResponse height(int $val = null) Get or set the height from the left
1554:  * @method PheryResponse width(int $val = null) Get or set the width from the left
1555:  * @method PheryResponse slice(int $start, int $end) Reduce the set of matched elements to a subset specified by a range of indices.
1556:  * @method PheryResponse not(string $val) Remove elements from the set of matched elements.
1557:  * @method PheryResponse eq(int $selector) Reduce the set of matched elements to the one at the specified index.
1558:  * @method PheryResponse offset(array $coordinates) Set the current coordinates of every element in the set of matched elements, relative to the document.
1559:  * @method PheryResponse map(PheryFunction $callback) Pass each element in the current matched set through a function, producing a new jQuery object containing the return values.
1560:  * @method PheryResponse children(string $selector) Get the children of each element in the set of matched elements, optionally filtered by a selector.
1561:  * @method PheryResponse closest(string $selector) Get the first ancestor element that matches the selector, beginning at the current element and progressing up through the DOM tree.
1562:  * @method PheryResponse find(string $selector) Get the descendants of each element in the current set of matched elements, filtered by a selector, jQuery object, or element.
1563:  * @method PheryResponse next(string $selector = null) Get the immediately following sibling of each element in the set of matched elements, optionally filtered by a selector.
1564:  * @method PheryResponse nextAll(string $selector) Get all following siblings of each element in the set of matched elements, optionally filtered by a selector.
1565:  * @method PheryResponse nextUntil(string $selector) Get all following siblings of each element up to  but not including the element matched by the selector.
1566:  * @method PheryResponse parentsUntil(string $selector) Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector.
1567:  * @method PheryResponse offsetParent() Get the closest ancestor element that is positioned.
1568:  * @method PheryResponse parent(string $selector = null) Get the parent of each element in the current set of matched elements, optionally filtered by a selector.
1569:  * @method PheryResponse parents(string $selector) Get the ancestors of each element in the current set of matched elements, optionally filtered by a selector.
1570:  * @method PheryResponse prev(string $selector = null) Get the immediately preceding sibling of each element in the set of matched elements, optionally filtered by a selector.
1571:  * @method PheryResponse prevAll(string $selector) Get all preceding siblings of each element in the set of matched elements, optionally filtered by a selector.
1572:  * @method PheryResponse prevUntil(string $selector) Get the ancestors of each element in the current set of matched elements, optionally filtered by a selector.
1573:  * @method PheryResponse siblings(string $selector) Get the siblings of each element in the set of matched elements, optionally filtered by a selector.
1574:  * @method PheryResponse add(PheryResponse $selector) Add elements to the set of matched elements.
1575:  * @method PheryResponse contents() Get the children of each element in the set of matched elements, including text nodes.
1576:  * @method PheryResponse end() End the most recent filtering operation in the current chain and return the set of matched elements to its previous state.
1577:  * @method PheryResponse after(string $content) Insert content, specified by the parameter, after each element in the set of matched elements.
1578:  * @method PheryResponse before(string $content) Insert content, specified by the parameter, before each element in the set of matched elements.
1579:  * @method PheryResponse insertAfter(string $target) Insert every element in the set of matched elements after the target.
1580:  * @method PheryResponse insertBefore(string $target) Insert every element in the set of matched elements before the target.
1581:  * @method PheryResponse unwrap() Remove the parents of the set of matched elements from the DOM, leaving the matched elements in their place.
1582:  * @method PheryResponse wrap(string $wrappingElement) Wrap an HTML structure around each element in the set of matched elements.
1583:  * @method PheryResponse wrapAll(string $wrappingElement) Wrap an HTML structure around all elements in the set of matched elements.
1584:  * @method PheryResponse wrapInner(string $wrappingElement) Wrap an HTML structure around the content of each element in the set of matched elements.
1585:  * @method PheryResponse delegate(string $selector, string $eventType, PheryFunction $handler) Attach a handler to one or more events for all elements that match the selector, now or in the future, based on a specific set of root elements.
1586:  * @method PheryResponse one(string $eventType, PheryFunction $handler) Attach a handler to an event for the elements. The handler is executed at most once per element.
1587:  * @method PheryResponse bind(string $eventType, PheryFunction $handler) Attach a handler to an event for the elements.
1588:  * @method PheryResponse each(PheryFunction $function) Iterate over a jQ object, executing a function for each matched element.
1589:  * @method PheryResponse phery(string $function = null, array $args = null) Access the phery() on the select element(s)
1590:  * @method PheryResponse addBack(string $selector = null) Add the previous set of elements on the stack to the current set, optionally filtered by a selector.
1591:  * @method PheryResponse clearQueue(string $queueName = null) Remove from the queue all items that have not yet been run.
1592:  * @method PheryResponse clone(boolean $withDataAndEvents = null, boolean $deepWithDataAndEvents = null) Create a deep copy of the set of matched elements.
1593:  * @method PheryResponse dblclick(array $eventData = null, PheryFunction $handler = null) Bind an event handler to the "dblclick" JavaScript event, or trigger that event on an element.
1594:  * @method PheryResponse always(PheryFunction $callback) Bind an event handler to the "dblclick" JavaScript event, or trigger that event on an element.
1595:  * @method PheryResponse done(PheryFunction $callback) Add handlers to be called when the Deferred object is resolved.
1596:  * @method PheryResponse fail(PheryFunction $callback) Add handlers to be called when the Deferred object is rejected.
1597:  * @method PheryResponse progress(PheryFunction $callback) Add handlers to be called when the Deferred object is either resolved or rejected.
1598:  * @method PheryResponse then(PheryFunction $donecallback, PheryFunction $failcallback = null, PheryFunction $progresscallback = null) Add handlers to be called when the Deferred object is resolved, rejected, or still in progress.
1599:  * @method PheryResponse empty() Remove all child nodes of the set of matched elements from the DOM.
1600:  * @method PheryResponse finish(string $queue) Stop the currently-running animation, remove all queued animations, and complete all animations for the matched elements.
1601:  * @method PheryResponse focus(array $eventData = null, PheryFunction $handler = null)  Bind an event handler to the "focusout" JavaScript event.
1602:  * @method PheryResponse focusin(array $eventData = null, PheryFunction $handler = null)  Bind an event handler to the "focusin" event.
1603:  * @method PheryResponse focusout(array $eventData = null, PheryFunction $handler = null) Bind an event handler to the "focus" JavaScript event, or trigger that event on an element.
1604:  * @method PheryResponse has(string $selector) Reduce the set of matched elements to those that have a descendant that matches the selector or DOM element.
1605:  * @method PheryResponse index(string $selector = null) Search for a given element from among the matched elements.
1606:  * @method PheryResponse on(string $events, string $selector, array $data = null, PheryFunction $handler = null) Attach an event handler function for one or more events to the selected elements.
1607:  * @method PheryResponse off(string $events, string $selector = null, PheryFunction $handler = null) Remove an event handler.
1608:  * @method PheryResponse prop(string $propertyName, $data_or_function = null) Set one or more properties for the set of matched elements.
1609:  * @method PheryResponse promise(string $type = null, array $target = null) Return a Promise object to observe when all actions of a certain type bound to the collection, queued or not, have finished.
1610:  * @method PheryResponse pushStack(array $elements, string $name = null, array $arguments = null) Add a collection of DOM elements onto the jQuery stack.
1611:  * @method PheryResponse removeProp(string $propertyName) Remove a property for the set of matched elements.
1612:  * @method PheryResponse resize($eventData_or_function = null, PheryFunction $handler = null) Bind an event handler to the "resize" JavaScript event, or trigger that event on an element.
1613:  * @method PheryResponse scroll($eventData_or_function = null, PheryFunction $handler = null) Bind an event handler to the "scroll" JavaScript event, or trigger that event on an element.
1614:  * @method PheryResponse select($eventData_or_function = null, PheryFunction $handler = null) Bind an event handler to the "select" JavaScript event, or trigger that event on an element.
1615:  * @method PheryResponse serializeArray() Encode a set of form elements as an array of names and values.
1616:  * @method PheryResponse replaceAll(string $target) Replace each target element with the set of matched elements.
1617:  * @method PheryResponse reset() Reset a form element.
1618:  * @method PheryResponse toArray() Retrieve all the DOM elements contained in the jQuery set, as an array.
1619:  * @property PheryResponse this The DOM element that is making the AJAX call
1620:  * @property PheryResponse jquery The $ jQuery object, can be used to call $.getJSON, $.getScript, etc
1621:  * @property PheryResponse window Shortcut for jquery('window') / $(window)
1622:  * @property PheryResponse document Shortcut for jquery('document') / $(document)
1623:  */
1624: class PheryResponse extends ArrayObject {
1625: 
1626:     /**
1627:      * All responses that were created in the run, access them through their name
1628:      * @var PheryResponse[]
1629:      */
1630:     protected static $responses = array();
1631:     /**
1632:      * Common data available to all responses
1633:      * @var array
1634:      */
1635:     protected static $global = array();
1636:     /**
1637:      * Last jQuery selector defined
1638:      * @var string
1639:      */
1640:     protected $last_selector = null;
1641:     /**
1642:      * Restore the selector if set
1643:      * @var string
1644:      */
1645:     protected $restore = null;
1646:     /**
1647:      * Array containing answer data
1648:      * @var array
1649:      */
1650:     protected $data = array();
1651:     /**
1652:      * Array containing merged data
1653:      * @var array
1654:      */
1655:     protected $merged = array();
1656:     /**
1657:      * This response config
1658:      * @var array
1659:      */
1660:     protected $config = array();
1661:     /**
1662:      * Name of the current response
1663:      * @var string
1664:      */
1665:     protected $name = null;
1666:     /**
1667:      * Internal count for multiple paths
1668:      * @var int
1669:      */
1670:     protected static $internal_count = 0;
1671:     /**
1672:      * Internal count for multiple commands
1673:      * @var int
1674:      */
1675:     protected $internal_cmd_count = 0;
1676:     /**
1677:      * Is the criteria from unless fulfilled?
1678:      * @var bool
1679:      */
1680:     protected $matched = true;
1681: 
1682:     /**
1683:      * Construct a new response
1684:      *
1685:      * @param string $selector Create the object already selecting the DOM element
1686:      * @param array $constructor Only available if you are creating an element, like $('&lt;p/&gt;')
1687:      */
1688:     public function __construct($selector = null, array $constructor = array())
1689:     {
1690:         parent::__construct();
1691: 
1692:         $this->config = array(
1693:             'typecast_objects' => true,
1694:             'convert_integers' => true,
1695:         );
1696: 
1697:         $this->jquery($selector, $constructor);
1698: 
1699:         $this->set_response_name(uniqid("", true));
1700:     }
1701: 
1702:     /**
1703:      * Change the config for this response
1704:      * You may pass in an associative array of your config
1705:      *
1706:      * @param array $config
1707:      * <pre>
1708:      * array(
1709:      *   'convert_integers' => true/false
1710:      *   'typecast_objects' => true/false
1711:      * </pre>
1712:      *
1713:      * @return PheryResponse
1714:      */
1715:     public function set_config(array $config)
1716:     {
1717:         if (isset($config['convert_integers']))
1718:         {
1719:             $this->config['convert_integers'] = (bool)$config['convert_integers'];
1720:         }
1721: 
1722:         if (isset($config['typecast_objects']))
1723:         {
1724:             $this->config['typecast_objects'] = (bool)$config['typecast_objects'];
1725:         }
1726: 
1727:         return $this;
1728:     }
1729: 
1730:     /**
1731:      * Increment the internal counter, so there are no conflicting stacked commands
1732:      *
1733:      * @param string $type Selector
1734:      * @param boolean $force Force unajusted selector into place
1735:      * @return string The previous overwritten selector
1736:      */
1737:     protected function set_internal_counter($type, $force = false)
1738:     {
1739:         $last = $this->last_selector;
1740:         if ($force && $last !== null && !isset($this->data[$last])) {
1741:             $this->data[$last] = array();
1742:         }
1743:         $this->last_selector = '{'.$type.(self::$internal_count++).'}';
1744:         return $last;
1745:     }
1746: 
1747:     /**
1748:      * Renew the CSRF token on a given Phery instance
1749:      * Resets any selectors that were being chained before
1750:      *
1751:      * @param Phery $instance Instance of Phery
1752:      * @return PheryResponse
1753:      */
1754:     public function renew_csrf(Phery $instance)
1755:     {
1756:         if ($instance->config('csrf') === true)
1757:         {
1758:             $this->jquery('head meta#csrf-token')->replaceWith($instance->csrf());
1759:         }
1760: 
1761:         return $this;
1762:     }
1763: 
1764:     /**
1765:      * Set the name of this response
1766:      *
1767:      * @param string $name Name of current response
1768:      *
1769:      * @return PheryResponse
1770:      */
1771:     public function set_response_name($name)
1772:     {
1773:         if (!empty($this->name))
1774:         {
1775:             unset(self::$responses[$this->name]);
1776:         }
1777:         $this->name = $name;
1778:         self::$responses[$this->name] = $this;
1779: 
1780:         return $this;
1781:     }
1782: 
1783:     /**
1784:      * Broadcast a remote message to the client to all elements that
1785:      * are subscribed to them. This removes the current selector if any
1786:      *
1787:      * @param string $name Name of the browser subscribed topic on the element
1788:      * @param array [$params] Any params to pass to the subscribed topic
1789:      *
1790:      * @return PheryResponse
1791:      */
1792:     public function phery_broadcast($name, array $params = array())
1793:     {
1794:         $this->last_selector = null;
1795:         return $this->cmd(12, array($name, array($this->typecast($params, true, true)), true));
1796:     }
1797: 
1798:     /**
1799:      * Publish a remote message to the client that is subscribed to them
1800:      * This removes the current selector (if any)
1801:      *
1802:      * @param string $name Name of the browser subscribed topic on the element
1803:      * @param array [$params] Any params to pass to the subscribed topic
1804:      *
1805:      * @return PheryResponse
1806:      */
1807:     public function publish($name, array $params = array())
1808:     {
1809:         $this->last_selector = null;
1810:         return $this->cmd(12, array($name, array($this->typecast($params, true, true))));
1811:     }
1812: 
1813:     /**
1814:      * Get the name of this response
1815:      *
1816:      * @return null|string
1817:      */
1818:     public function get_response_name()
1819:     {
1820:         return $this->name;
1821:     }
1822: 
1823:     /**
1824:      * Borrowed from Ruby, the next imediate instruction will be executed unless
1825:      * it matches this criteria.
1826:      *
1827:      * <code>
1828:      *   $count = 3;
1829:      *   PheryResponse::factory()
1830:      *     // if not $count equals 2 then
1831:      *     ->unless($count === 2)
1832:      *     ->call('func'); // This won't trigger, $count is 2
1833:      * </code>
1834:      *
1835:      * <code>
1836:      *   PheryResponse::factory('.widget')
1837:      *     ->unless(PheryFunction::factory('return !this.hasClass("active");'), true)
1838:      *     ->remove(); // This won't remove if the element have the active class
1839:      * </code>
1840:      *
1841:      *
1842:      * @param boolean|PheryFunction $condition
1843:      * When not remote, can be any criteria that evaluates to FALSE.
1844:      * When it's remote, if passed a PheryFunction, it will skip the next
1845:      * iteration unless the return value of the PheryFunction is false.
1846:      * Passing a PheryFunction automatically sets $remote param to true
1847:      *
1848:      * @param bool $remote
1849:      * Instead of doing it in the server side, do it client side, for example,
1850:      * append something ONLY if an element exists. The context (this) of the function
1851:      * will be the last selected element or the calling element.
1852:      *
1853:      * @return PheryResponse
1854:      */
1855:     public function unless($condition, $remote = false)
1856:     {
1857:         if (!$remote && !($condition instanceof PheryFunction) && !($condition instanceof PheryResponse))
1858:         {
1859:             $this->matched = !$condition;
1860:         }
1861:         else
1862:         {
1863:             $this->set_internal_counter('!', true);
1864:             $this->cmd(0xff, array($this->typecast($condition, true, true)));
1865:         }
1866: 
1867:         return $this;
1868:     }
1869: 
1870:     /**
1871:      * It's the opposite of unless(), the next command will be issued in
1872:      * case the condition is true
1873:      *
1874:      * <code>
1875:      *   $count = 3;
1876:      *   PheryResponse::factory()
1877:      *     // if $count is greater than 2 then
1878:      *     ->incase($count > 2)
1879:      *     ->call('func'); // This will be executed, $count is greater than 2
1880:      * </code>
1881:      *
1882:      * <code>
1883:      *   PheryResponse::factory('.widget')
1884:      *     ->incase(PheryFunction::factory('return this.hasClass("active");'), true)
1885:      *     ->remove(); // This will remove the element if it has the active class
1886:      * </code>
1887:      *
1888:      * @param boolean|callable|PheryFunction $condition
1889:      * When not remote, can be any criteria that evaluates to TRUE.
1890:      * When it's remote, if passed a PheryFunction, it will execute the next
1891:      * iteration when the return value of the PheryFunction is true
1892:      *
1893:      * @param bool $remote
1894:      * Instead of doing it in the server side, do it client side, for example,
1895:      * append something ONLY if an element exists. The context (this) of the function
1896:      * will be the last selected element or the calling element.
1897:      *
1898:      * @return PheryResponse
1899:      */
1900:     public function incase($condition, $remote = false)
1901:     {
1902:         if (!$remote && !($condition instanceof PheryFunction) && !($condition instanceof PheryResponse))
1903:         {
1904:             $this->matched = $condition;
1905:         }
1906:         else
1907:         {
1908:             $this->set_internal_counter('=', true);
1909:             $this->cmd(0xff, array($this->typecast($condition, true, true)));
1910:         }
1911: 
1912:         return $this;
1913:     }
1914: 
1915:     /**
1916:      * This helper function is intended to normalize the $_FILES array, because when uploading multiple
1917:      * files, the order gets messed up. The result will always be in the format:
1918:      *
1919:      * <code>
1920:      * array(
1921:      *    'name of the file input' => array(
1922:      *       array(
1923:      *         'name' => ...,
1924:      *         'tmp_name' => ...,
1925:      *         'type' => ...,
1926:      *         'error' => ...,
1927:      *         'size' => ...,
1928:      *       ),
1929:      *       array(
1930:      *         'name' => ...,
1931:      *         'tmp_name' => ...,
1932:      *         'type' => ...,
1933:      *         'error' => ...,
1934:      *         'size' => ...,
1935:      *       ),
1936:      *    )
1937:      * );
1938:      * </code>
1939:      *
1940:      * So you can always do like (regardless of one or multiple files uploads)
1941:      *
1942:      * <code>
1943:      * <input name="avatar" type="file" multiple>
1944:      * <input name="pic" type="file">
1945:      *
1946:      * <?php
1947:      * foreach(PheryResponse::files('avatar') as $index => $file){
1948:      *     if (is_uploaded_file($file['tmp_name'])){
1949:      *        //...
1950:      *     }
1951:      * }
1952:      *
1953:      * foreach(PheryResponse::files() as $field => $group){
1954:      *   foreach ($group as $file){
1955:      *     if (is_uploaded_file($file['tmp_name'])){
1956:      *       if ($field === 'avatar') {
1957:      *          //...
1958:      *       } else if ($field === 'pic') {
1959:      *          //...
1960:      *       }
1961:      *     }
1962:      *   }
1963:      * }
1964:      * ?>
1965:      * </code>
1966:      *
1967:      * If no files were uploaded, returns an empty array.
1968:      *
1969:      * @param string|bool $group Pluck out the file group directly
1970:      * @return array
1971:      */
1972:     public static function files($group = false)
1973:     {
1974:         $result = array();
1975: 
1976:         foreach ($_FILES as $name => $keys)
1977:         {
1978:             if (is_array($keys))
1979:             {
1980:                 if (is_array($keys['name']))
1981:                 {
1982:                     $len = count($keys['name']);
1983:                     for ($i = 0; $i < $len; $i++)
1984:                     {
1985:                         $result[$name][$i] = array(
1986:                             'name' => $keys['name'][$i],
1987:                             'tmp_name' => $keys['tmp_name'][$i],
1988:                             'type' => $keys['type'][$i],
1989:                             'error' => $keys['error'][$i],
1990:                             'size' => $keys['size'][$i],
1991:                         );
1992:                     }
1993:                 }
1994:                 else
1995:                 {
1996:                     $result[$name] = array(
1997:                         $keys
1998:                     );
1999:                 }
2000:             }
2001:         }
2002: 
2003:         return $group !== false && isset($result[$group]) ? $result[$group] : $result;
2004:     }
2005: 
2006:     /**
2007:      * Set a global value that can be accessed through $pheryresponse['value']
2008:      * It's available in all responses, and can also be acessed using self['value']
2009:      *
2010:      * @param array|string Key => value combination or the name of the global
2011:      * @param mixed $value [Optional]
2012:      */
2013:     public static function set_global($name, $value = null)
2014:     {
2015:         if (isset($name) && is_array($name))
2016:         {
2017:             foreach ($name as $n => $v)
2018:             {
2019:                 self::$global[$n] = $v;
2020:             }
2021:         }
2022:         else
2023:         {
2024:             self::$global[$name] = $value;
2025:         }
2026:     }
2027: 
2028:     /**
2029:      * Unset a global variable
2030:      *
2031:      * @param string $name Variable name
2032:      */
2033:     public static function unset_global($name)
2034:     {
2035:         unset(self::$global[$name]);
2036:     }
2037: 
2038:     /**
2039:      * Will check for globals and local values
2040:      *
2041:      * @param string|int $index
2042:      *
2043:      * @return mixed
2044:      */
2045:     public function offsetExists($index)
2046:     {
2047:         if (isset(self::$global[$index]))
2048:         {
2049:             return true;
2050:         }
2051: 
2052:         return parent::offsetExists($index);
2053:     }
2054: 
2055:     /**
2056:      * Set local variables, will be available only in this instance
2057:      *
2058:      * @param string|int|null $index
2059:      * @param mixed           $newval
2060:      *
2061:      * @return void
2062:      */
2063:     public function offsetSet($index, $newval)
2064:     {
2065:         if ($index === null)
2066:         {
2067:             $this[] = $newval;
2068:         }
2069:         else
2070:         {
2071:             parent::offsetSet($index, $newval);
2072:         }
2073:     }
2074: 
2075:     /**
2076:      * Return null if no value
2077:      *
2078:      * @param mixed $index
2079:      *
2080:      * @return mixed|null
2081:      */
2082:     public function offsetGet($index)
2083:     {
2084:         if (parent::offsetExists($index))
2085:         {
2086:             return parent::offsetGet($index);
2087:         }
2088:         if (isset(self::$global[$index]))
2089:         {
2090:             return self::$global[$index];
2091:         }
2092: 
2093:         return null;
2094:     }
2095: 
2096:     /**
2097:      * Get a response by name
2098:      *
2099:      * @param string $name
2100:      *
2101:      * @return PheryResponse|null
2102:      */
2103:     public static function get_response($name)
2104:     {
2105:         if (isset(self::$responses[$name]) && self::$responses[$name] instanceof PheryResponse)
2106:         {
2107:             return self::$responses[$name];
2108:         }
2109: 
2110:         return null;
2111:     }
2112: 
2113:     /**
2114:      * Get merged response data as a new PheryResponse.
2115:      * This method works like a constructor if the previous response was destroyed
2116:      *
2117:      * @param string $name Name of the merged response
2118:      * @return PheryResponse|null
2119:      */
2120:     public function get_merged($name)
2121:     {
2122:         if (isset($this->merged[$name]))
2123:         {
2124:             if (isset(self::$responses[$name]))
2125:             {
2126:                 return self::$responses[$name];
2127:             }
2128:             $response = new PheryResponse;
2129:             $response->data = $this->merged[$name];
2130:             return $response;
2131:         }
2132:         return null;
2133:     }
2134: 
2135:     /**
2136:      * Same as phery.remote()
2137:      *
2138:      * @param string  $remote     Function
2139:      * @param array   $args       Arguments to pass to the
2140:      * @param array   $attr       Here you may set like method, target, type, cache, proxy
2141:      * @param boolean $directCall Setting to false returns the jQuery object, that can bind
2142:      *                            events, append to DOM, etc
2143:      *
2144:      * @return PheryResponse
2145:      */
2146:     public function phery_remote($remote, $args = array(), $attr = array(), $directCall = true)
2147:     {
2148:         $this->set_internal_counter('-');
2149: 
2150:         return $this->cmd(0xff, array(
2151:             $remote,
2152:             $args,
2153:             $attr,
2154:             $directCall
2155:         ));
2156:     }
2157: 
2158:     /**
2159:      * Set a global variable, that can be accessed directly through window object,
2160:      * can set properties inside objects if you pass an array as the variable.
2161:      * If it doesn't exist it will be created
2162:      *
2163:      * <code>
2164:      * // window.customer_info = {'name': 'John','surname': 'Doe', 'age': 39}
2165:      * PheryResponse::factory()->set_var('customer_info', array('name' => 'John', 'surname' => 'Doe', 'age' => 39));
2166:      * </code>
2167:      *
2168:      * <code>
2169:      * // window.customer_info.name = 'John'
2170:      * PheryResponse::factory()->set_var(array('customer_info','name'), 'John');
2171:      * </code>
2172:      *
2173:      * @param string|array $variable Global variable name
2174:      * @param mixed        $data     Any data
2175:      * @return PheryResponse
2176:      */
2177:     public function set_var($variable, $data)
2178:     {
2179:         $this->last_selector = null;
2180: 
2181:         if (!empty($data) && is_array($data))
2182:         {
2183:             foreach ($data as $name => $d)
2184:             {
2185:                 $data[$name] = $this->typecast($d, true, true);
2186:             }
2187:         }
2188:         else
2189:         {
2190:             $data = $this->typecast($data, true, true);
2191:         }
2192: 
2193:         return $this->cmd(9, array(
2194:             !is_array($variable) ? array($variable) : $variable,
2195:             array($data)
2196:         ));
2197:     }
2198: 
2199:     /**
2200:      * Delete a global variable, that can be accessed directly through window, can unset object properties,
2201:      * if you pass an array
2202:      *
2203:      * <code>
2204:      * PheryResponse::factory()->unset('customer_info');
2205:      * </code>
2206:      *
2207:      * <code>
2208:      * PheryResponse::factory()->unset(array('customer_info','name')); // translates to delete customer_info['name']
2209:      * </code>
2210:      *
2211:      * @param string|array $variable Global variable name
2212:      * @return PheryResponse
2213:      */
2214:     public function unset_var($variable)
2215:     {
2216:         $this->last_selector = null;
2217: 
2218:         return $this->cmd(9, array(
2219:             !is_array($variable) ? array($variable) : $variable,
2220:         ));
2221:     }
2222: 
2223:     /**
2224:      * Create a new PheryResponse instance for chaining, fast and effective for one line returns
2225:      *
2226:      * <code>
2227:      * function answer($data)
2228:      * {
2229:      *  return
2230:      *         PheryResponse::factory('a#link-'.$data['rel'])
2231:      *         ->attr('href', '#')
2232:      *         ->alert('done');
2233:      * }
2234:      * </code>
2235:      *
2236:      * @param string $selector optional
2237:      * @param array $constructor Same as $('&lt;p/&gt;', {})
2238:      *
2239:      * @static
2240:      * @return PheryResponse
2241:      */
2242:     public static function factory($selector = null, array $constructor = array())
2243:     {
2244:         return new PheryResponse($selector, $constructor);
2245:     }
2246: 
2247:     /**
2248:      * Remove a batch of calls for a selector. Won't remove for merged responses.
2249:      * Passing an integer, will remove commands, like dump_vars, call, etc, in the
2250:      * order they were called
2251:      *
2252:      * @param string|int $selector
2253:      *
2254:      * @return PheryResponse
2255:      */
2256:     public function remove_selector($selector)
2257:     {
2258:         if ((is_string($selector) || is_int($selector)) && isset($this->data[$selector]))
2259:         {
2260:             unset($this->data[$selector]);
2261:         }
2262: 
2263:         return $this;
2264:     }
2265: 
2266:     /**
2267:      * Access the current calling DOM element without the need for IDs, names, etc
2268:      * Use $response->this (as a property) instead
2269:      *
2270:      * @deprecated
2271:      * @return PheryResponse
2272:      */
2273:     public function this()
2274:     {
2275:         return $this->this;
2276:     }
2277: 
2278:     /**
2279:      * Merge another response to this one.
2280:      * Selectors with the same name will be added in order, for example:
2281:      *
2282:      * <code>
2283:      * function process()
2284:      * {
2285:      *      $response = PheryResponse::factory('a.links')->remove();
2286:      *      // $response will execute before
2287:      *      // there will be no more "a.links" in the DOM, so the addClass() will fail silently
2288:      *      // to invert the order, merge $response to $response2
2289:      *      $response2 = PheryResponse::factory('a.links')->addClass('red');
2290:      *      return $response->merge($response2);
2291:      * }
2292:      * </code>
2293:      *
2294:      * @param PheryResponse|string $phery_response Another PheryResponse object or a name of response
2295:      *
2296:      * @return PheryResponse
2297:      */
2298:     public function merge($phery_response)
2299:     {
2300:         if (is_string($phery_response))
2301:         {
2302:             if (isset(self::$responses[$phery_response]))
2303:             {
2304:                 $this->merged[self::$responses[$phery_response]->name] = self::$responses[$phery_response]->data;
2305:             }
2306:         }
2307:         elseif ($phery_response instanceof PheryResponse)
2308:         {
2309:             $this->merged[$phery_response->name] = $phery_response->data;
2310:         }
2311: 
2312:         return $this;
2313:     }
2314: 
2315:     /**
2316:      * Remove a previously merged response, if you pass TRUE will removed all merged responses
2317:      *
2318:      * @param PheryResponse|string|boolean $phery_response
2319:      *
2320:      * @return PheryResponse
2321:      */
2322:     public function unmerge($phery_response)
2323:     {
2324:         if (is_string($phery_response))
2325:         {
2326:             if (isset(self::$responses[$phery_response]))
2327:             {
2328:                 unset($this->merged[self::$responses[$phery_response]->name]);
2329:             }
2330:         }
2331:         elseif ($phery_response instanceof PheryResponse)
2332:         {
2333:             unset($this->merged[$phery_response->name]);
2334:         }
2335:         elseif ($phery_response === true)
2336:         {
2337:             $this->merged = array();
2338:         }
2339: 
2340:         return $this;
2341:     }
2342: 
2343:     /**
2344:      * Pretty print to console.log
2345:      *
2346:      * @param mixed $vars,... Any var
2347:      *
2348:      * @return PheryResponse
2349:      */
2350:     public function print_vars($vars)
2351:     {
2352:         $this->last_selector = null;
2353: 
2354:         $args = array();
2355:         foreach (func_get_args() as $name => $arg)
2356:         {
2357:             if (is_object($arg))
2358:             {
2359:                 $arg = get_object_vars($arg);
2360:             }
2361:             $args[$name] = array(var_export($arg, true));
2362:         }
2363: 
2364:         return $this->cmd(6, $args);
2365:     }
2366: 
2367:     /**
2368:      * Dump var to console.log
2369:      *
2370:      * @param mixed $vars,... Any var
2371:      *
2372:      * @return PheryResponse
2373:      */
2374:     public function dump_vars($vars)
2375:     {
2376:         $this->last_selector = null;
2377:         $args = array();
2378:         foreach (func_get_args() as $index => $func)
2379:         {
2380:             if ($func instanceof PheryResponse || $func instanceof PheryFunction)
2381:             {
2382:                 $args[$index] = array($this->typecast($func, true, true));
2383:             }
2384:             elseif (is_object($func))
2385:             {
2386:                 $args[$index] = array(get_object_vars($func));
2387:             }
2388:             else
2389:             {
2390:                 $args[$index] = array($func);
2391:             }
2392:         }
2393: 
2394:         return $this->cmd(6, $args);
2395:     }
2396: 
2397:     /**
2398:      * Sets the jQuery selector, so you can chain many calls to it.
2399:      *
2400:      * <code>
2401:      * PheryResponse::factory()
2402:      * ->jquery('.slides')
2403:      * ->fadeTo(0,0)
2404:      * ->css(array('top' => '10px', 'left' => '90px'));
2405:      * </code>
2406:      *
2407:      * For creating an element
2408:      *
2409:      * <code>
2410:      * PheryResponse::factory()
2411:      * ->jquery('.slides', array(
2412:      *   'css' => array(
2413:      *     'left': '50%',
2414:      *     'textDecoration': 'underline'
2415:      *   )
2416:      * ))
2417:      * ->appendTo('body');
2418:      * </code>
2419:      *
2420:      * @param string $selector Sets the current selector for subsequent chaining, like you would using $()
2421:      * @param array $constructor Only available if you are creating a new element, like $('&lt;p/&gt;', {'class': 'classname'})
2422:      *
2423:      * @return PheryResponse
2424:      */
2425:     public function jquery($selector, array $constructor = array())
2426:     {
2427:         if ($selector)
2428:         {
2429:             $this->last_selector = $selector;
2430:         }
2431: 
2432:         if (isset($selector) && is_string($selector) && count($constructor) && substr($selector, 0, 1) === '<')
2433:         {
2434:             foreach ($constructor as $name => $value)
2435:             {
2436:                 $this->$name($value);
2437:             }
2438:         }
2439:         return $this;
2440:     }
2441: 
2442:     /**
2443:      * Shortcut/alias for jquery($selector) Passing null works like jQuery.func
2444:      *
2445:      * @param string $selector Sets the current selector for subsequent chaining
2446:      * @param array $constructor Only available if you are creating a new element, like $('&lt;p/&gt;', {})
2447:      *
2448:      * @return PheryResponse
2449:      */
2450:     public function j($selector, array $constructor = array())
2451:     {
2452:         return $this->jquery($selector, $constructor);
2453:     }
2454: 
2455:     /**
2456:      * Show an alert box
2457:      *
2458:      * @param string $msg Message to be displayed
2459:      *
2460:      * @return PheryResponse
2461:      */
2462:     public function alert($msg)
2463:     {
2464:         if (is_array($msg))
2465:         {
2466:             $msg = join("\n", $msg);
2467:         }
2468: 
2469:         $this->last_selector = null;
2470: 
2471:         return $this->cmd(1, array($this->typecast($msg, true)));
2472:     }
2473: 
2474:     /**
2475:      * Pass JSON to the browser
2476:      *
2477:      * @param mixed $obj Data to be encoded to json (usually an array or a JsonSerializable)
2478:      *
2479:      * @return PheryResponse
2480:      */
2481:     public function json($obj)
2482:     {
2483:         $this->last_selector = null;
2484: 
2485:         return $this->cmd(4, array(json_encode($obj)));
2486:     }
2487: 
2488:     /**
2489:      * Remove the current jQuery selector
2490:      *
2491:      * @param string|boolean $selector Set a selector
2492:      *
2493:      * @return PheryResponse
2494:      */
2495:     public function remove($selector = null)
2496:     {
2497:         return $this->cmd('remove', array(), $selector);
2498:     }
2499: 
2500:     /**
2501:      * Add a command to the response
2502:      *
2503:      * @param int|string|array $cmd      Integer for command, see Phery.js for more info
2504:      * @param array            $args     Array to pass to the response
2505:      * @param string           $selector Insert the jquery selector
2506:      *
2507:      * @return PheryResponse
2508:      */
2509:     public function cmd($cmd, array $args = array(), $selector = null)
2510:     {
2511:         if (!$this->matched)
2512:         {
2513:             $this->matched = true;
2514:             return $this;
2515:         }
2516: 
2517:         $selector = Phery::coalesce($selector, $this->last_selector);
2518: 
2519:         if ($selector === null)
2520:         {
2521:             $this->data['0'.($this->internal_cmd_count++)] = array(
2522:                 'c' => $cmd,
2523:                 'a' => $args
2524:             );
2525:         }
2526:         else
2527:         {
2528:             if (!isset($this->data[$selector]))
2529:             {
2530:                 $this->data[$selector] = array();
2531:             }
2532:             $this->data[$selector][] = array(
2533:                 'c' => $cmd,
2534:                 'a' => $args
2535:             );
2536:         }
2537: 
2538:         if ($this->restore !== null)
2539:         {
2540:             $this->last_selector = $this->restore;
2541:             $this->restore = null;
2542:         }
2543: 
2544:         return $this;
2545:     }
2546: 
2547:     /**
2548:      * Set the attribute of a jQuery selector
2549:      *
2550:      * Example:
2551:      *
2552:      * <code>
2553:      * PheryResponse::factory()
2554:      * ->attr('href', 'http://url.com', 'a#link-' . $args['id']);
2555:      * </code>
2556:      *
2557:      * @param string $attr     HTML attribute of the item
2558:      * @param string $data     Value
2559:      * @param string $selector [optional] Provide the jQuery selector directly
2560:      *
2561:      * @return PheryResponse
2562:      */
2563:     public function attr($attr, $data, $selector = null)
2564:     {
2565:         return $this->cmd('attr', array(
2566:             $attr,
2567:             $data
2568:         ), $selector);
2569:     }
2570: 
2571:     /**
2572:      * Trigger the phery:exception event on the calling element
2573:      * with additional data
2574:      *
2575:      * @param string $msg  Message to pass to the exception
2576:      * @param mixed  $data Any data to pass, can be anything
2577:      *
2578:      * @return PheryResponse
2579:      */
2580:     public function exception($msg, $data = null)
2581:     {
2582:         $this->last_selector = null;
2583: 
2584:         return $this->cmd(7, array(
2585:             $msg,
2586:             $data
2587:         ));
2588:     }
2589: 
2590:     /**
2591:      * Call a javascript function.
2592:      * Warning: calling this function will reset the selector jQuery selector previously stated
2593:      *
2594:      * The context of `this` call is the object in the $func_name path or window, if not provided
2595:      *
2596:      * @param string|array $func_name Function name. If you pass a string, it will be accessed on window.func.
2597:      *                                If you pass an array, it will access a member of an object, like array('object', 'property', 'function')
2598:      * @param              mixed      $args,... Any additional arguments to pass to the function
2599:      *
2600:      * @return PheryResponse
2601:      */
2602:     public function call($func_name, $args = null)
2603:     {
2604:         $args = func_get_args();
2605:         array_shift($args);
2606:         $this->last_selector = null;
2607: 
2608:         return $this->cmd(2, array(
2609:             !is_array($func_name) ? array($func_name) : $func_name,
2610:             $args
2611:         ));
2612:     }
2613: 
2614:     /**
2615:      * Call 'apply' on a javascript function.
2616:      * Warning: calling this function will reset the selector jQuery selector previously stated
2617:      *
2618:      * The context of `this` call is the object in the $func_name path or window, if not provided
2619:      *
2620:      * @param string|array $func_name Function name
2621:      * @param array        $args      Any additional arguments to pass to the function
2622:      *
2623:      * @return PheryResponse
2624:      */
2625:     public function apply($func_name, array $args = array())
2626:     {
2627:         $this->last_selector = null;
2628: 
2629:         return $this->cmd(2, array(
2630:             !is_array($func_name) ? array($func_name) : $func_name,
2631:             $args
2632:         ));
2633:     }
2634: 
2635:     /**
2636:      * Clear the selected attribute.
2637:      * Alias for attr('attribute', '')
2638:      *
2639:      * @see attr()
2640:      *
2641:      * @param string $attr     Name of the DOM attribute to clear, such as 'innerHTML', 'style', 'href', etc not the jQuery counterparts
2642:      * @param string $selector [optional] Provide the jQuery selector directly
2643:      *
2644:      * @return PheryResponse
2645:      */
2646:     public function clear($attr, $selector = null)
2647:     {
2648:         return $this->attr($attr, '', $selector);
2649:     }
2650: 
2651:     /**
2652:      * Set the HTML content of an element.
2653:      * Automatically typecasted to string, so classes that
2654:      * respond to __toString() will be converted automatically
2655:      *
2656:      * @param string $content
2657:      * @param string $selector [optional] Provide the jQuery selector directly
2658:      *
2659:      * @return PheryResponse
2660:      */
2661:     public function html($content, $selector = null)
2662:     {
2663:         if (is_array($content))
2664:         {
2665:             $content = join("\n", $content);
2666:         }
2667: 
2668:         return $this->cmd('html', array(
2669:             $this->typecast($content, true, true)
2670:         ), $selector);
2671:     }
2672: 
2673:     /**
2674:      * Set the text of an element.
2675:      * Automatically typecasted to string, so classes that
2676:      * respond to __toString() will be converted automatically
2677:      *
2678:      * @param string $content
2679:      * @param string $selector [optional] Provide the jQuery selector directly
2680:      *
2681:      * @return PheryResponse
2682:      */
2683:     public function text($content, $selector = null)
2684:     {
2685:         if (is_array($content))
2686:         {
2687:             $content = join("\n", $content);
2688:         }
2689: 
2690:         return $this->cmd('text', array(
2691:             $this->typecast($content, true, true)
2692:         ), $selector);
2693:     }
2694: 
2695:     /**
2696:      * Compile a script and call it on-the-fly.
2697:      * There is a closure on the executed function, so
2698:      * to reach out global variables, you need to use window.variable
2699:      * Warning: calling this function will reset the selector jQuery selector previously set
2700:      *
2701:      * @param string|array $script Script content. If provided an array, it will be joined with \n
2702:      *
2703:      * <pre>
2704:      * PheryResponse::factory()
2705:      * ->script(array("if (confirm('Are you really sure?')) $('*').remove()"));
2706:      * </pre>
2707:      *
2708:      * @return PheryResponse
2709:      */
2710:     public function script($script)
2711:     {
2712:         $this->last_selector = null;
2713: 
2714:         if (is_array($script))
2715:         {
2716:             $script = join("\n", $script);
2717:         }
2718: 
2719:         return $this->cmd(3, array(
2720:             $script
2721:         ));
2722:     }
2723: 
2724:     /**
2725:      * Access a global object path
2726:      *
2727:      * @param string|string[] $namespace             For accessing objects, like $.namespace.function() or
2728:      *                                               document.href. if you want to access a global variable,
2729:      *                                               use array('object','property'). You may use a mix of getter/setter
2730:      *                                               to apply a global value to a variable
2731:      *
2732:      * <pre>
2733:      * PheryResponse::factory()->set_var(array('obj','newproperty'),
2734:      *      PheryResponse::factory()->access(array('other_obj','enabled'))
2735:      * );
2736:      * </pre>
2737:      *
2738:      * @param boolean         $new                   Create a new instance of the object, acts like "var v = new JsClass"
2739:      *                                               only works on classes, don't try to use new on a variable or a property
2740:      *                                               that can't be instantiated
2741:      *
2742:      * @return PheryResponse
2743:      */
2744:     public function access($namespace, $new = false)
2745:     {
2746:         $last = $this->set_internal_counter('+');
2747: 
2748:         return $this->cmd(!is_array($namespace) ? array($namespace) : $namespace, array($new, $last));
2749:     }
2750: 
2751:     /**
2752:      * Render a view to the container previously specified
2753:      *
2754:      * @param string $html HTML to be replaced in the container
2755:      * @param array  $data Array of data to pass to the before/after functions set on Phery.view
2756:      *
2757:      * @see Phery.view() on JS
2758:      * @return PheryResponse
2759:      */
2760:     public function render_view($html, $data = array())
2761:     {
2762:         $this->last_selector = null;
2763: 
2764:         if (is_array($html))
2765:         {
2766:             $html = join("\n", $html);
2767:         }
2768: 
2769:         return $this->cmd(5, array(
2770:             $this->typecast($html, true, true),
2771:             $data
2772:         ));
2773:     }
2774: 
2775:     /**
2776:      * Creates a redirect
2777:      *
2778:      * @param string        $url      Complete url with http:// (according to W3 http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.30)
2779:      * @param bool|string   $view     Internal means that phery will cancel the
2780:      *                                current DOM manipulation and commands and will issue another
2781:      *                                phery.remote to the location in url, useful if your PHP code is
2782:      *                                issuing redirects but you are using AJAX views.
2783:      *                                Passing false will issue a browser redirect
2784:      *
2785:      * @return PheryResponse
2786:      */
2787:     public function redirect($url, $view = false)
2788:     {
2789:         if ($view === false && !preg_match('#^https?\://#i', $url))
2790:         {
2791:             $_url = (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === 'off' ? 'http://' : 'https://') . $_SERVER['HTTP_HOST'];
2792:             $start = substr($url, 0, 1);
2793: 
2794:             if (!empty($start))
2795:             {
2796:                 if ($start === '?')
2797:                 {
2798:                     $_url .= str_replace('?' . $_SERVER['QUERY_STRING'], '', $_SERVER['REQUEST_URI']);
2799:                 }
2800:                 elseif ($start !== '/')
2801:                 {
2802:                     $_url .= '/';
2803:                 }
2804:             }
2805:             $_url .= $url;
2806:         }
2807:         else
2808:         {
2809:             $_url = $url;
2810:         }
2811: 
2812:         $this->last_selector = null;
2813: 
2814:         if ($view !== false)
2815:         {
2816:             return $this->reset_response()->cmd(8, array(
2817:                 $_url,
2818:                 $view
2819:             ));
2820:         }
2821:         else
2822:         {
2823:             return $this->cmd(8, array(
2824:                 $_url,
2825:                 false
2826:             ));
2827:         }
2828:     }
2829: 
2830:     /**
2831:      * Prepend string/HTML to target(s)
2832:      *
2833:      * @param string $content  Content to be prepended to the selected element
2834:      * @param string $selector [optional] Optional jquery selector string
2835:      *
2836:      * @return PheryResponse
2837:      */
2838:     public function prepend($content, $selector = null)
2839:     {
2840:         if (is_array($content))
2841:         {
2842:             $content = join("\n", $content);
2843:         }
2844: 
2845:         return $this->cmd('prepend', array(
2846:             $this->typecast($content, true, true)
2847:         ), $selector);
2848:     }
2849: 
2850:     /**
2851:      * Clear all the selectors and commands in the current response.
2852:      * @return PheryResponse
2853:      */
2854:     public function reset_response()
2855:     {
2856:         $this->data = array();
2857:         $this->last_selector = null;
2858:         $this->merged = array();
2859:         return $this;
2860:     }
2861: 
2862:     /**
2863:      * Append string/HTML to target(s)
2864:      *
2865:      * @param string $content  Content to be appended to the selected element
2866:      * @param string $selector [optional] Optional jquery selector string
2867:      *
2868:      * @return PheryResponse
2869:      */
2870:     public function append($content, $selector = null)
2871:     {
2872:         if (is_array($content))
2873:         {
2874:             $content = join("\n", $content);
2875:         }
2876: 
2877:         return $this->cmd('append', array(
2878:             $this->typecast($content, true, true)
2879:         ), $selector);
2880:     }
2881: 
2882:     /**
2883:      * Include a stylesheet in the head of the page
2884:      *
2885:      * @param array $path An array of stylesheets, comprising of 'id' => 'path'
2886:      * @param bool $replace Replace any existing ids
2887:      * @return PheryResponse
2888:      */
2889:     public function include_stylesheet(array $path, $replace = false)
2890:     {
2891:         $this->last_selector = null;
2892: 
2893:         return $this->cmd(10, array(
2894:             'c',
2895:             $path,
2896:             $replace
2897:         ));
2898:     }
2899: 
2900:     /**
2901:      * Include a script in the head of the page
2902:      *
2903:      * @param array $path An array of scripts, comprising of 'id' => 'path'
2904:      * @param bool $replace Replace any existing ids
2905:      * @return PheryResponse
2906:      */
2907:     public function include_script($path, $replace = false)
2908:     {
2909:         $this->last_selector = null;
2910: 
2911:         return $this->cmd(10, array(
2912:             'j',
2913:             $path,
2914:             $replace
2915:         ));
2916:     }
2917: 
2918:     /**
2919:      * Magically map to any additional jQuery function.
2920:      * To reach this magically called functions, the jquery() selector must be called prior
2921:      * to any jquery specific call
2922:      *
2923:      * @param string $name
2924:      * @param array $arguments
2925:      *
2926:      * @see jquery()
2927:      * @see j()
2928:      * @return PheryResponse
2929:      */
2930:     public function __call($name, $arguments)
2931:     {
2932:         if ($this->last_selector)
2933:         {
2934:             if (count($arguments))
2935:             {
2936:                 foreach ($arguments as $_name => $argument)
2937:                 {
2938:                     $arguments[$_name] = $this->typecast($argument, true, true);
2939:                 }
2940: 
2941:                 $this->cmd($name, $arguments);
2942:             }
2943:             else
2944:             {
2945:                 $this->cmd($name);
2946:             }
2947: 
2948:         }
2949: 
2950:         return $this;
2951:     }
2952: 
2953:     /**
2954:      * Magic functions
2955:      *
2956:      * @param string $name
2957:      * @return PheryResponse
2958:      */
2959:     function __get($name)
2960:     {
2961:         $name = strtolower($name);
2962: 
2963:         if ($name === 'this')
2964:         {
2965:             $this->set_internal_counter('~');
2966:         }
2967:         elseif ($name === 'document')
2968:         {
2969:             $this->jquery('document');
2970:         }
2971:         elseif ($name === 'window')
2972:         {
2973:             $this->jquery('window');
2974:         }
2975:         elseif ($name === 'jquery')
2976:         {
2977:             $this->set_internal_counter('#');
2978:         }
2979:         else
2980:         {
2981:             $this->access($name);
2982:         }
2983: 
2984:         return $this;
2985:     }
2986: 
2987:     /**
2988:      * Convert, to a maximum depth, nested responses, and typecast int properly
2989:      *
2990:      * @param mixed $argument The value
2991:      * @param bool $toString Call class __toString() if possible, and typecast int correctly
2992:      * @param bool $nested Should it look for nested arrays and classes?
2993:      * @param int $depth Max depth
2994:      * @return mixed
2995:      */
2996:     protected function typecast($argument, $toString = true, $nested = false, $depth = 4)
2997:     {
2998:         if ($nested)
2999:         {
3000:             $depth--;
3001:             if ($argument instanceof PheryResponse)
3002:             {
3003:                 $argument = array('PR' => $argument->process_merged());
3004:             }
3005:             elseif ($argument instanceof PheryFunction)
3006:             {
3007:                 $argument = array('PF' => $argument->compile());
3008:             }
3009:             elseif ($depth > 0 && is_array($argument))
3010:             {
3011:                 foreach ($argument as $name => $arg) {
3012:                     $argument[$name] = $this->typecast($arg, $toString, $nested, $depth);
3013:                 }
3014:             }
3015:         }
3016: 
3017:         if ($toString && !empty($argument))
3018:         {
3019:             if (is_string($argument) && ctype_digit($argument))
3020:             {
3021:                 if ($this->config['convert_integers'] === true)
3022:                 {
3023:                     $argument = (int)$argument;
3024:                 }
3025:             }
3026:             elseif (is_object($argument) && $this->config['typecast_objects'] === true)
3027:             {
3028:                 $class = get_class($argument);
3029:                 if ($class !== false)
3030:                 {
3031:                     $rc = new ReflectionClass(get_class($argument));
3032:                     if ($rc->hasMethod('__toString'))
3033:                     {
3034:                         $argument = "{$argument}";
3035:                     }
3036:                     else
3037:                     {
3038:                         $argument = json_decode(json_encode($argument), true);
3039:                     }
3040:                 }
3041:                 else
3042:                 {
3043:                     $argument = json_decode(json_encode($argument), true);
3044:                 }
3045:             }
3046:         }
3047: 
3048:         return $argument;
3049:     }
3050: 
3051:     /**
3052:      * Process merged responses
3053:      * @return array
3054:      */
3055:     protected function process_merged()
3056:     {
3057:         $data = $this->data;
3058: 
3059:         if (empty($data) && $this->last_selector !== null && !$this->is_special_selector('#'))
3060:         {
3061:             $data[$this->last_selector] = array();
3062:         }
3063: 
3064:         foreach ($this->merged as $r)
3065:         {
3066:             foreach ($r as $selector => $response)
3067:             {
3068:                 if (!ctype_digit($selector))
3069:                 {
3070:                     if (isset($data[$selector]))
3071:                     {
3072:                         $data[$selector] = array_merge_recursive($data[$selector], $response);
3073:                     }
3074:                     else
3075:                     {
3076:                         $data[$selector] = $response;
3077:                     }
3078:                 }
3079:                 else
3080:                 {
3081:                     $selector = (int)$selector;
3082:                     while (isset($data['0'.$selector]))
3083:                     {
3084:                         $selector++;
3085:                     }
3086:                     $data['0'.$selector] = $response;
3087:                 }
3088:             }
3089:         }
3090: 
3091:         return $data;
3092:     }
3093: 
3094:     /**
3095:      * Return the JSON encoded data
3096:      * @return string
3097:      */
3098:     public function render()
3099:     {
3100:         return json_encode((object)$this->process_merged());
3101:     }
3102: 
3103:     /**
3104:      * Output the current answer as a load directive, as a ready-to-use string
3105:      *
3106:      * <code>
3107:      *
3108:      * </code>
3109:      *
3110:      * @param bool $echo Automatically echo the javascript instead of returning it
3111:      * @return string
3112:      */
3113:     public function inline_load($echo = false)
3114:     {
3115:         $body = addcslashes($this->render(), "\\'");
3116: 
3117:         $javascript = "phery.load('{$body}');";
3118: 
3119:         if ($echo)
3120:         {
3121:             echo $javascript;
3122:         }
3123: 
3124:         return $javascript;
3125:     }
3126: 
3127:     /**
3128:      * Return the JSON encoded data
3129:      * if the object is typecasted as a string
3130:      * @return string
3131:      */
3132:     public function __toString()
3133:     {
3134:         return $this->render();
3135:     }
3136: 
3137:     /**
3138:      * Initialize the instance from a serialized state
3139:      *
3140:      * @param string $serialized
3141:      * @throws PheryException
3142:      * @return PheryResponse
3143:      */
3144:     public function unserialize($serialized)
3145:     {
3146:         $obj = json_decode($serialized, true);
3147:         if ($obj && is_array($obj) && json_last_error() === JSON_ERROR_NONE)
3148:         {
3149:             $this->exchangeArray($obj['this']);
3150:             $this->data = (array)$obj['data'];
3151:             $this->set_response_name((string)$obj['name']);
3152:             $this->merged = (array)$obj['merged'];
3153:         }
3154:         else
3155:         {
3156:             throw new PheryException('Invalid data passed to unserialize');
3157:         }
3158:         return $this;
3159:     }
3160: 
3161:     /**
3162:      * Serialize the response in JSON
3163:      * @return string|bool
3164:      */
3165:     public function serialize()
3166:     {
3167:         return json_encode(array(
3168:             'data' => $this->data,
3169:             'this' => $this->getArrayCopy(),
3170:             'name' => $this->name,
3171:             'merged' => $this->merged,
3172:         ));
3173:     }
3174: 
3175:     /**
3176:      * Determine if the last selector or the selector provided is an special
3177:      *
3178:      * @param string $type
3179:      * @param string $selector
3180:      * @return boolean
3181:      */
3182:     protected function is_special_selector($type = null, $selector = null)
3183:     {
3184:         $selector = Phery::coalesce($selector, $this->last_selector);
3185: 
3186:         if ($selector && preg_match('/\{([\D]+)\d+\}/', $selector, $matches))
3187:         {
3188:             if ($type === null)
3189:             {
3190:                 return true;
3191:             }
3192: 
3193:             return ($matches[1] === $type);
3194:         }
3195: 
3196:         return false;
3197:     }
3198: }
3199: 
3200: /**
3201:  * Create an anonymous function for use on Javascript callbacks
3202:  * @package    Phery
3203:  */
3204: class PheryFunction {
3205: 
3206:     /**
3207:      * Parameters that will be replaced inside the response
3208:      * @var array
3209:      */
3210:     protected $parameters = array();
3211:     /**
3212:      * The function string itself
3213:      * @var array
3214:      */
3215:     protected $value = null;
3216: 
3217:     /**
3218:      * Sets new raw parameter to be passed, that will be eval'ed.
3219:      * If you don't pass the function(){ } it will be appended
3220:      *
3221:      * <code>
3222:      * $raw = new PheryFunction('function($val){ return $val; }');
3223:      * // or
3224:      * $raw = new PheryFunction('alert("done");'); // turns into function(){ alert("done"); }
3225:      * </code>
3226:      *
3227:      * @param   string|array  $value      Raw function string. If you pass an array,
3228:      *                                    it will be joined with a line feed \n
3229:      * @param   array         $parameters You can pass parameters that will be replaced
3230:      *                                    in the $value when compiling
3231:      */
3232:     public function __construct($value, $parameters = array())
3233:     {
3234:         if (!empty($value))
3235:         {
3236:             // Set the expression string
3237:             if (is_array($value))
3238:             {
3239:                 $this->value = join("\n", $value);
3240:             }
3241:             elseif (is_string($value))
3242:             {
3243:                 $this->value = $value;
3244:             }
3245: 
3246:             if (!preg_match('/^\s*function/im', $this->value))
3247:             {
3248:                 $this->value = 'function(){' . $this->value . '}';
3249:             }
3250: 
3251:             $this->parameters = $parameters;
3252:         }
3253:     }
3254: 
3255:     /**
3256:      * Bind a variable to a parameter.
3257:      *
3258:      * @param   string  $param  parameter key to replace
3259:      * @param   mixed   $var    variable to use
3260:      * @return  PheryFunction
3261:      */
3262:     public function bind($param, & $var)
3263:     {
3264:         $this->parameters[$param] =& $var;
3265: 
3266:         return $this;
3267:     }
3268: 
3269:     /**
3270:      * Set the value of a parameter.
3271:      *
3272:      * @param   string  $param  parameter key to replace
3273:      * @param   mixed   $value  value to use
3274:      * @return  PheryFunction
3275:      */
3276:     public function param($param, $value)
3277:     {
3278:         $this->parameters[$param] = $value;
3279: 
3280:         return $this;
3281:     }
3282: 
3283:     /**
3284:      * Add multiple parameter values.
3285:      *
3286:      * @param   array   $params list of parameter values
3287:      * @return  PheryFunction
3288:      */
3289:     public function parameters(array $params)
3290:     {
3291:         $this->parameters = $params + $this->parameters;
3292: 
3293:         return $this;
3294:     }
3295: 
3296:     /**
3297:      * Get the value as a string.
3298:      *
3299:      * @return  string
3300:      */
3301:     public function value()
3302:     {
3303:         return (string) $this->value;
3304:     }
3305: 
3306:     /**
3307:      * Return the value of the expression as a string.
3308:      *
3309:      * <code>
3310:      *     echo $expression;
3311:      * </code>
3312:      *
3313:      * @return  string
3314:      */
3315:     public function __toString()
3316:     {
3317:         return $this->value();
3318:     }
3319: 
3320:     /**
3321:      * Compile function and return it. Replaces any parameters with
3322:      * their given values.
3323:      *
3324:      * @return  string
3325:      */
3326:     public function compile()
3327:     {
3328:         $value = $this->value();
3329: 
3330:         if ( ! empty($this->parameters))
3331:         {
3332:             $params = $this->parameters;
3333:             $value = strtr($value, $params);
3334:         }
3335: 
3336:         return $value;
3337:     }
3338: 
3339:     /**
3340:      * Static instantation for PheryFunction
3341:      *
3342:      * @param string|array $value
3343:      * @param array        $parameters
3344:      *
3345:      * @return PheryFunction
3346:      */
3347:     public static function factory($value, $parameters = array())
3348:     {
3349:         return new PheryFunction($value, $parameters);
3350:     }
3351: }
3352: 
3353: /**
3354:  * Exception class for Phery specific exceptions
3355:  * @package    Phery
3356:  */
3357: class PheryException extends Exception {
3358: 
3359: }
3360: 
Phery API documentation generated by ApiGen 2.8.0