Completed
Pull Request — master (#68)
by lan tian
06:52
created

Sms::initTask()   C

Complexity

Conditions 7
Paths 3

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 7.0084

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 29
rs 6.7272
ccs 17
cts 18
cp 0.9444
cc 7
eloc 19
nc 3
nop 0
crap 7.0084
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] for serialize closure objects.
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
    public static function bootstrap()
132
    {
133
        if (!self::taskInitialized()) {
134 6
            self::configuration();
135
            self::initTask();
136 6
        }
137 6
    }
138 3
139 3
    /**
140 2
     * Whether task initialized.
141 6
     *
142
     * Note: 判断drivers是否为空不能用'empty',因为在TaskBalance库的中Task类的drivers属性是受保护的(不可访问),
143
     * 虽然通过魔术方法可以获取到其值,但在其目前版本(v0.4.2)其内部却并没有使用'__isset'魔术方法对'empty'或'isset'函数进行逻辑补救.
144
     *
145
     * @return bool
146
     */
147
    protected static function taskInitialized()
148 15
    {
149
        $task = self::getTask();
150 15
151 3
        return (bool) count($task->drivers);
152 2
    }
153
154 15
    /**
155
     * Get or generate a balancing task instance for send SMS/voice verify.
156
     *
157
     * @return Task
158
     */
159
    public static function getTask()
160 6
    {
161
        if (!Balancer::hasTask(self::TASK_NAME)) {
162 6
            Balancer::task(self::TASK_NAME);
163 6
        }
164 3
165 2
        return Balancer::getTask(self::TASK_NAME);
166 6
    }
167 6
168 6
    /**
169 6
     * Configuration.
170
     */
171
    protected static function configuration()
172
    {
173
        $config = [];
174
        if (!count(self::scheme())) {
175
            self::initScheme($config);
176 3
        }
177
        $diff = array_diff_key(self::scheme(), self::$agentsConfig);
178 3
        self::initAgentsConfig(array_keys($diff), $config);
179 3
        self::validateConfig();
180 3
    }
181 3
182
    /**
183
     * Try to read the dispatch scheme of agent from config file.
184
     *
185
     * @param array $config
186
     */
187
    protected static function initScheme(array &$config)
188
    {
189 6
        $config = empty($config) ? include __DIR__ . '/../config/phpsms.php' : $config;
190
        $scheme = isset($config['scheme']) ? $config['scheme'] : [];
191 6
        self::scheme($scheme);
192 3
    }
193
194 3
    /**
195 3
     * Try to initialize the specified agents` configuration information.
196 3
     *
197 3
     * @param array $agents
198 3
     * @param array $config
199 2
     */
200 3
    protected static function initAgentsConfig(array $agents, array &$config)
201
    {
202
        if (empty($agents)) {
203
            return;
204
        }
205
        $config = empty($config) ? include __DIR__ . '/../config/phpsms.php' : $config;
206
        $agentsConfig = isset($config['agents']) ? $config['agents'] : [];
207 6
        foreach ($agents as $name) {
208
            $agentConfig = isset($agentsConfig[$name]) ? $agentsConfig[$name] : [];
209 6
            self::config($name, $agentConfig);
210
        }
211
    }
212 6
213
    /**
214
     * validate configuration.
215
     *
216
     * @throws PhpSmsException
217
     */
218
    protected static function validateConfig()
219 18
    {
220
        if (!count(self::scheme())) {
221 3
            throw new PhpSmsException('Please configure at least one agent');
222
        }
223 3
    }
224 3
225 3
    /**
226 2
     * Initialize the task.
227
     */
228
    protected static function initTask()
229 18
    {
230 18
        foreach (self::scheme() as $name => $scheme) {
231 18
            //解析代理器数组模式的调度配置
232 18
            if (is_array($scheme)) {
233 18
                $data = self::parseScheme($scheme);
234
                $scheme = $data['scheme'];
235 18
            }
236 18
            //创建任务驱动器
237 12
            self::getTask()->driver("$name $scheme")->work(function ($driver) {
238 18
                $agent = self::getAgent($driver->name);
239 18
                $smsData = $driver->getTaskData();
240 18
                extract($smsData);
241 12
                $template = isset($templates[$driver->name]) ? $templates[$driver->name] : 0;
242 18
                if ($type === self::TYPE_VOICE) {
243
                    $agent->voiceVerify($to, $voiceCode, $template, $templateData);
244 18
                } elseif ($type === self::TYPE_SMS) {
245 3
                    $agent->sendSms($to, $content, $template, $templateData);
246 2
                }
247 3
                $result = $agent->result();
248
                if ($result['success']) {
249
                    $driver->success();
250
                }
251
                unset($result['success']);
252
253
                return $result;
254
            });
255
        }
256
    }
257 9
258
    /**
259 9
     * Parsing the dispatch scheme.
260 9
     * 解析代理器的数组模式的调度配置
261 9
     *
262 9
     * @param array $options
263 9
     *
264
     * @return array
265 9
     */
266
    protected static function parseScheme(array $options)
267
    {
268
        $agentClass = Util::pullFromArrayByKey($options, 'agentClass');
269
        $sendSms = Util::pullFromArrayByKey($options, 'sendSms');
270
        $voiceVerify = Util::pullFromArrayByKey($options, 'voiceVerify');
271
        $backup = Util::pullFromArrayByKey($options, 'backup') ? 'backup' : '';
272
        $scheme = implode(' ', array_values($options)) . " $backup";
273
274
        return compact('agentClass', 'sendSms', 'voiceVerify', 'scheme');
275
    }
276
277
    /**
278 27
     * Get a sms agent instance by agent name,
279
     * if null, will try to create a new agent instance.
280 27
     *
281 9
     * @param string $name
282 9
     *
283 9
     * @throws PhpSmsException
284 9
     *
285 9
     * @return mixed
286 3
     */
287 8
    public static function getAgent($name)
288 6
    {
289 4
        if (!self::hasAgent($name)) {
290
            $scheme = self::scheme($name);
291
            $data = self::parseScheme(is_array($scheme) ? $scheme : [$scheme]);
292 6
            $data = array_merge(self::config($name), $data);
293
            $className = $data['agentClass'] ?: ('Toplan\\PhpSms\\' . $name . 'Agent');
294 27
            if (is_callable($data['sendSms']) || is_callable($data['voiceVerify'])) {
295
                self::$agents[$name] = new ParasiticAgent($data);
296
            } elseif (class_exists($className)) {
297
                self::$agents[$name] = new $className($data);
298
            } else {
299
                throw new PhpSmsException("Dont support [$name] agent.");
300
            }
301
        }
302
303
        return self::$agents[$name];
304 36
    }
305
306 36
    /**
307
     * Whether to has specified agent.
308
     *
309
     * @param string $name
310
     *
311
     * @return bool
312
     */
313
    public static function hasAgent($name)
314
    {
315
        return isset(self::$agents[$name]);
316
    }
317 18
318
    /**
319
     * Set or get the dispatch scheme of agent by name.
320 6
     *
321 3
     * @param mixed $name
322 6
     * @param mixed $scheme
323 6
     *
324 4
     * @return mixed
325 18
     */
326
    public static function scheme($name = null, $scheme = null)
327
    {
328
        return Util::operateArray(self::$scheme, $name, $scheme, null, function ($key, $value) {
329
            if (is_string($key)) {
330
                self::modifyScheme($key, is_array($value) ? $value : "$value");
331
            } elseif (is_int($key)) {
332
                self::modifyScheme($value, '');
333
            }
334
        });
335
    }
336 6
337
    /**
338 6
     * Modify the dispatch scheme of agent by name.
339 6
     *
340 6
     * @param $key
341
     * @param $value
342
     *
343
     * @throws PhpSmsException
344
     */
345
    protected static function modifyScheme($key, $value)
346
    {
347
        if (self::taskInitialized()) {
348
            throw new PhpSmsException("Modify the dispatch scheme failed for [$key] agent, because the task system has already started.");
349
        }
350
        self::validateAgentName($key);
351
        self::$scheme[$key] = $value;
352
    }
353 18
354
    /**
355 18
     * Set or get configuration information by agent name.
356 6
     *
357 4
     * @param mixed $name
358
     * @param mixed $config
359
     * @param bool  $override
360 9
     *
361 9
     * @throws PhpSmsException
362 6
     *
363 18
     * @return array
364 6
     */
365 6
    public static function config($name = null, $config = null, $override = false)
366 6
    {
367 4
        if (is_array($name) && is_bool($config)) {
368 2
            $override = $config;
369 4
        }
370 18
371
        return Util::operateArray(self::$agentsConfig, $name, $config, [], function ($key, $value) {
372
            if (is_array($value)) {
373
                self::modifyConfig($key, $value);
374
            }
375
        }, $override, function (array $origin) {
376
            $nameList = array_keys($origin);
377
            foreach ($nameList as $name) {
378
                if (self::hasAgent("$name")) {
379
                    self::getAgent("$name")->config([], true);
380
                }
381 9
            }
382
        });
383 9
    }
384 9
385 9
    /**
386 3
     * Modify the configuration information of agent by name.
387 2
     *
388 9
     * @param string $key
389
     * @param array  $value
390
     *
391
     * @throws PhpSmsException
392
     */
393
    protected static function modifyConfig($key, array $value)
394
    {
395
        self::validateAgentName($key);
396
        self::$agentsConfig[$key] = $value;
397
        if (self::hasAgent($key)) {
398 12
            self::getAgent($key)->config($value);
399
        }
400 12
    }
401
402
    /**
403 12
     * Validate the agent name.
404
     * Agent name must be a string, but not be a number string
405
     *
406
     * @param string $name
407
     *
408
     * @throws PhpSmsException
409 6
     */
410
    protected static function validateAgentName($name)
411 6
    {
412 6
        if (!$name || !is_string($name) || preg_match('/^[0-9]+$/', $name)) {
413 6
            throw new PhpSmsException("The agent name [$name] is illegal. Agent name must be a string, but not be a number string.");
414
        }
415
    }
416
417
    /**
418 6
     * Tear down agent use scheme and prepare to create and start a new balancing task,
419
     * so before do it must destroy old task instance.
420 6
     *
421 6
     */
422
    public static function cleanScheme()
423
    {
424
        Balancer::destroy(self::TASK_NAME);
425
        self::$scheme = [];
426
    }
427
428
    /**
429
     * Tear down all the configuration information of agent.
430
     */
431
    public static function cleanConfig()
432
    {
433
        self::config([], true);
434
    }
435
436
    /**
437
     * Create a sms instance send SMS,
438
     * your can also set SMS templates or content at the same time.
439
     *
440
     * @param mixed $agentName
441
     * @param mixed $tempId
442
     *
443
     * @return Sms
444
     */
445
    public static function make($agentName = null, $tempId = null)
446
    {
447
        $sms = new self();
448
        $sms->smsData['type'] = self::TYPE_SMS;
449
        if (is_array($agentName)) {
450
            $sms->template($agentName);
451
        } elseif ($agentName && is_string($agentName)) {
452
            if ($tempId === null) {
453
                $sms->content($agentName);
454
            } elseif (is_string($tempId) || is_int($tempId)) {
455
                $sms->template($agentName, "$tempId");
456
            }
457 3
        }
458
459 3
        return $sms;
460 3
    }
461 3
462
    /**
463 3
     * Create a sms instance send voice verify,
464
     * your can also set verify code at the same time.
465
     *
466
     * @param int|string|null $code
467
     *
468
     * @return Sms
469
     */
470
    public static function voice($code = null)
471
    {
472
        $sms = new self();
473
        $sms->smsData['type'] = self::TYPE_VOICE;
474 3
        $sms->smsData['voiceCode'] = $code;
475
476 3
        return $sms;
477 3
    }
478
479 3
    /**
480 3
     * Set whether to use the queue system, and define how to use it.
481 3
     *
482 2
     * @param mixed $enable
483 3
     * @param mixed $handler
484 3
     *
485 3
     * @return bool
486 2
     */
487
    public static function queue($enable = null, $handler = null)
488 3
    {
489
        if ($enable === null && $handler === null) {
490
            return self::$enableQueue;
491
        }
492
        if (is_callable($enable)) {
493
            $handler = $enable;
494
            $enable = true;
495
        }
496
        self::$enableQueue = (bool) $enable;
497
        if (is_callable($handler)) {
498 6
            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...
499
        }
500 6
501
        return self::$enableQueue;
502 6
    }
503
504
    /**
505
     * Set the recipient`s mobile number.
506
     *
507
     * @param string $mobile
508
     *
509
     * @return $this
510
     */
511
    public function to($mobile)
512 3
    {
513
        $this->smsData['to'] = trim((string) $mobile);
514 3
515
        return $this;
516 3
    }
517
518
    /**
519
     * Set the content for content SMS.
520
     *
521
     * @param string $content
522
     *
523
     * @return $this
524
     */
525
    public function content($content)
526
    {
527 3
        $this->smsData['content'] = trim((string) $content);
528
529 3
        return $this;
530
    }
531 3
532
    /**
533
     * Set the template id for template SMS.
534
     *
535
     * @param mixed $name
536
     * @param mixed $tempId
537
     *
538
     * @return $this
539
     */
540
    public function template($name, $tempId = null)
541 3
    {
542
        Util::operateArray($this->smsData['templates'], $name, $tempId);
543 3
544
        return $this;
545 3
    }
546
547
    /**
548
     * Set the template data for template SMS.
549
     *
550
     * @param array $data
551
     *
552
     * @return $this
553
     */
554
    public function data(array $data)
555 3
    {
556
        $this->smsData['templateData'] = $data;
557 3
558
        return $this;
559 3
    }
560
561
    /**
562
     * Set the first agent by name.
563
     *
564
     * @param string $name
565
     *
566
     * @return $this
567
     */
568
    public function agent($name)
569
    {
570
        $this->firstAgent = (string) $name;
571
572
        return $this;
573 18
    }
574
575 18
    /**
576 18
     * Start send SMS/voice verify.
577 12
     *
578 18
     * If give a true parameter, this system will immediately start request to send SMS/voice verify whatever whether to use the queue.
579 18
     * if you are already pushed sms instance to the queue, you can recall the method `send()` in queue system without `true` parameter,
580 18
     * so this mechanism in order to make you convenient use the method `send()` in queue system.
581 18
     *
582 12
     * @param bool $immediately
583 12
     *
584 3
     * @return mixed
585
     */
586
    public function send($immediately = false)
587 18
    {
588
        if (!self::$enableQueue || $this->pushedToQueue) {
589
            $immediately = true;
590
        }
591
        if ($immediately) {
592
            $result = Balancer::run(self::TASK_NAME, [
593
                'data'   => $this->getData(),
594
                'driver' => $this->firstAgent,
595
            ]);
596
        } else {
597 3
            $result = $this->push();
598
        }
599 3
600
        return $result;
601 3
    }
602
603 3
    /**
604
     * Push to the queue by a custom method.
605
     *
606
     * @throws \Exception | PhpSmsException
607
     *
608
     * @return mixed
609
     */
610
    public function push()
611
    {
612
        if (is_callable(self::$howToUseQueue)) {
613
            try {
614
                $this->pushedToQueue = true;
615
616
                return call_user_func_array(self::$howToUseQueue, [$this, $this->getData()]);
617
            } catch (\Exception $e) {
618
                $this->pushedToQueue = false;
619
                throw $e;
620 36
            }
621
        } else {
622 36
            throw new PhpSmsException('Please define how to use queue by this static method: queue(...)');
623 3
        }
624
    }
625
626 36
    /**
627
     * Get all the data of SMS/voice verify.
628
     *
629
     * @param null|string $name
630
     *
631
     * @return mixed
632
     */
633
    public function getData($name = null)
634
    {
635
        if (is_string($name) && isset($this->smsData["$name"])) {
636
            return $this->smsData[$name];
637 9
        }
638
639 9
        return $this->smsData;
640 9
    }
641 9
642 9
    /**
643 9
     * Overload static method.
644 9
     *
645 9
     * @param string $name
646 9
     * @param array  $args
647 9
     *
648 9
     * @throws PhpSmsException
649 6
     */
650 3
    public static function __callStatic($name, $args)
651
    {
652 6
        $name = $name === 'beforeSend' ? 'beforeRun' : $name;
653
        $name = $name === 'afterSend' ? 'afterRun' : $name;
654
        $name = $name === 'beforeAgentSend' ? 'beforeDriverRun' : $name;
655 9
        $name = $name === 'afterAgentSend' ? 'afterDriverRun' : $name;
656
        if (in_array($name, self::$availableHooks)) {
657
            $handler = $args[0];
658
            $override = isset($args[1]) ? (bool) $args[1] : false;
659
            if (is_callable($handler)) {
660
                $task = self::getTask();
661
                $task->hook($name, $handler, $override);
662
            } else {
663
                throw new PhpSmsException("Please give method $name() a callable parameter");
664
            }
665
        } else {
666 3
            throw new PhpSmsException("Dont find method $name()");
667
        }
668
    }
669 3
670 2
    /**
671
     * Overload method.
672
     *
673 3
     * @param string $name
674
     * @param array  $args
675
     *
676
     * @throws PhpSmsException
677
     * @throws \Exception
678
     */
679
    public function __call($name, $args)
680 3
    {
681
        try {
682
            $this->__callStatic($name, $args);
683 3
        } catch (\Exception $e) {
684 3
            throw $e;
685 3
        }
686 2
    }
687
688
    /**
689
     * Serialize magic method.
690 3
     *
691
     * @return array
692
     */
693
    public function __sleep()
694
    {
695
        try {
696 3
            $this->_status_before_enqueue_['scheme'] = self::serializeOrDeserializeScheme(self::scheme());
697
            $this->_status_before_enqueue_['agentsConfig'] = self::config();
698 3
            $this->_status_before_enqueue_['handlers'] = self::serializeHandlers();
699
        } catch (\Exception $e) {
700
            //swallow exception
701 3
        }
702 3
703 3
        return ['smsData', 'firstAgent', 'pushedToQueue', '_status_before_enqueue_'];
704 3
    }
705 3
706 3
    /**
707 3
     * Deserialize magic method.
708
     */
709
    public function __wakeup()
710
    {
711
        if (empty($this->_status_before_enqueue_)) {
712
            return;
713
        }
714 3
        $status = $this->_status_before_enqueue_;
715
        self::$scheme = self::serializeOrDeserializeScheme($status['scheme']);
716 3
        self::$agentsConfig = $status['agentsConfig'];
717 3
        Balancer::destroy(self::TASK_NAME);
718 2
        self::bootstrap();
719
        self::reinstallHandlers($status['handlers']);
720 3
    }
721
722
    /**
723
     * Get a closure serializer.
724
     *
725
     * @return Serializer
726
     */
727
    protected static function getSerializer()
728
    {
729
        if (!self::$serializer) {
730 3
            self::$serializer = new Serializer();
731
        }
732 3
733 3
        return self::$serializer;
734 3
    }
735 3
736 2
    /**
737 2
     * Serialize or deserialize the agent use scheme.
738
     *
739 3
     * @param array $scheme
740
     *
741
     * @return array
742
     */
743
    protected static function serializeOrDeserializeScheme(array $scheme)
744
    {
745
        foreach ($scheme as $name => &$options) {
746
            if (is_array($options)) {
747 3
                self::serializeOrDeserializeClosureAndReplace($options, 'sendSms');
748
                self::serializeOrDeserializeClosureAndReplace($options, 'voiceVerify');
749 3
            }
750 3
        }
751 3
752 3
        return $scheme;
753 3
    }
754 2
755 2
    /**
756
     * Serialize the hooks` handlers of balancing task
757 3
     *
758
     * @return array
759
     */
760
    protected static function serializeHandlers()
761
    {
762
        $task = self::getTask();
763
        $hooks = (array) $task->handlers;
764
        foreach ($hooks as &$handlers) {
765 3
            foreach (array_keys($handlers) as $key) {
766
                self::serializeOrDeserializeClosureAndReplace($handlers, $key);
767 3
            }
768 3
        }
769 3
770 3
        return $hooks;
771 3
    }
772 2
773 3
    /**
774 2
     * Reinstall hooks` handlers for balancing task.
775 2
     *
776 3
     * @param array $handlers
777
     */
778
    protected static function reinstallHandlers(array $handlers)
779
    {
780
        $serializer = self::getSerializer();
781
        foreach ($handlers as $hookName => $serializedHandlers) {
782
            foreach ($serializedHandlers as $index => $handler) {
783
                if (is_string($handler)) {
784 3
                    $handler = $serializer->unserialize($handler);
785
                }
786 3
                self::$hookName($handler, $index === 0);
787 3
            }
788
        }
789 3
    }
790 3
791 3
    /**
792 3
     * Serialize/deserialize the specified closure and replace the origin value.
793 3
     *
794 2
     * @param array      $options
795 3
     * @param int|string $key
796
     */
797
    protected static function serializeOrDeserializeClosureAndReplace(array &$options, $key)
798
    {
799
        if (!isset($options[$key])) {
800
            return;
801
        }
802
        $serializer = self::getSerializer();
803
        if (is_callable($options[$key])) {
804
            $options[$key] = (string) $serializer->serialize($options[$key]);
805
        } elseif (is_string($options[$key])) {
806
            $options[$key] = $serializer->unserialize($options[$key]);
807
        }
808
    }
809
}
810