• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

miaoxing / plugin / 6057194056

02 Sep 2023 07:53AM UTC coverage: 39.283% (+0.2%) from 39.08%
6057194056

push

github

semantic-release-bot
chore(release): publish

See CHANGELOG.md for more details.

920 of 2342 relevant lines covered (39.28%)

18.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.23
/src/Service/App.php
1
<?php
2

3
namespace Miaoxing\Plugin\Service;
4

5
use Exception;
6
use JsonSerializable;
7
use ReflectionException;
8
use ReflectionMethod;
9
use ReflectionParameter;
10
use Wei\BaseController;
11
use Wei\BaseController as WeiBaseController;
12
use Wei\BaseModel;
13
use Wei\Res;
14
use Wei\Ret\RetException;
15

16
/**
17
 * 应用
18
 *
19
 * @mixin \EventMixin
20
 * @mixin \StrMixin
21
 * @mixin \AppModelMixin
22
 * @mixin \CacheMixin
23
 * @mixin \PageRouterMixin
24
 * @mixin \ConfigMixin
25
 */
26
class App extends \Wei\App
27
{
28
    protected const NOT_FOUND = 404;
29

30
    protected const METHOD_NOT_ALLOWED = 405;
31

32
    /**
33
     * 插件控制器不使用该格式,留空可减少类查找
34
     *
35
     * {@inheritdoc}
36
     */
37
    protected $controllerFormat = '';
38

39
    /**
40
     * {@inheritdoc}
41
     */
42
    protected $actionMethodFormat = '%action%';
43

44
    /**
45
     * 当前运行的插件名称
46
     *
47
     * @var false|string
48
     */
49
    protected $plugin = false;
50

51
    /**
52
     * 默认域名
53
     *
54
     * 如果请求的默认域名,就不到数据库查找域名
55
     *
56
     * @var array
57
     */
58
    protected $domains = [];
59

60
    /**
61
     * @var string
62
     */
63
    protected $defaultViewFile = '@plugin/_default.php';
64

65
    /**
66
     * @var string|null
67
     */
68
    protected $fallbackPathInfo;
69

70
    /**
71
     * Whether the application is in demo mode
72
     *
73
     * @var bool
74
     */
75
    protected $isDemo = false;
76

77
    /**
78
     * @var string
79
     * @internal
80
     */
81
    protected $accessControlAllowOrigin = '*';
82

83
    /**
84
     * The id of the current application
85
     *
86
     * @var string
87
     */
88
    protected $id;
89

90
    /**
91
     * 应用模型缓存
92
     *
93
     * @var AppModel[]
94
     */
95
    protected $models = [];
96

97
    /**
98
     * @var array
99
     * @internal
100
     */
101
    protected $pathMap = [
102
        '/admin-api/' => '/api/admin/',
103
        '/api/admin/' => '/admin-api/',
104
        '/m-api/' => '/api/',
105
        '/api/' => '/m-api/',
106
    ];
107

108
    /**
109
     * @var WeiBaseController|null
110
     */
111
    private $curControllerInstance;
112

113
    /**
114
     * {@inheritdoc}
115
     */
116
    public function __invoke(array $options = [])
117
    {
118
        $this->prepareHeaders();
×
119

120
        // Load global config
121
        $this->config->preloadGlobal();
×
122

123
        $this->event->trigger('appInit');
×
124

125
        return $this->invokeApp($options);
×
126
    }
127

128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function getDefaultTemplate($controller = null, $action = null)
132
    {
133
        $file = $controller ?: $this->controller;
3✔
134
        $file = dirname($file) . '/_' . basename($file);
3✔
135

136
        $plugin = $this->getPlugin();
3✔
137

138
        return $plugin ? '@' . $plugin . '/' . $file : $file;
3✔
139
    }
140

141
    /**
142
     * 获取当前插件下的视图文件,即可省略当前插件名称不写
143
     *
144
     * @param string $name
145
     * @return string
146
     */
147
    public function getPluginFile($name)
148
    {
149
        return $this->view->getFile('@' . $this->getPlugin() . '/' . $name);
×
150
    }
151

152
    /**
153
     * 获取当前运行的插件名称
154
     *
155
     * @return string
156
     */
157
    public function getPlugin()
158
    {
159
        if (!$this->plugin && $this->controller) {
3✔
160
            // 认为第二部分是插件名称
161
            [, $plugin] = explode('/', $this->controller, 3);
×
162
            $this->plugin = $plugin;
×
163
        }
164
        return $this->plugin;
3✔
165
    }
166

167
    /**
168
     * Return the current application model object
169
     *
170
     * @return AppModel
171
     * @throws Exception When the application not found
172
     */
173
    public function getModel(): AppModel
174
    {
175
        $id = $this->getId();
3✔
176
        if (!isset($this->models[$id])) {
3✔
177
            $model = AppModel::new();
3✔
178
            $this->models[$id] = $model
3✔
179
                ->setCacheKey($model->getModelCacheKey($id))
3✔
180
                ->setCacheTime(86400)
3✔
181
                ->findOrFail($id);
3✔
182
        }
183
        return $this->models[$id];
3✔
184
    }
185

186
    /**
187
     * Set the current application model object
188
     *
189
     * @param AppModel|null $model
190
     * @return $this
191
     */
192
    public function setModel(?AppModel $model): self
193
    {
194
        $this->models[$this->getId()] = $model;
3✔
195
        return $this;
3✔
196
    }
197

198
    /**
199
     * Set the id of the current application
200
     *
201
     * @param string|null $id
202
     * @return $this
203
     */
204
    public function setId(?string $id): self
205
    {
206
        $this->id = $id;
3✔
207
        return $this;
3✔
208
    }
209

210
    /**
211
     * Return the id of the current application
212
     *
213
     * @return string
214
     */
215
    public function getId(): string
216
    {
217
        if (!$this->id) {
279✔
218
            $this->id = $this->detectId();
3✔
219
        }
220
        return $this->id;
279✔
221
    }
222

223
    /**
224
     * 重写handleResponse,支持Ret结构
225
     *
226
     * @param mixed $response
227
     * @return Res
228
     * @throws Exception
229
     */
230
    public function handleResponse($response)
231
    {
232
        if ($response instanceof Ret) {
39✔
233
            return $response->toRes($this->req, $this->res);
9✔
234
        } elseif ($response instanceof JsonSerializable) {
30✔
235
            return $this->res->json($response);
×
236
        } elseif (is_array($response)) {
30✔
237
            $template = $this->getDefaultTemplate();
3✔
238
            $file = $this->view->resolveFile($template) ? $template : $this->defaultViewFile;
3✔
239
            $content = $this->view->render($file, $response);
3✔
240
            return $this->res->setContent($content);
3✔
241
        } else {
242
            return parent::handleResponse($response);
27✔
243
        }
244
    }
245

246
    /**
247
     * 判断是否请求到后台页面
248
     *
249
     * @return bool
250
     */
251
    public function isAdmin()
252
    {
253
        // NOTE: 控制器不存在时,回退的控制器不带有 admin
254
        return false !== strpos($this->req->getRouterPathInfo(), '/admin/');
×
255
    }
256

257
    /**
258
     * 设置默认视图文件
259
     *
260
     * @param string $defaultViewFile
261
     * @return $this
262
     */
263
    public function setDefaultViewFile($defaultViewFile)
264
    {
265
        $this->defaultViewFile = $defaultViewFile;
3✔
266
        return $this;
3✔
267
    }
268

269
    /**
270
     * @return WeiBaseController
271
     * @experimental
272
     */
273
    public function getCurControllerInstance(): WeiBaseController
274
    {
275
        if (!$this->curControllerInstance) {
3✔
276
            $this->curControllerInstance = require $this->controller;
3✔
277
        }
278
        return $this->curControllerInstance;
3✔
279
    }
280

281
    /**
282
     * Returns whether the application is in demo mode
283
     *
284
     * @return bool
285
     * @svc
286
     */
287
    protected function isDemo(): bool
288
    {
289
        return $this->isDemo;
×
290
    }
291

292
    protected function invokeApp(array $options = [])
293
    {
294
        $options && $this->setOption($options);
×
295

296
        $pathInfo = $this->req->getRouterPathInfo();
×
297

298
        $result = $this->matchPathInfo($pathInfo);
×
299
        if (!$result) {
×
300
            throw new \Exception('Not Found', static::NOT_FOUND);
×
301
        }
302

303
        $action = strtolower($this->req->getMethod());
×
304
        return $this->dispatch($result['file'], $action, $result['params']);
×
305
    }
306

307
    /**
308
     * {@inheritdoc}
309
     */
310
    public function dispatch($controller, $action = null, array $params = [], $throwException = true /* ignored */)
311
    {
312
        $this->setController($controller);
3✔
313
        $this->setAction($action);
3✔
314
        $this->req->set($params);
3✔
315

316
        $page = $this->getCurControllerInstance();
3✔
317

318
        if ($this->req->isPreflight()) {
3✔
319
            return $this->res->send();
×
320
        }
321

322
        if (!method_exists($page, $action)) {
3✔
323
            $this->res->setStatusCode(static::METHOD_NOT_ALLOWED);
×
324
            throw new \Exception('Method Not Allowed', static::METHOD_NOT_ALLOWED);
×
325
        }
326

327
        return $this->execute($page, $action);
3✔
328
    }
329

330
    /**
331
     * @param BaseController $instance
332
     * @param string $action
333
     * @return Res
334
     * @throws Exception
335
     */
336
    protected function execute($instance, $action)
337
    {
338
        $wei = $this->wei;
42✔
339

340
        $instance->init();
42✔
341
        $middleware = $this->getMiddleware($instance, $action);
42✔
342

343
        $callback = function () use ($instance, $action) {
28✔
344
            $instance->before($this->req, $this->res);
33✔
345

346
            $method = $this->getActionMethod($action);
33✔
347
            // TODO 和 forward 异常合并一起处理
348
            try {
349
                $args = $this->buildActionArgs($instance, $method);
33✔
350
                $response = $instance->{$method}(...$args);
30✔
351
            } catch (RetException $e) {
3✔
352
                return $e->getRet();
×
353
            }
354

355
            $instance->after($this->req, $response);
30✔
356

357
            return $response;
30✔
358
        };
42✔
359

360
        $next = function () use (&$middleware, &$next, $callback, $wei, $instance) {
28✔
361
            $config = array_splice($middleware, 0, 1);
42✔
362
            if ($config) {
42✔
363
                $class = key($config);
9✔
364
                $service = new $class(['wei' => $wei] + $config[$class]);
9✔
365
                $result = $service($next, $instance);
9✔
366
            } else {
367
                $result = $callback();
33✔
368
            }
369

370
            return $result;
39✔
371
        };
42✔
372

373
        return $this->handleResponse($next())->send();
42✔
374
    }
375

376
    /**
377
     * @param object $instance
378
     * @param string $method
379
     * @return array
380
     * @throws ReflectionException
381
     */
382
    protected function buildActionArgs($instance, string $method)
383
    {
384
        $ref = new ReflectionMethod($instance, $method);
33✔
385
        $params = $ref->getParameters();
33✔
386
        if (!$params || 'req' === $params[0]->getName()) {
33✔
387
            return [$this->req, $this->res];
15✔
388
        }
389

390
        $args = [];
18✔
391
        foreach ($params as $param) {
18✔
392
            $args[] = $this->buildActionArg($param);
18✔
393
        }
394
        return $args;
15✔
395
    }
396

397
    /**
398
     * @param ReflectionParameter $param
399
     * @return mixed
400
     * @throws ReflectionException
401
     */
402
    protected function buildActionArg(ReflectionParameter $param)
403
    {
404
        /** @link https://github.com/phpstan/phpstan/issues/1133 */
405
        /** @var \ReflectionNamedType|null $type */
406
        $type = $param->getType();
18✔
407

408
        // Handle Model class
409
        if (
410
            $type
18✔
411
            && !$type->isBuiltin()
18✔
412
            && is_a($type->getName(), BaseModel::class, true)
18✔
413
        ) {
414
            return $type->getName()::findOrFail($this->req['id']);
3✔
415
        }
416

417
        // Handle other class
418
        if ($type && !$type->isBuiltin()) {
15✔
419
            throw new Exception('Unsupported action parameter type: ' . $type);
×
420
        }
421

422
        // TODO Throw exception for unsupported builtin type
423
        // Handle builtin type
424
        $arg = $this->req[$param->getName()];
15✔
425
        if (null === $arg) {
15✔
426
            if ($param->isDefaultValueAvailable()) {
9✔
427
                $arg = $param->getDefaultValue();
6✔
428
            } else {
429
                throw new Exception('Missing required parameter: ' . $param->getName(), 400);
9✔
430
            }
431
        } elseif ($type) {
9✔
432
            settype($arg, $type->getName());
6✔
433
        }
434

435
        return $arg;
12✔
436
    }
437

438
    /**
439
     * Detect the id of application
440
     *
441
     * @return string
442
     */
443
    protected function detectId(): string
444
    {
445
        // 1. Domain
446
        if ($id = $this->getIdByDomain()) {
3✔
447
            return $id;
3✔
448
        }
449

450
        // 2. Request parameter
451
        if ($id = $this->req->get('appId')) {
3✔
452
            return $id;
×
453
        }
454

455
        // 3. First id from database
456
        return $this->cache->remember('app:firstId', 86400, static function () {
2✔
457
            return AppModel::select('id')->asc('id')->fetchColumn();
×
458
        });
3✔
459
    }
460

461
    /**
462
     * 根据域名查找应用编号
463
     *
464
     * @return string|null
465
     */
466
    protected function getIdByDomain(): ?string
467
    {
468
        $domain = $this->req->getHost();
3✔
469
        if (!$domain) {
3✔
470
            // CLI 下默认没有域名,直接返回
471
            return null;
3✔
472
        }
473

474
        if (in_array($domain, $this->domains, true)) {
3✔
475
            return null;
×
476
        }
477

478
        return $this->cache->remember('appDomain:' . $domain, 86400, static function () use ($domain) {
2✔
479
            $app = AppModel::select('id')->fetch('domain', $domain);
3✔
480
            return $app ? $app['id'] : null;
3✔
481
        });
3✔
482
    }
483

484
    /**
485
     * @internal
486
     */
487
    protected function matchPathInfo(string $pathInfo): ?array
488
    {
489
        $result = $this->pageRouter->match($pathInfo);
×
490
        if ($result) {
×
491
            return $result;
×
492
        }
493

494
        foreach ($this->pathMap as $search => $replace) {
×
495
            if (str_contains($pathInfo, $search)) {
×
496
                $pathInfo = str_replace($search, $replace, $pathInfo);
×
497
                $result = $this->pageRouter->match($pathInfo);
×
498
                if ($result) {
×
499
                    return $result;
×
500
                }
501
                break;
×
502
            }
503
        }
504

505
        if ($this->fallbackPathInfo) {
×
506
            return $this->pageRouter->match($this->fallbackPathInfo);
×
507
        }
508

509
        return null;
×
510
    }
511

512
    /**
513
     * 根据请求设置跨域标头信息
514
     *
515
     * @return void
516
     * @internal
517
     */
518
    public function prepareHeaders()
519
    {
520
        $this->res->setHeader('Access-Control-Allow-Origin', $this->accessControlAllowOrigin);
×
521
        if ($this->req->isPreflight()) {
×
522
            $this->res
×
523
                ->setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
×
524
                // NOTE: antd upload 组件上传会加上 XMLHttpRequest 头
525
                ->setHeader('Access-Control-Allow-Headers', 'Origin, Content-Type, Authorization, X-Requested-With')
×
526
                ->setHeader('Access-Control-Max-Age', 0);
×
527
        }
528
    }
529
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc