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