Completed
Push — master ( 9cf391...4a311c )
by lan tian
8s
created

Sms::hasAgent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Toplan\PhpSms;
4
5
use SuperClosure\Serializer;
6
use Toplan\TaskBalance\Balancer;
7
use Toplan\TaskBalance\Task;
8
9
/**
10
 * Class Sms
11
 *
12
 * @author toplan<[email protected]>
13
 */
14
class Sms
15
{
16
    const TASK_NAME = 'PhpSms';
17
    const TYPE_SMS = 1;
18
    const TYPE_VOICE = 2;
19
20
    /**
21
     * The instances of Agent.
22
     *
23
     * @var array
24
     */
25
    protected static $agents = [];
26
27
    /**
28
     * The dispatch scheme of agent,
29
     * and these agents are available.
30
     * example:
31
     * [
32
     *   'Agent1' => '10 backup',
33
     *   'Agent2' => '20 backup',
34
     * ]
35
     *
36
     * @var array
37
     */
38
    protected static $scheme = [];
39
40
    /**
41
     * The configuration information of agents.
42
     *
43
     * @var array
44
     */
45
    protected static $agentsConfig = [];
46
47
    /**
48
     * Whether to use the queue.
49
     *
50
     * @var bool
51
     */
52
    protected static $enableQueue = false;
53
54
    /**
55
     * How to use the queue.
56
     *
57
     * @var \Closure
58
     */
59
    protected static $howToUseQueue = null;
60
61
    /**
62
     * The available hooks for balancing task.
63
     *
64
     * @var array
65
     */
66
    protected static $availableHooks = [
67
        'beforeRun',
68
        'beforeDriverRun',
69
        'afterDriverRun',
70
        'afterRun',
71
    ];
72
73
    /**
74
     * An instance of class [SuperClosure\Serializer] use for serialization closures.
75
     *
76
     * @var Serializer
77
     */
78
    protected static $serializer = null;
79
80
    /**
81
     * The data container of SMS/voice verify.
82
     *
83
     * @var array
84
     */
85
    protected $smsData = [
86
        'type'         => self::TYPE_SMS,
87
        'to'           => null,
88
        'templates'    => [],
89
        'templateData' => [],
90
        'content'      => null,
91
        'voiceCode'    => null,
92
    ];
93
94
    /**
95
     * The name of first agent.
96
     *
97
     * @var string|null
98
     */
99
    protected $firstAgent = null;
100
101
    /**
102
     * Whether the current instance has already pushed to the queue system.
103
     *
104
     * @var bool
105
     */
106
    protected $pushedToQueue = false;
107
108
    /**
109
     * Status container,
110
     * store some configuration information before serialize current instance(before enqueue).
111
     *
112
     * @var array
113
     */
114
    protected $_status_before_enqueue_ = [];
115
116
    /**
117
     * Constructor
118
     *
119
     * @param bool $autoBoot
120
     */
121 6
    public function __construct($autoBoot = true)
122
    {
123 6
        if ($autoBoot) {
124 3
            self::bootstrap();
125 2
        }
126 6
    }
127
128
    /**
129
     * Boot balancing task for send SMS/voice verify.
130
     *
131
     * Note: 判断drivers是否为空不能用'empty',因为在TaskBalance库的中Task类的drivers属性是受保护的(不可访问),
132
     * 虽然通过魔术方法可以获取到其值,但在其目前版本(v0.4.2)其内部却并没有使用'__isset'魔术方法对'empty'或'isset'函数进行逻辑补救.
133
     */
134 6
    public static function bootstrap()
135
    {
136 6
        $task = self::getTask();
137 6
        if (!count($task->drivers)) {
138 3
            self::configuration();
139 3
            self::createDrivers($task);
140 2
        }
141 6
    }
142
143
    /**
144
     * Get or generate a balancing task instance for send SMS/voice verify.
145
     *
146
     * @return Task
147
     */
148 15
    public static function getTask()
149
    {
150 15
        if (!Balancer::hasTask(self::TASK_NAME)) {
151 3
            Balancer::task(self::TASK_NAME);
152 2
        }
153
154 15
        return Balancer::getTask(self::TASK_NAME);
155
    }
156
157
    /**
158
     * Configuration.
159
     */
160 6
    protected static function configuration()
161
    {
162 6
        $config = [];
163 6
        if (!count(self::scheme())) {
164 3
            self::initScheme($config);
165 2
        }
166 6
        $diff = array_diff_key(self::scheme(), self::$agentsConfig);
167 6
        self::initAgentsConfig(array_keys($diff), $config);
168 6
        self::validateConfig();
169 6
    }
170
171
    /**
172
     * Try to read the dispatch scheme of agent from config file.
173
     *
174
     * @param array $config
175
     */
176 3
    protected static function initScheme(array &$config)
177
    {
178 3
        $config = empty($config) ? include __DIR__ . '/../config/phpsms.php' : $config;
179 3
        $scheme = isset($config['scheme']) ? $config['scheme'] : [];
180 3
        self::scheme($scheme);
181 3
    }
182
183
    /**
184
     * Try to initialize the specified agents` configuration information.
185
     *
186
     * @param array $agents
187
     * @param array $config
188
     */
189 6
    protected static function initAgentsConfig(array $agents, array &$config)
190
    {
191 6
        if (empty($agents)) {
192 3
            return;
193
        }
194 3
        $config = empty($config) ? include __DIR__ . '/../config/phpsms.php' : $config;
195 3
        $agentsConfig = isset($config['agents']) ? $config['agents'] : [];
196 3
        foreach ($agents as $name) {
197 3
            $agentConfig = isset($agentsConfig[$name]) ? $agentsConfig[$name] : [];
198 3
            self::config($name, $agentConfig);
199 2
        }
200 3
    }
201
202
    /**
203
     * validate configuration.
204
     *
205
     * @throws PhpSmsException
206
     */
207 6
    protected static function validateConfig()
208
    {
209 6
        if (!count(self::scheme())) {
210
            throw new PhpSmsException('Please configure at least one agent');
211
        }
212 6
    }
213
214
    /**
215
     * Create drivers for the balancing task.
216
     *
217
     * @param Task $task
218
     */
219 18
    protected static function createDrivers(Task $task)
220
    {
221 3
        foreach (self::scheme() as $name => $scheme) {
222
            //解析代理器数组模式的调度配置
223 3
            if (is_array($scheme)) {
224 3
                $data = self::parseScheme($scheme);
225 3
                $scheme = $data['scheme'];
226 2
            }
227
            //创建任务驱动器
228
            $task->driver("$name $scheme")->work(function ($driver) {
229 18
                $agent = self::getAgent($driver->name);
230 18
                $smsData = $driver->getTaskData();
231 18
                extract($smsData);
232 18
                $template = isset($templates[$driver->name]) ? $templates[$driver->name] : 0;
233 18
                if ($type === self::TYPE_VOICE) {
234
                    $agent->voiceVerify($to, $voiceCode, $template, $templateData);
235 18
                } elseif ($type === self::TYPE_SMS) {
236 18
                    $agent->sendSms($to, $content, $template, $templateData);
237 12
                }
238 18
                $result = $agent->result();
239 18
                if ($result['success']) {
240 18
                    $driver->success();
241 12
                }
242 18
                unset($result['success']);
243
244 18
                return $result;
245 3
            });
246 2
        }
247 3
    }
248
249
    /**
250
     * Parsing the dispatch scheme.
251
     * 解析代理器的数组模式的调度配置
252
     *
253
     * @param array $options
254
     *
255
     * @return array
256
     */
257 9
    protected static function parseScheme(array $options)
258
    {
259 9
        $agentClass = Util::pullFromArrayByKey($options, 'agentClass');
260 9
        $sendSms = Util::pullFromArrayByKey($options, 'sendSms');
261 9
        $voiceVerify = Util::pullFromArrayByKey($options, 'voiceVerify');
262 9
        $backup = Util::pullFromArrayByKey($options, 'backup') ? 'backup' : '';
263 9
        $scheme = implode(' ', array_values($options)) . " $backup";
264
265 9
        return compact('agentClass', 'sendSms', 'voiceVerify', 'scheme');
266
    }
267
268
    /**
269
     * Get a sms agent instance by agent name,
270
     * if null, will try to create a new agent instance.
271
     *
272
     * @param string $name
273
     *
274
     * @throws PhpSmsException
275
     *
276
     * @return mixed
277
     */
278 27
    public static function getAgent($name)
279
    {
280 27
        if (!self::hasAgent($name)) {
281 9
            $scheme = self::scheme($name);
282 9
            $data = self::parseScheme(is_array($scheme) ? $scheme : [$scheme]);
283 9
            $data = array_merge(self::config($name), $data);
284 9
            $className = $data['agentClass'] ?: ('Toplan\\PhpSms\\' . $name . 'Agent');
285 9
            if (is_callable($data['sendSms']) || is_callable($data['voiceVerify'])) {
286 3
                self::$agents[$name] = new ParasiticAgent($data);
287 8
            } elseif (class_exists($className)) {
288 6
                self::$agents[$name] = new $className($data);
289 4
            } else {
290
                throw new PhpSmsException("Dont support [$name] agent.");
291
            }
292 6
        }
293
294 27
        return self::$agents[$name];
295
    }
296
297
    /**
298
     * Whether to has specified agent.
299
     *
300
     * @param string $name
301
     *
302
     * @return bool
303
     */
304 36
    public static function hasAgent($name)
305
    {
306 36
        return isset(self::$agents[$name]);
307
    }
308
309
    /**
310
     * Set or get the dispatch scheme of agent by name.
311
     *
312
     * @param mixed $name
313
     * @param mixed $scheme
314
     *
315
     * @return mixed
316
     */
317 18
    public static function scheme($name = null, $scheme = null)
318
    {
319
        return Util::operateArray(self::$scheme, $name, $scheme, null, function ($key, $value) {
320 6
            if (is_string($key)) {
321 3
                self::modifyScheme($key, is_array($value) ? $value : "$value");
322 6
            } elseif (is_int($key)) {
323 6
                self::modifyScheme($value, '');
324 4
            }
325 18
        });
326
    }
327
328
    /**
329
     * Modify the dispatch scheme of agent by name.
330
     *
331
     * @param $key
332
     * @param $value
333
     *
334
     * @throws PhpSmsException
335
     */
336 6
    protected static function modifyScheme($key, $value)
337
    {
338 6
        self::validateAgentName($key);
339 6
        self::$scheme[$key] = $value;
340 6
    }
341
342
    /**
343
     * Set or get configuration information by agent name.
344
     *
345
     * @param mixed $name
346
     * @param mixed $config
347
     * @param bool  $override
348
     *
349
     * @throws PhpSmsException
350
     *
351
     * @return array
352
     */
353 18
    public static function config($name = null, $config = null, $override = false)
354
    {
355 18
        if (is_array($name) && is_bool($config)) {
356 6
            $override = $config;
357 4
        }
358
359
        return Util::operateArray(self::$agentsConfig, $name, $config, [], function ($key, $value) {
360 9
            if (is_array($value)) {
361 9
                self::modifyConfig($key, $value);
362 6
            }
363 18
        }, $override, function (array $origin) {
364 6
            $nameList = array_keys($origin);
365 6
            foreach ($nameList as $name) {
366 6
                if (self::hasAgent("$name")) {
367 4
                    self::getAgent("$name")->config([], true);
368 2
                }
369 4
            }
370 18
        });
371
    }
372
373
    /**
374
     * Modify the configuration information of agent by name.
375
     *
376
     * @param string $key
377
     * @param array  $value
378
     *
379
     * @throws PhpSmsException
380
     */
381 9
    protected static function modifyConfig($key, array $value)
382
    {
383 9
        self::validateAgentName($key);
384 9
        self::$agentsConfig[$key] = $value;
385 9
        if (self::hasAgent($key)) {
386 3
            self::getAgent($key)->config($value);
387 2
        }
388 9
    }
389
390
    /**
391
     * Validate the agent name.
392
     * Agent name must be a string, but not be a number string
393
     *
394
     * @param string $name
395
     *
396
     * @throws PhpSmsException
397
     */
398 12
    protected static function validateAgentName($name)
399
    {
400 12
        if (!$name || !is_string($name) || preg_match('/^[0-9]+$/', $name)) {
401
            throw new PhpSmsException("The agent name [$name] is illegal. Agent name must be a string, but not be a number string.");
402
        }
403 12
    }
404
405
    /**
406
     * Tear down agent use scheme and prepare to create and start a new balancing task,
407
     * so before do it must destroy old task instance.
408
     */
409 6
    public static function cleanScheme()
410
    {
411 6
        Balancer::destroy(self::TASK_NAME);
412 6
        self::$scheme = [];
413 6
    }
414
415
    /**
416
     * Tear down all the configuration information of agent.
417
     */
418 6
    public static function cleanConfig()
419
    {
420 6
        self::config([], true);
421 6
    }
422
423
    /**
424
     * Create a sms instance send SMS,
425
     * your can also set SMS templates or content at the same time.
426
     *
427
     * @param mixed $agentName
428
     * @param mixed $tempId
429
     *
430
     * @return Sms
431
     */
432
    public static function make($agentName = null, $tempId = null)
433
    {
434
        $sms = new self();
435
        $sms->smsData['type'] = self::TYPE_SMS;
436
        if (is_array($agentName)) {
437
            $sms->template($agentName);
438
        } elseif ($agentName && is_string($agentName)) {
439
            if ($tempId === null) {
440
                $sms->content($agentName);
441
            } elseif (is_string($tempId) || is_int($tempId)) {
442
                $sms->template($agentName, "$tempId");
443
            }
444
        }
445
446
        return $sms;
447
    }
448
449
    /**
450
     * Create a sms instance send voice verify,
451
     * your can also set verify code at the same time.
452
     *
453
     * @param int|string|null $code
454
     *
455
     * @return Sms
456
     */
457 3
    public static function voice($code = null)
458
    {
459 3
        $sms = new self();
460 3
        $sms->smsData['type'] = self::TYPE_VOICE;
461 3
        $sms->smsData['voiceCode'] = $code;
462
463 3
        return $sms;
464
    }
465
466
    /**
467
     * Set whether to use the queue system, and define how to use it.
468
     *
469
     * @param mixed $enable
470
     * @param mixed $handler
471
     *
472
     * @return bool
473
     */
474 3
    public static function queue($enable = null, $handler = null)
475
    {
476 3
        if ($enable === null && $handler === null) {
477 3
            return self::$enableQueue;
478
        }
479 3
        if (is_callable($enable)) {
480 3
            $handler = $enable;
481 3
            $enable = true;
482 2
        }
483 3
        self::$enableQueue = (bool) $enable;
484 3
        if (is_callable($handler)) {
485 3
            self::$howToUseQueue = $handler;
0 ignored issues
show
Documentation Bug introduced by
It seems like $handler of type callable is incompatible with the declared type object<Closure> of property $howToUseQueue.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
486 2
        }
487
488 3
        return self::$enableQueue;
489
    }
490
491
    /**
492
     * Set the recipient`s mobile number.
493
     *
494
     * @param string $mobile
495
     *
496
     * @return $this
497
     */
498 6
    public function to($mobile)
499
    {
500 6
        $this->smsData['to'] = $mobile;
501
502 6
        return $this;
503
    }
504
505
    /**
506
     * Set the content for content SMS.
507
     *
508
     * @param string $content
509
     *
510
     * @return $this
511
     */
512 3
    public function content($content)
513
    {
514 3
        $this->smsData['content'] = trim((string) $content);
515
516 3
        return $this;
517
    }
518
519
    /**
520
     * Set the template id for template SMS.
521
     *
522
     * @param mixed $agentName
523
     * @param mixed $tempId
524
     *
525
     * @return $this
526
     */
527 3
    public function template($agentName, $tempId = null)
528
    {
529 3
        Util::operateArray($this->smsData['templates'], $agentName, $tempId);
530
531 3
        return $this;
532
    }
533
534
    /**
535
     * Set the template data for template SMS.
536
     *
537
     * @param array $data
538
     *
539
     * @return $this
540
     */
541 3
    public function data(array $data)
542
    {
543 3
        $this->smsData['templateData'] = $data;
544
545 3
        return $this;
546
    }
547
548
    /**
549
     * Set the first agent by name.
550
     *
551
     * @param string $name
552
     *
553
     * @return $this
554
     */
555 3
    public function agent($name)
556
    {
557 3
        $this->firstAgent = (string) $name;
558
559 3
        return $this;
560
    }
561
562
    /**
563
     * Start send SMS/voice verify.
564
     *
565
     * If give a true parameter, this system will immediately start request to send SMS/voice verify whatever whether to use the queue.
566
     * if you are already pushed sms instance to the queue, you can recall the method `send()` in queue system without `true` parameter,
567
     * so this mechanism in order to make you convenient use the method `send()` in queue system.
568
     *
569
     * @param bool $immediately
570
     *
571
     * @return mixed
572
     */
573 18
    public function send($immediately = false)
574
    {
575 18
        if (!self::$enableQueue || $this->pushedToQueue) {
576 18
            $immediately = true;
577 12
        }
578 18
        if ($immediately) {
579 18
            $result = Balancer::run(self::TASK_NAME, [
580 18
                'data'   => $this->getData(),
581 18
                'driver' => $this->firstAgent,
582 12
            ]);
583 12
        } else {
584 3
            $result = $this->push();
585
        }
586
587 18
        return $result;
588
    }
589
590
    /**
591
     * Push to the queue by a custom method.
592
     *
593
     * @throws \Exception | PhpSmsException
594
     *
595
     * @return mixed
596
     */
597 3
    public function push()
598
    {
599 3
        if (is_callable(self::$howToUseQueue)) {
600
            try {
601 3
                $this->pushedToQueue = true;
602
603 3
                return call_user_func_array(self::$howToUseQueue, [$this, $this->getData()]);
604
            } catch (\Exception $e) {
605
                $this->pushedToQueue = false;
606
                throw $e;
607
            }
608
        } else {
609
            throw new PhpSmsException('Please define how to use queue by method `queue($available, $handler)`');
610
        }
611
    }
612
613
    /**
614
     * Get all the data of SMS/voice verify.
615
     *
616
     * @param null|string $name
617
     *
618
     * @return mixed
619
     */
620 36
    public function getData($name = null)
621
    {
622 36
        if (is_string($name) && isset($this->smsData["$name"])) {
623 3
            return $this->smsData[$name];
624
        }
625
626 36
        return $this->smsData;
627
    }
628
629
    /**
630
     * Overload static method.
631
     *
632
     * @param string $name
633
     * @param array  $args
634
     *
635
     * @throws PhpSmsException
636
     */
637 9
    public static function __callStatic($name, $args)
638
    {
639 9
        $name = $name === 'beforeSend' ? 'beforeRun' : $name;
640 9
        $name = $name === 'afterSend' ? 'afterRun' : $name;
641 9
        $name = $name === 'beforeAgentSend' ? 'beforeDriverRun' : $name;
642 9
        $name = $name === 'afterAgentSend' ? 'afterDriverRun' : $name;
643 9
        if (in_array($name, self::$availableHooks)) {
644 9
            $handler = $args[0];
645 9
            $override = isset($args[1]) ? (bool) $args[1] : false;
646 9
            if (is_callable($handler)) {
647 9
                $task = self::getTask();
648 9
                $task->hook($name, $handler, $override);
649 6
            } else {
650 3
                throw new PhpSmsException("Please give method $name() a callable parameter");
651
            }
652 6
        } else {
653
            throw new PhpSmsException("Dont find method $name()");
654
        }
655 9
    }
656
657
    /**
658
     * Overload method.
659
     *
660
     * @param string $name
661
     * @param array  $args
662
     *
663
     * @throws PhpSmsException
664
     * @throws \Exception
665
     */
666 3
    public function __call($name, $args)
667
    {
668
        try {
669 3
            $this->__callStatic($name, $args);
670 2
        } catch (\Exception $e) {
671
            throw $e;
672
        }
673 3
    }
674
675
    /**
676
     * Serialize magic method.
677
     *
678
     * @return array
679
     */
680 3
    public function __sleep()
681
    {
682
        try {
683 3
            $this->_status_before_enqueue_['scheme'] = self::serializeOrDeserializeScheme(self::scheme());
684 3
            $this->_status_before_enqueue_['agentsConfig'] = self::config();
685 3
            $this->_status_before_enqueue_['handlers'] = self::serializeHandlers();
686 2
        } catch (\Exception $e) {
687
            //swallow exception
688
        }
689
690 3
        return ['smsData', 'firstAgent', 'pushedToQueue', '_status_before_enqueue_'];
691
    }
692
693
    /**
694
     * Deserialize magic method.
695
     */
696 3
    public function __wakeup()
697
    {
698 3
        if (empty($this->_status_before_enqueue_)) {
699
            return;
700
        }
701 3
        $status = $this->_status_before_enqueue_;
702 3
        self::$scheme = self::serializeOrDeserializeScheme($status['scheme']);
703 3
        self::$agentsConfig = $status['agentsConfig'];
704 3
        Balancer::destroy(self::TASK_NAME);
705 3
        self::bootstrap();
706 3
        self::reinstallHandlers($status['handlers']);
707 3
    }
708
709
    /**
710
     * Get a closure serializer.
711
     *
712
     * @return Serializer
713
     */
714 3
    protected static function getSerializer()
715
    {
716 3
        if (!self::$serializer) {
717 3
            self::$serializer = new Serializer();
718 2
        }
719
720 3
        return self::$serializer;
721
    }
722
723
    /**
724
     * Serialize or deserialize the agent use scheme.
725
     *
726
     * @param array $scheme
727
     *
728
     * @return array
729
     */
730 3
    protected static function serializeOrDeserializeScheme(array $scheme)
731
    {
732 3
        foreach ($scheme as $name => &$options) {
733 3
            if (is_array($options)) {
734 3
                self::serializeOrDeserializeClosureAndReplace($options, 'sendSms');
735 3
                self::serializeOrDeserializeClosureAndReplace($options, 'voiceVerify');
736 2
            }
737 2
        }
738
739 3
        return $scheme;
740
    }
741
742
    /**
743
     * Serialize the hooks` handlers of balancing task
744
     *
745
     * @return array
746
     */
747 3
    protected static function serializeHandlers()
748
    {
749 3
        $task = self::getTask();
750 3
        $hooks = $task->handlers;
751 3
        foreach ($hooks as &$handlers) {
752 3
            foreach (array_keys($handlers) as $key) {
753 3
                self::serializeOrDeserializeClosureAndReplace($handlers, $key);
754 2
            }
755 2
        }
756
757 3
        return $hooks;
758
    }
759
760
    /**
761
     * Reinstall hooks` handlers for balancing task.
762
     *
763
     * @param array $handlers
764
     */
765 3
    protected static function reinstallHandlers(array $handlers)
766
    {
767 3
        $serializer = self::getSerializer();
768 3
        foreach ($handlers as $hookName => $serializedHandlers) {
769 3
            foreach ($serializedHandlers as $index => $handler) {
770 3
                if (is_string($handler)) {
771 3
                    $handler = $serializer->unserialize($handler);
772 2
                }
773 3
                self::$hookName($handler, $index === 0);
774 2
            }
775 2
        }
776 3
    }
777
778
    /**
779
     * Serialize/deserialize the specified closure and replace the origin value.
780
     *
781
     * @param array      $options
782
     * @param int|string $key
783
     */
784 3
    protected static function serializeOrDeserializeClosureAndReplace(array &$options, $key)
785
    {
786 3
        if (!isset($options[$key])) {
787 3
            return;
788
        }
789 3
        $serializer = self::getSerializer();
790 3
        if (is_callable($options[$key])) {
791 3
            $options[$key] = (string) $serializer->serialize($options[$key]);
792 3
        } elseif (is_string($options[$key])) {
793 3
            $options[$key] = $serializer->unserialize($options[$key]);
794 2
        }
795 3
    }
796
}
797