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