Completed
Push — master ( daa55e...283203 )
by lan tian
9s
created

Sms::__callStatic()   B

Complexity

Conditions 8
Paths 80

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 8.1867

Importance

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