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

miaoxing / plugin / 7322930040

25 Dec 2023 04:02PM UTC coverage: 37.918% (-1.7%) from 39.661%
7322930040

push

github

twinh
ci: add PHP 8, remove PHP 7.2, 7.3

907 of 2392 relevant lines covered (37.92%)

5.95 hits per line

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

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

3
namespace Miaoxing\Plugin\Service;
4

5
use Exception;
6
use Wei\BaseController;
7
use Wei\BaseController as WeiBaseController;
8
use Wei\BaseModel;
9
use Wei\Res;
10
use Wei\Ret\RetException;
11

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

26
    protected const METHOD_NOT_ALLOWED = 405;
27

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

35
    /**
36
     * {@inheritdoc}
37
     */
38
    protected $actionMethodFormat = '%action%';
39

40
    /**
41
     * 当前运行的插件名称
42
     *
43
     * @var false|string
44
     */
45
    protected $plugin = false;
46

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

56
    /**
57
     * @var string
58
     */
59
    protected $defaultViewFile = '@plugin/_default.php';
60

61
    /**
62
     * @var string|null
63
     */
64
    protected $fallbackPathInfo;
65

66
    /**
67
     * Whether the application is in demo mode
68
     *
69
     * @var bool
70
     */
71
    protected $isDemo = false;
72

73
    /**
74
     * @var string
75
     * @internal
76
     */
77
    protected $accessControlAllowOrigin = '*';
78

79
    /**
80
     * The id of the current application
81
     *
82
     * @var string
83
     */
84
    protected $id;
85

86
    /**
87
     * 应用模型缓存
88
     *
89
     * @var AppModel[]
90
     */
91
    protected $models = [];
92

93
    /**
94
     * @var array
95
     * @internal
96
     */
97
    protected $pathMap = [
98
        '/admin-api/' => '/api/admin/',
99
        '/api/admin/' => '/admin-api/',
100
        '/m-api/' => '/api/',
101
        '/api/' => '/m-api/',
102
    ];
103

104
    /**
105
     * @var WeiBaseController|null
106
     */
107
    private $curControllerInstance;
108

109
    /**
110
     * {@inheritdoc}
111
     */
112
    public function __invoke(array $options = [])
113
    {
114
        $this->prepareHeaders();
×
115

116
        // Load global config
117
        $this->config->preloadGlobal();
×
118

119
        $this->event->trigger('appInit');
×
120

121
        return $this->invokeApp($options);
×
122
    }
123

124
    /**
125
     * {@inheritdoc}
126
     */
127
    public function getDefaultTemplate($controller = null, $action = null)
128
    {
129
        $file = $controller ?: $this->controller;
1✔
130
        $file = dirname($file) . '/_' . basename($file);
1✔
131

132
        $plugin = $this->getPlugin();
1✔
133

134
        return $plugin ? '@' . $plugin . '/' . $file : $file;
1✔
135
    }
136

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

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

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

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

194
    /**
195
     * Set the id of the current application
196
     *
197
     * @param string|null $id
198
     * @return $this
199
     */
200
    public function setId(?string $id): self
201
    {
202
        $this->id = $id;
1✔
203
        return $this;
1✔
204
    }
205

206
    /**
207
     * Return the id of the current application
208
     *
209
     * @return string
210
     */
211
    public function getId(): string
212
    {
213
        if (!$this->id) {
96✔
214
            $this->id = $this->detectId();
1✔
215
        }
216
        return $this->id;
96✔
217
    }
218

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

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

253
    /**
254
     * 设置默认视图文件
255
     *
256
     * @param string $defaultViewFile
257
     * @return $this
258
     */
259
    public function setDefaultViewFile($defaultViewFile)
260
    {
261
        $this->defaultViewFile = $defaultViewFile;
1✔
262
        return $this;
1✔
263
    }
264

265
    /**
266
     * @return WeiBaseController
267
     * @experimental
268
     */
269
    public function getCurControllerInstance(): WeiBaseController
270
    {
271
        if (!$this->curControllerInstance) {
1✔
272
            $this->curControllerInstance = require $this->controller;
1✔
273
        }
274
        return $this->curControllerInstance;
1✔
275
    }
276

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

288
    protected function invokeApp(array $options = [])
289
    {
290
        $options && $this->setOption($options);
×
291

292
        $pathInfo = $this->req->getRouterPathInfo();
×
293

294
        $result = $this->matchPathInfo($pathInfo);
×
295
        if (!$result) {
×
296
            throw new \Exception('Not Found', static::NOT_FOUND);
×
297
        }
298

299
        $action = strtolower($this->req->getMethod());
×
300
        return $this->dispatch($result['file'], $action, $result['params']);
×
301
    }
302

303
    /**
304
     * {@inheritdoc}
305
     */
306
    public function dispatch($controller, $action = null, array $params = [], $throwException = true /* ignored */)
307
    {
308
        $this->setController($controller);
1✔
309
        $this->setAction($action);
1✔
310
        $this->req->set($params);
1✔
311

312
        $page = $this->getCurControllerInstance();
1✔
313

314
        if ($this->req->isPreflight()) {
1✔
315
            return $this->res->send();
×
316
        }
317

318
        if (!method_exists($page, $action)) {
1✔
319
            $this->res->setStatusCode(static::METHOD_NOT_ALLOWED);
×
320
            throw new \Exception('Method Not Allowed', static::METHOD_NOT_ALLOWED);
×
321
        }
322

323
        return $this->execute($page, $action);
1✔
324
    }
325

326
    /**
327
     * @param BaseController $instance
328
     * @param string $action
329
     * @return Res
330
     * @throws \Exception
331
     */
332
    protected function execute($instance, $action)
333
    {
334
        $wei = $this->wei;
14✔
335

336
        $instance->init();
14✔
337
        $middleware = $this->getMiddleware($instance, $action);
14✔
338

339
        $callback = function () use ($instance, $action) {
14✔
340
            $instance->before($this->req, $this->res);
11✔
341

342
            $method = $this->getActionMethod($action);
11✔
343
            // TODO 和 forward 异常合并一起处理
344
            try {
345
                $args = $this->buildActionArgs($instance, $method);
11✔
346
                $response = $instance->{$method}(...$args);
10✔
347
            } catch (RetException $e) {
1✔
348
                return $e->getRet();
×
349
            }
350

351
            $instance->after($this->req, $response);
10✔
352

353
            return $response;
10✔
354
        };
14✔
355

356
        $next = static function () use (&$middleware, &$next, $callback, $wei, $instance) {
14✔
357
            $config = array_splice($middleware, 0, 1);
14✔
358
            if ($config) {
14✔
359
                $class = key($config);
3✔
360
                $service = new $class(['wei' => $wei] + $config[$class]);
3✔
361
                $result = $service($next, $instance);
3✔
362
            } else {
363
                $result = $callback();
11✔
364
            }
365

366
            return $result;
13✔
367
        };
14✔
368

369
        return $this->handleResponse($next())->send();
14✔
370
    }
371

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

386
        $args = [];
6✔
387
        foreach ($params as $param) {
6✔
388
            $args[] = $this->buildActionArg($param);
6✔
389
        }
390
        return $args;
5✔
391
    }
392

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

404
        // Handle Model class
405
        if (
406
            $type
6✔
407
            && !$type->isBuiltin()
6✔
408
            && is_a($type->getName(), BaseModel::class, true)
6✔
409
        ) {
410
            return $type->getName()::findOrFail($this->req['id']);
1✔
411
        }
412

413
        // Handle other class
414
        if ($type && !$type->isBuiltin()) {
5✔
415
            throw new \Exception('Unsupported action parameter type: ' . $type);
×
416
        }
417

418
        // TODO Throw exception for unsupported builtin type
419
        // Handle builtin type
420
        $arg = $this->req[$param->getName()];
5✔
421
        if (null === $arg) {
5✔
422
            if ($param->isDefaultValueAvailable()) {
3✔
423
                $arg = $param->getDefaultValue();
2✔
424
            } else {
425
                throw new \Exception('Missing required parameter: ' . $param->getName(), 400);
3✔
426
            }
427
        } elseif ($type) {
3✔
428
            settype($arg, $type->getName());
2✔
429
        }
430

431
        return $arg;
4✔
432
    }
433

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

446
        // 2. Request parameter
447
        if ($id = $this->req->get('appId')) {
1✔
448
            return $id;
×
449
        }
450

451
        // 3. First id from database
452
        return $this->cache->remember('app:firstId', 86400, static function () {
1✔
453
            return AppModel::select('id')->asc('id')->fetchColumn();
×
454
        });
1✔
455
    }
456

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

470
        if (in_array($domain, $this->domains, true)) {
1✔
471
            return null;
×
472
        }
473

474
        return $this->cache->remember('appDomain:' . $domain, 86400, static function () use ($domain) {
1✔
475
            $app = AppModel::select('id')->fetch('domain', $domain);
1✔
476
            return $app ? $app['id'] : null;
1✔
477
        });
1✔
478
    }
479

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

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

501
        if ($this->fallbackPathInfo) {
×
502
            return $this->pageRouter->match($this->fallbackPathInfo);
×
503
        }
504

505
        return null;
×
506
    }
507

508
    /**
509
     * 根据请求设置跨域标头信息
510
     *
511
     * @return void
512
     * @internal
513
     */
514
    public function prepareHeaders()
515
    {
516
        $this->res->setHeader('Access-Control-Allow-Origin', $this->accessControlAllowOrigin);
×
517
        if ($this->req->isPreflight()) {
×
518
            $this->res
×
519
                ->setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
×
520
                // NOTE: antd upload 组件上传会加上 XMLHttpRequest 头
×
521
                ->setHeader('Access-Control-Allow-Headers', 'Origin, Content-Type, Authorization, X-Requested-With')
×
522
                ->setHeader('Access-Control-Max-Age', 0);
×
523
        }
524
    }
525
}
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

© 2026 Coveralls, Inc