Overview

Namespaces

  • None
  • Wei
    • Validator

Classes

  • Wei\Validator\All
  • Wei\Validator\AllOf
  • Wei\Validator\Alnum
  • Wei\Validator\Alpha
  • Wei\Validator\BaseValidator
  • Wei\Validator\Between
  • Wei\Validator\Blank
  • Wei\Validator\Callback
  • Wei\Validator\CharLength
  • Wei\Validator\Chinese
  • Wei\Validator\Color
  • Wei\Validator\Contains
  • Wei\Validator\CreditCard
  • Wei\Validator\Date
  • Wei\Validator\DateTime
  • Wei\Validator\Decimal
  • Wei\Validator\Digit
  • Wei\Validator\Dir
  • Wei\Validator\DivisibleBy
  • Wei\Validator\DoubleByte
  • Wei\Validator\Email
  • Wei\Validator\EndsWith
  • Wei\Validator\EqualTo
  • Wei\Validator\Exists
  • Wei\Validator\FieldExists
  • Wei\Validator\File
  • Wei\Validator\GreaterThan
  • Wei\Validator\GreaterThanOrEqual
  • Wei\Validator\IdCardCn
  • Wei\Validator\IdCardHk
  • Wei\Validator\IdCardMo
  • Wei\Validator\IdCardTw
  • Wei\Validator\IdenticalTo
  • Wei\Validator\Image
  • Wei\Validator\In
  • Wei\Validator\Ip
  • Wei\Validator\Length
  • Wei\Validator\LessThan
  • Wei\Validator\LessThanOrEqual
  • Wei\Validator\Lowercase
  • Wei\Validator\Luhn
  • Wei\Validator\MaxLength
  • Wei\Validator\MinLength
  • Wei\Validator\MobileCn
  • Wei\Validator\NaturalNumber
  • Wei\Validator\NoneOf
  • Wei\Validator\Null
  • Wei\Validator\Number
  • Wei\Validator\OneOf
  • Wei\Validator\Password
  • Wei\Validator\Phone
  • Wei\Validator\PhoneCn
  • Wei\Validator\PlateNumberCn
  • Wei\Validator\PositiveInteger
  • Wei\Validator\PostcodeCn
  • Wei\Validator\Present
  • Wei\Validator\QQ
  • Wei\Validator\RecordExists
  • Wei\Validator\Regex
  • Wei\Validator\Required
  • Wei\Validator\SomeOf
  • Wei\Validator\StartsWith
  • Wei\Validator\Time
  • Wei\Validator\Tld
  • Wei\Validator\Type
  • Wei\Validator\Uppercase
  • Wei\Validator\Url
  • Wei\Validator\Uuid
  • Overview
  • Namespace
  • Function
  1: <?php
  2: /**
  3:  * Wei Framework
  4:  *
  5:  * @copyright   Copyright (c) 2008-2015 Twin Huang
  6:  * @license     http://opensource.org/licenses/mit-license.php MIT License
  7:  */
  8: 
  9: namespace Wei;
 10: 
 11: use Closure;
 12: use SimpleXMLElement;
 13: 
 14: /**
 15:  * A service handles WeChat(WeiXin) callback message
 16:  *
 17:  * @author      Twin Huang <twinhuang@qq.com>
 18:  * @link        http://mp.weixin.qq.com/wiki/index.php?title=%E6%B6%88%E6%81%AF%E6%8E%A5%E5%8F%A3%E6%8C%87%E5%8D%97
 19:  */
 20: class WeChatApp extends Base
 21: {
 22:     /**
 23:      * The WeChat token to generate signature
 24:      *
 25:      * @var string
 26:      */
 27:     protected $token = 'wei';
 28: 
 29:     /**
 30:      * The HTTP raw post data, equals to $GLOBALS['HTTP_RAW_POST_DATA'] on default
 31:      *
 32:      * @var string
 33:      */
 34:     protected $postData;
 35: 
 36:     /**
 37:      * The URL query parameters, equals to $_GET on default
 38:      *
 39:      * @var array
 40:      */
 41:     protected $query;
 42: 
 43:     /**
 44:      * The rules to generate output message
 45:      *
 46:      * @var array
 47:      */
 48:     protected $rules = array(
 49:         'text'      => array(),
 50:         'event'     => array(),
 51:         'image'     => null,
 52:         'location'  => null,
 53:         'voice'     => null,
 54:         'video'     => null,
 55:         'link'      => null
 56:     );
 57: 
 58:     /**
 59:      * A handler executes when none of rules handled the input
 60:      *
 61:      * @var callable
 62:      */
 63:     protected $defaults;
 64: 
 65:     /**
 66:      * Whether the signature is valid
 67:      *
 68:      * @var bool
 69:      */
 70:     protected $valid = false;
 71: 
 72:     /**
 73:      * Are there any callbacks handled the message ?
 74:      *
 75:      * @var bool
 76:      */
 77:     protected $handled = false;
 78: 
 79:     /**
 80:      * The callback executes before send the XML data
 81:      *
 82:      * @var callable
 83:      */
 84:     protected $beforeSend;
 85: 
 86:     /**
 87:      * The element values of post XML data
 88:      *
 89:      * Most of the available element names in post XML data
 90:      * common  : MsgType, FromUserName, ToUserName, MsgId, CreateTime, Ticket
 91:      * text    : Content
 92:      * image   : PicUrl
 93:      * location: Location_X, Location_Y, Scale, Label
 94:      * voice   : MediaId, Format
 95:      * event   : Event, EventKey
 96:      * video   : MediaId, ThumbMediaId
 97:      * link    : Title, Description
 98:      *
 99:      * @var array
100:      */
101:     protected $attrs = array();
102: 
103:     /**
104:      * Constructor
105:      *
106:      * @param array $options
107:      * @global string $GLOBALS['HTTP_RAW_POST_DATA']
108:      */
109:     public function __construct($options = array())
110:     {
111:         parent::__construct($options);
112: 
113:         if (!$this->query) {
114:             $this->query = &$_GET;
115:         }
116: 
117:         if (is_null($this->postData) && isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
118:             $this->postData = $GLOBALS['HTTP_RAW_POST_DATA'];
119:         }
120: 
121:         $this->parsePostData();
122:     }
123: 
124:     /**
125:      * Start up WeChat application and output the matched rule message
126:      *
127:      * @return $this
128:      */
129:     public function __invoke()
130:     {
131:         echo $this->run();
132:         return $this;
133:     }
134: 
135:     /**
136:      * Execute the matched rule and returns the rule result
137:      *
138:      * Returns false when the token is invalid or no rules matched
139:      *
140:      * @return string|false
141:      */
142:     public function run()
143:     {
144:         // The token is invalid
145:         if (!$this->valid) {
146:             return false;
147:         }
148: 
149:         // Output 'echostr' for fist time authentication
150:         if (isset($this->query['echostr'])) {
151:             return htmlspecialchars($this->query['echostr'], ENT_QUOTES, 'UTF-8');
152:         }
153: 
154:         switch ($this->getMsgType()) {
155:             case 'text':
156:                 if ($result = $this->handleText()) {
157:                     return $result;
158:                 }
159:                 break;
160: 
161:             case 'event':
162:                 $event = strtolower($this->getEvent());
163:                 switch ($event) {
164:                     case 'subscribe':
165:                         $result = $this->handleEvent('subscribe');
166:                         if ($this->getTicket()) {
167:                             $result = $this->handleEvent('scan');
168:                         }
169:                         if ($result) {
170:                             return $result;
171:                         }
172:                         break;
173: 
174:                     case 'scan':
175:                         if ($result = $this->handleEvent('scan')) {
176:                             return $result;
177:                         }
178:                         break;
179: 
180:                     default:
181:                         if ($result = $this->handleEvent($event, $this->getEventKey())) {
182:                             return $result;
183:                         }
184:                         break;
185:                 }
186:                 break;
187: 
188:             // Including location, image, voice, video and link
189:             default:
190:                 if (isset($this->rules[$this->getMsgType()])) {
191:                     return $this->handle($this->rules[$this->getMsgType()]);
192:                 }
193:         }
194: 
195:         // Fallback to the default rule
196:         if (!$this->handled && $this->defaults) {
197:             return $this->handle($this->defaults);
198:         }
199: 
200:         return false;
201:     }
202: 
203:     /**
204:      * Check if the request is verify the token string
205:      *
206:      * @return bool
207:      */
208:     public function isVerifyToken()
209:     {
210:         return isset($this->query['echostr']);
211:     }
212: 
213:     /**
214:      * Attach a callback which triggered when user subscribed you
215:      *
216:      * @param Closure $fn
217:      * @return $this
218:      */
219:     public function subscribe(Closure $fn)
220:     {
221:         return $this->addEventRule('subscribe', null, $fn);
222:     }
223: 
224:     /**
225:      * Attach a callback which triggered when user unsubscribed you
226:      *
227:      * @param Closure $fn
228:      * @return $this
229:      */
230:     public function unsubscribe(Closure $fn)
231:     {
232:         return $this->addEventRule('unsubscribe', null, $fn);
233:     }
234: 
235:     /**
236:      * Attach a callback which triggered when user click the custom menu
237:      *
238:      * @param string $key The key of event
239:      * @param Closure $fn
240:      * @return $this
241:      */
242:     public function click($key, Closure $fn)
243:     {
244:         return $this->addEventRule('click', $key, $fn);
245:     }
246: 
247:     /**
248:      * Attach a callback which triggered when user scan the QR Code
249:      *
250:      * @param Closure $fn
251:      * @return $this
252:      */
253:     public function scan(Closure $fn)
254:     {
255:         return $this->addEventRule('scan', null, $fn);
256:     }
257: 
258:     /**
259:      * Attach a callback which triggered when user input equals to the keyword
260:      *
261:      * @param string $keyword The keyword to compare with user input
262:      * @param Closure $fn
263:      * @return $this
264:      */
265:     public function is($keyword, Closure $fn)
266:     {
267:         return $this->addTextRule('is', $keyword, $fn);
268:     }
269: 
270:     /**
271:      * Attach a callback with a keyword, which triggered when user input contains the keyword
272:      *
273:      * @param string $keyword The keyword to search in user input
274:      * @param Closure $fn
275:      * @return $this
276:      */
277:     public function has($keyword, Closure $fn)
278:     {
279:         return $this->addTextRule('has', $keyword, $fn);
280:     }
281: 
282:     /**
283:      * Attach a callback with a keyword, which triggered when user input starts with the keyword (case insensitive)
284:      *
285:      * @param string $keyword The keyword to search in user input
286:      * @param Closure $fn
287:      * @return $this
288:      */
289:     public function startsWith($keyword, Closure $fn)
290:     {
291:         return $this->addTextRule('startsWith', $keyword, $fn);
292:     }
293: 
294:     /**
295:      * Attach a callback with a regex pattern which triggered when user input match the pattern
296:      *
297:      * @param string $pattern The pattern to match
298:      * @param Closure $fn
299:      * @return $this
300:      */
301:     public function match($pattern, Closure $fn)
302:     {
303:         return $this->addTextRule('match', $pattern, $fn);
304:     }
305: 
306:     /**
307:      * Attach a callback to handle image message
308:      *
309:      * @param Closure $fn
310:      * @return $this
311:      */
312:     public function receiveImage(Closure $fn)
313:     {
314:         $this->rules['image'] = $fn;
315:         return $this;
316:     }
317: 
318:     /**
319:      * Attach a callback to handle location message
320:      *
321:      * @param Closure $fn
322:      * @return $this
323:      */
324:     public function receiveLocation(Closure $fn)
325:     {
326:         $this->rules['location'] = $fn;
327:         return $this;
328:     }
329: 
330:     /**
331:      * Attach a callback to handle voice message
332:      *
333:      * @param Closure $fn
334:      * @return $this
335:      */
336:     public function receiveVoice(Closure $fn)
337:     {
338:         $this->rules['voice'] = $fn;
339:         return $this;
340:     }
341: 
342:     /**
343:      * Attach a callback to handle video message
344:      *
345:      * @param Closure $fn
346:      * @return $this
347:      */
348:     public function receiveVideo(Closure $fn)
349:     {
350:         $this->rules['video'] = $fn;
351:         return $this;
352:     }
353: 
354:     /**
355:      * Attach a callback to handle link message
356:      *
357:      * @param Closure $fn
358:      * @return $this
359:      */
360:     public function receiveLink(Closure $fn)
361:     {
362:         $this->rules['link'] = $fn;
363:         return $this;
364:     }
365: 
366:     /**
367:      * Attach a handler which executes when none of the rule handled the input
368:      *
369:      * @param Closure $fn
370:      * @return boolean
371:      */
372:     public function defaults(Closure $fn)
373:     {
374:         $this->defaults = $fn;
375:         return $this;
376:     }
377: 
378:     /**
379:      * Generate text message for output
380:      *
381:      * @param string $content
382:      * @return array
383:      */
384:     public function sendText($content)
385:     {
386:         return $this->send('text', array(
387:             'Content' => $content
388:         ));
389:     }
390: 
391:     /**
392:      * Generate music message for output
393:      *
394:      * @param string $title The title of music
395:      * @param string $description The description display blow the title
396:      * @param string $url The music URL for player
397:      * @param string $hqUrl The HQ music URL for player when user in WIFI
398:      * @return array
399:      */
400:     public function sendMusic($title, $description, $url, $hqUrl = null)
401:     {
402:         return $this->send('music', array(
403:             'Music' => array(
404:                 'Title' => $title,
405:                 'Description' => $description,
406:                 'MusicUrl' => $url,
407:                 'HQMusicUrl' => $hqUrl
408:             )
409:         ));
410:     }
411: 
412:     /**
413:      * Generate article message for output
414:      *
415:      * ```
416:      * // Sends one article
417:      * $app->sendArticle(array(
418:      *     'title' => 'The title of article',
419:      *     'description' => 'The description of article',
420:      *     'picUrl' => 'The picture URL of article',
421:      *     'url' => 'The URL link to of article'
422:      * ));
423:      *
424:      * // Sends two or more articles
425:      * $app->sendArticle(array(
426:      *     array(
427:      *         'title' => 'The title of article',
428:      *         'description' => 'The description of article',
429:      *         'picUrl' => 'The picture URL of article',
430:      *         'url' => 'The URL link to of article'
431:      *     ),
432:      *     array(
433:      *         'title' => 'The title of article',
434:      *         'description' => 'The description of article',
435:      *         'picUrl' => 'Te picture URL of article',
436:      *         'url' => 'The URL link to of article'
437:      *     ),
438:      *     // more...
439:      *  ));
440:      * ```
441:      *
442:      * @param array $articles The article array
443:      * @return array
444:      */
445:     public function sendArticle(array $articles)
446:     {
447:         // Convert single article array
448:         if (!is_int(key($articles))) {
449:             $articles = array($articles);
450:         }
451: 
452:         $response = array(
453:             'ArticleCount' => count($articles),
454:             'Articles' => array(
455:                 'item' => array()
456:             )
457:         );
458: 
459:         foreach ($articles as $article) {
460:             $article += array(
461:                 'title' => null,
462:                 'description' => null,
463:                 'picUrl' => null,
464:                 'url' => null
465:             );
466:             $response['Articles']['item'][] = array(
467:                 'Title' => $article['title'],
468:                 'Description' => $article['description'],
469:                 'PicUrl' => $article['picUrl'],
470:                 'Url' => $article['url']
471:             );
472:         }
473: 
474:         return $this->send('news', $response);
475:     }
476: 
477:     /**
478:      * Returns if the token is valid
479:      *
480:      * @return bool
481:      */
482:     public function isValid()
483:     {
484:         return $this->valid;
485:     }
486: 
487:     /**
488:      * Returns the XML element value
489:      *
490:      * @param string $name
491:      * @return mixed
492:      */
493:     public function getAttr($name)
494:     {
495:         return isset($this->attrs[$name]) ? $this->attrs[$name] : null;
496:     }
497: 
498:     /**
499:      * Returns all of XML element values
500:      *
501:      * @return array
502:      */
503:     public function getAttrs()
504:     {
505:         return $this->attrs;
506:     }
507: 
508:     /**
509:      * Returns the HTTP raw post data
510:      *
511:      * @return string
512:      */
513:     public function getPostData()
514:     {
515:         return $this->postData;
516:     }
517: 
518:     /**
519:      * Returns your user id
520:      *
521:      * @return string
522:      */
523:     public function getToUserName()
524:     {
525:         return $this->getAttr('ToUserName');
526:     }
527: 
528:     /**
529:      * Returns the user openID who sent message to you
530:      *
531:      * @return string
532:      */
533:     public function getFromUserName()
534:     {
535:         return $this->getAttr('FromUserName');
536:     }
537: 
538:     /**
539:      * Returns the timestamp when message created
540:      *
541:      * @return string
542:      */
543:     public function getCreateTime()
544:     {
545:         return $this->getAttr('CreateTime');
546:     }
547: 
548:     /**
549:      * Returns the user input string, available when the message type is text
550:      *
551:      * @return string
552:      */
553:     public function getContent()
554:     {
555:         return $this->getAttr('Content');
556:     }
557: 
558:     /**
559:      * Returns the message id
560:      *
561:      * @return string
562:      */
563:     public function getMsgId()
564:     {
565:         return $this->getAttr('MsgId');
566:     }
567: 
568:     /**
569:      * Returns the message type
570:      *
571:      * Currently could be text, image, location, link, event, voice, video
572:      *
573:      * @return string
574:      */
575:     public function getMsgType()
576:     {
577:         return $this->getAttr('MsgType');
578:     }
579: 
580:     /**
581:      * Returns the picture URL, available when the message type is image
582:      *
583:      * @return string
584:      */
585:     public function getPicUrl()
586:     {
587:         return $this->getAttr('PicUrl');
588:     }
589: 
590:     /**
591:      * Returns the latitude of location, available when the message type is location
592:      *
593:      * @return string
594:      */
595:     public function getLocationX()
596:     {
597:         return $this->getAttr('Location_X');
598:     }
599: 
600:     /**
601:      * Returns the longitude of location, available when the message type is location
602:      *
603:      * @return string
604:      */
605:     public function getLocationY()
606:     {
607:         return $this->getAttr('Location_Y');
608:     }
609: 
610:     /**
611:      * Returns the detail address of location, available when the message type is location
612:      *
613:      * @return string
614:      */
615:     public function getLabel()
616:     {
617:         return $this->getAttr('Label');
618:     }
619: 
620:     /**
621:      * Returns the scale of map, available when the message type is location
622:      *
623:      * @return string
624:      */
625:     public function getScale()
626:     {
627:         return $this->getAttr('Scale');
628:     }
629: 
630:     /**
631:      * Returns the media id, available when the message type is voice or video
632:      *
633:      * @return string
634:      */
635:     public function getMediaId()
636:     {
637:         return $this->getAttr('MediaId');
638:     }
639: 
640:     /**
641:      * Returns the media format, available when the message type is voice
642:      *
643:      * @return string
644:      */
645:     public function getFormat()
646:     {
647:         return $this->getAttr('Format');
648:     }
649: 
650:     /**
651:      * Returns the type of event, could be subscribe, unsubscribe or CLICK, available when the message type is event
652:      *
653:      * @return string
654:      */
655:     public function getEvent()
656:     {
657:         return $this->getAttr('Event');
658:     }
659: 
660:     /**
661:      * Returns the key value of custom menu, available when the message type is event
662:      *
663:      * @return string
664:      */
665:     public function getEventKey()
666:     {
667:         return $this->getAttr('EventKey');
668:     }
669: 
670:     /**
671:      * Returns the scene id from the scan result, available when the message event is subscribe or scan
672:      *
673:      * @return string
674:      */
675:     public function getScanSceneId()
676:     {
677:         $eventKey = $this->getEventKey();
678:         if (strpos($eventKey, 'qrscene_') === 0) {
679:             $eventKey = substr($eventKey, 8);
680:         }
681:         return $eventKey;
682:     }
683: 
684:     /**
685:      * Returns the thumbnail id of video, available when the message type is video
686:      *
687:      * @return string
688:      */
689:     public function getThumbMediaId()
690:     {
691:         return $this->getAttr('ThumbMediaId');
692:     }
693: 
694:     /**
695:      * Returns the title of URL, available when the message type is link
696:      *
697:      * @return string
698:      */
699:     public function getTitle()
700:     {
701:         return $this->getAttr('Title');
702:     }
703: 
704:     /**
705:      * Returns the description of URL, available when the message type is link
706:      *
707:      * @return string
708:      */
709:     public function getDescription()
710:     {
711:         return $this->getAttr('Description');
712:     }
713: 
714:     /**
715:      * Returns the URL link, available when the message type is link
716:      *
717:      * @return string
718:      */
719:     public function getUrl()
720:     {
721:         return $this->getAttr('Url');
722:     }
723: 
724:     /**
725:      * Returns the ticket string, available when user scan from the QR Code
726:      *
727:      * @return string
728:      */
729:     public function getTicket()
730:     {
731:         return $this->getAttr('Ticket');
732:     }
733: 
734:     /**
735:      * Returns the user inputted content or clicked button value
736:      *
737:      * @return bool|string
738:      */
739:     public function getKeyword()
740:     {
741:         if ($this->getMsgType() == 'text') {
742:             return strtolower($this->getContent());
743:         } elseif ($this->getMsgType() == 'event' && strtolower($this->getEvent()) == 'click') {
744:             return strtolower($this->getEventKey());
745:         }
746:         return false;
747:     }
748: 
749:     /**
750:      * Generate message for output
751:      *
752:      * @param string $type The type of message
753:      * @param array $response The response content
754:      * @return array
755:      */
756:     protected function send($type, array $response)
757:     {
758:         return $response + array(
759:             'ToUserName' => $this->getFromUserName(),
760:             'FromUserName' => $this->getToUserName(),
761:             'MsgType' => $type,
762:             'CreateTime' => time()
763:         );
764:     }
765: 
766:     /**
767:      * Adds a rule to handle user text input
768:      *
769:      * @param string $type
770:      * @param string $keyword
771:      * @param Closure $fn
772:      * @return $this
773:      */
774:     protected function addTextRule($type, $keyword, Closure $fn)
775:     {
776:         $this->rules['text'][] = array(
777:             'type' => $type,
778:             'keyword' => $keyword,
779:             'fn' => $fn
780:         );
781:         return $this;
782:     }
783: 
784:     /**
785:      * Adds a rule to handle user event, such as click, subscribe
786:      *
787:      * @param string $name
788:      * @param string $key
789:      * @param Closure $fn
790:      * @return $this
791:      */
792:     protected function addEventRule($name, $key, Closure $fn)
793:     {
794:         $this->rules['event'][$name][$key] = $fn;
795:         return $this;
796:     }
797: 
798:     /**
799:      * Parse post data to receive user OpenID and input content and message attr
800:      */
801:     protected function parsePostData()
802:     {
803:         // Check if the WeChat server signature is valid
804:         $query = $this->query;
805:         $tmpArr = array(
806:             $this->token,
807:             isset($query['timestamp']) ? $query['timestamp'] : '',
808:             isset($query['nonce']) ? $query['nonce'] : ''
809:         );
810:         sort($tmpArr, SORT_STRING);
811:         $tmpStr = sha1(implode($tmpArr));
812:         $this->valid = (isset($query['signature']) && $tmpStr === $query['signature']);
813: 
814:         // Parse the message data
815:         if ($this->valid && $this->postData) {
816:             // Do not output libxml error messages to screen
817:             $useErrors = libxml_use_internal_errors(true);
818:             $attrs = simplexml_load_string($this->postData, 'SimpleXMLElement', LIBXML_NOCDATA);
819:             libxml_use_internal_errors($useErrors);
820: 
821:             // Fix the issue that XML parse empty data to new SimpleXMLElement object
822:             $this->attrs = array_map('strval', (array)$attrs);
823:         }
824:     }
825: 
826:     /**
827:      * Handle text rule
828:      *
829:      * @return string|false
830:      */
831:     protected function handleText()
832:     {
833:         $content = $this->getContent();
834:         foreach ($this->rules['text'] as $rule) {
835:             if ($rule['type'] == 'is' && 0 === strcasecmp($content, $rule['keyword'])) {
836:                 return $this->handle($rule['fn']);
837:             }
838: 
839:             if ($rule['type'] == 'has' && false !== mb_stripos($content, $rule['keyword'])) {
840:                 return $this->handle($rule['fn']);
841:             }
842: 
843:             if ($rule['type'] == 'startsWith' && 0 === mb_stripos($content, $rule['keyword'])) {
844:                 return $this->handle($rule['fn']);
845:             }
846: 
847:             if ($rule['type'] == 'match' && preg_match($rule['keyword'], $content)) {
848:                 return $this->handle($rule['fn']);
849:             }
850:         }
851:         return false;
852:     }
853: 
854:     protected function handleEvent($event, $eventKey = false)
855:     {
856:         if ($eventKey !== false) {
857:             if (isset($this->rules['event'][$event][$eventKey])) {
858:                 return $this->handle($this->rules['event'][$event][$eventKey]);
859:             }
860:         } else {
861:             if (isset($this->rules['event'][$event])) {
862:                 return $this->handle(end($this->rules['event'][$event]));
863:             }
864:         }
865:         return false;
866:     }
867: 
868:     /**
869:      * Executes callback handler
870:      *
871:      * @param Closure $fn
872:      * @return string
873:      */
874:     protected function handle($fn)
875:     {
876:         $this->handled = true;
877: 
878:         // Converts string to array
879:         $content = $fn($this, $this->wei) ?: array();
880:         if ($content && !is_array($content)) {
881:             $content = $this->sendText($content);
882:         }
883: 
884:         $this->beforeSend && call_user_func_array($this->beforeSend, array($this, &$content, $this->wei));
885: 
886:         // Returns empty string if no response
887:         // http://mp.weixin.qq.com/wiki/index.php?title=%E5%8F%91%E9%80%81%E8%A2%AB%E5%8A%A8%E5%93%8D%E5%BA%94%E6%B6%88%E6%81%AF
888:         return $content ? $this->arrayToXml($content)->asXML() : '';
889:     }
890: 
891:     /**
892:      * Convert to XML element
893:      *
894:      * @param array $array
895:      * @param SimpleXMLElement $xml
896:      * @return SimpleXMLElement
897:      */
898:     protected function arrayToXml(array $array, SimpleXMLElement $xml = null)
899:     {
900:         if ($xml === null) {
901:             $xml = new SimpleXMLElement('<xml/>');
902:         }
903:         foreach($array as $key => $value) {
904:             if(is_array($value)) {
905:                 if (isset($value[0])) {
906:                     foreach ($value as $subValue) {
907:                         $subNode = $xml->addChild($key);
908:                         $this->arrayToXml($subValue, $subNode);
909:                     }
910:                 } else {
911:                     $subNode = $xml->addChild($key);
912:                     $this->arrayToXml($value, $subNode);
913:                 }
914:             } else {
915:                 // Wrap cdata for non-numeric string
916:                 if (is_numeric($value)) {
917:                     $xml->addChild($key, $value);
918:                 } else {
919:                     $child = $xml->addChild($key);
920:                     $node = dom_import_simplexml($child);
921:                     $node->appendChild($node->ownerDocument->createCDATASection($value));
922:                 }
923:             }
924:         }
925:         return $xml;
926:     }
927: }
928: 
Wei Framework API documentation generated by ApiGen