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

miaoxing / plugin / 4804448060

pending completion
4804448060

push

github

twinh
feat(plugin): 页面不存在时,默认展示 404

0 of 3 new or added lines in 1 file covered. (0.0%)

855 of 2264 relevant lines covered (37.77%)

18.44 hits per line

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

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

3
namespace Miaoxing\Plugin\Service;
4

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

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

29
    protected const METHOD_NOT_ALLOWED = 405;
30

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

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

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

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

59
    /**
60
     * @var string|null
61
     */
62
    protected $fallbackPathInfo;
63

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

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

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

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

91
    /**
92
     * @var array
93
     */
94
    private $page = [
95
        'file' => '',
96
    ];
97

98
    /**
99
     * {@inheritdoc}
100
     */
101
    public function __invoke(array $options = [])
102
    {
103
        $this->prepareHeaders();
×
104

105
        // Load global config
106
        $this->config->preloadGlobal();
×
107

108
        $this->event->trigger('appInit');
×
109

110
        return $this->invokeApp($options);
×
111
    }
112

113
    /**
114
     * {@inheritdoc}
115
     */
116
    public function getDefaultTemplate($controller = null, $action = null)
117
    {
118
        $file = $controller ?: $this->page['file'];
9✔
119
        $file = dirname($file) . '/_' . basename($file);
9✔
120

121
        $plugin = $this->getPlugin();
9✔
122

123
        return $plugin ? '@' . $plugin . '/' . $file : $file;
9✔
124
    }
125

126
    /**
127
     * 获取当前插件下的视图文件,即可省略当前插件名称不写
128
     *
129
     * @param string $name
130
     * @return string
131
     */
132
    public function getPluginFile($name)
133
    {
134
        return $this->view->getFile('@' . $this->getPlugin() . '/' . $name);
×
135
    }
136

137
    /**
138
     * 获取当前运行的插件名称
139
     *
140
     * @return string
141
     */
142
    public function getPlugin()
143
    {
144
        if (!$this->plugin && $this->page['file']) {
9✔
145
            // 认为第二部分是插件名称
146
            list(, $plugin) = explode('/', $this->page['file'], 3);
×
147
            $this->plugin = $plugin;
×
148
        }
149
        return $this->plugin;
9✔
150
    }
151

152
    /**
153
     * Return the current application model object
154
     *
155
     * @return AppModel
156
     * @throws Exception When the application not found
157
     */
158
    public function getModel(): AppModel
159
    {
160
        $id = $this->getId();
3✔
161
        if (!isset($this->models[$id])) {
3✔
162
            $model = AppModel::new();
3✔
163
            $this->models[$id] = $model
3✔
164
                ->setCacheKey($model->getModelCacheKey($id))
3✔
165
                ->setCacheTime(86400)
3✔
166
                ->findOrFail($id);
3✔
167
        }
168
        return $this->models[$id];
3✔
169
    }
170

171
    /**
172
     * Set the current application model object
173
     *
174
     * @param AppModel|null $model
175
     * @return $this
176
     */
177
    public function setModel(?AppModel $model): self
178
    {
179
        $this->models[$this->getId()] = $model;
3✔
180
        return $this;
3✔
181
    }
182

183
    /**
184
     * Set the id of the current application
185
     *
186
     * @param string|null $id
187
     * @return $this
188
     */
189
    public function setId(?string $id): self
190
    {
191
        $this->id = $id;
3✔
192
        return $this;
3✔
193
    }
194

195
    /**
196
     * Return the id of the current application
197
     *
198
     * @return string
199
     */
200
    public function getId(): string
201
    {
202
        if (!$this->id) {
288✔
203
            $this->id = $this->detectId();
3✔
204
        }
205
        return $this->id;
288✔
206
    }
207

208
    /**
209
     * 重写handleResponse,支持Ret结构
210
     *
211
     * @param mixed $response
212
     * @return Res
213
     * @throws Exception
214
     */
215
    public function handleResponse($response)
216
    {
217
        if ($response instanceof Ret || $this->isRet($response)) {
45✔
218
            return $this->handleRet($response);
12✔
219
        } elseif ($response instanceof JsonSerializable) {
33✔
220
            return $this->res->json($response);
×
221
        } elseif (is_array($response)) {
33✔
222
            $template = $this->getDefaultTemplate();
9✔
223
            $file = $this->view->resolveFile($template) ? $template : $this->defaultViewFile;
9✔
224
            $content = $this->view->render($file, $response);
9✔
225
            return $this->res->setContent($content);
9✔
226
        } else {
227
            return parent::handleResponse($response);
24✔
228
        }
229
    }
230

231
    /**
232
     * 判断是否请求到后台页面
233
     *
234
     * @return bool
235
     */
236
    public function isAdmin()
237
    {
238
        // NOTE: 控制器不存在时,回退的控制器不带有 admin
239
        return 0 === strpos($this->req->getRouterPathInfo(), '/admin');
×
240
    }
241

242
    /**
243
     * 设置默认视图文件
244
     *
245
     * @param string $defaultViewFile
246
     * @return $this
247
     */
248
    public function setDefaultViewFile($defaultViewFile)
249
    {
250
        $this->defaultViewFile = $defaultViewFile;
9✔
251
        return $this;
9✔
252
    }
253

254
    /**
255
     * Returns the method name of specified acion
256
     *
257
     * @param string $action
258
     * @return string
259
     */
260
    public function getActionMethod($action)
261
    {
262
        return $action;
39✔
263
    }
264

265
    /**
266
     * Returns whether the application is in demo mode
267
     *
268
     * @return bool
269
     * @svc
270
     */
271
    protected function isDemo(): bool
272
    {
273
        return $this->isDemo;
×
274
    }
275

276
    protected function invokeApp(array $options = [])
277
    {
278
        $options && $this->setOption($options);
×
279

280
        $pathInfo = $this->req->getRouterPathInfo();
×
281
        $result = $this->pageRouter->match($pathInfo);
×
282
        if (!$result) {
×
NEW
283
            if ($this->fallbackPathInfo) {
×
NEW
284
                $result = $this->pageRouter->match($this->fallbackPathInfo);
×
285
            } else {
NEW
286
                throw new \Exception('Not Found', static::NOT_FOUND);
×
287
            }
288
        }
289

290
        $this->req->set($result['params']);
×
291
        $page = require $result['file'];
×
292

293
        $this->page = [
×
294
            'file' => $result['file'],
×
295
            'page' => $page,
×
296
        ];
297

298
        if ($this->req->isPreflight()) {
×
299
            return $this->res->send();
×
300
        }
301

302
        $method = strtolower($this->req->getMethod());
×
303
        if (!method_exists($page, $method)) {
×
304
            $this->res->setStatusCode(static::METHOD_NOT_ALLOWED);
×
305
            throw new \Exception('Method Not Allowed', static::METHOD_NOT_ALLOWED);
×
306
        }
307

308
        return $this->execute($page, $method);
×
309
    }
310

311
    /**
312
     * @param BaseController $instance
313
     * @param string $action
314
     * @return Res
315
     * @throws Exception
316
     */
317
    protected function execute($instance, $action)
318
    {
319
        $wei = $this->wei;
48✔
320

321
        $instance->init();
48✔
322
        $middleware = $this->getMiddleware($instance, $action);
48✔
323

324
        $callback = function () use ($instance, $action) {
32✔
325
            $instance->before($this->req, $this->res);
39✔
326

327
            $method = $this->getActionMethod($action);
39✔
328
            // TODO 和 forward 异常合并一起处理
329
            try {
330
                $args = $this->buildActionArgs($instance, $method);
39✔
331
                $response = $instance->{$method}(...$args);
36✔
332
            } catch (RetException $e) {
3✔
333
                return $e->getRet();
×
334
            }
335

336
            $instance->after($this->req, $response);
36✔
337

338
            return $response;
36✔
339
        };
48✔
340

341
        $next = function () use (&$middleware, &$next, $callback, $wei, $instance) {
32✔
342
            $config = array_splice($middleware, 0, 1);
48✔
343
            if ($config) {
48✔
344
                $class = key($config);
9✔
345
                $service = new $class(['wei' => $wei] + $config[$class]);
9✔
346
                $result = $service($next, $instance);
9✔
347
            } else {
348
                $result = $callback();
39✔
349
            }
350

351
            return $result;
45✔
352
        };
48✔
353

354
        return $this->handleResponse($next())->send();
48✔
355
    }
356

357
    /**
358
     * @param object $instance
359
     * @param string $method
360
     * @return array
361
     * @throws ReflectionException
362
     */
363
    protected function buildActionArgs($instance, string $method)
364
    {
365
        $ref = new ReflectionMethod($instance, $method);
39✔
366
        $params = $ref->getParameters();
39✔
367
        if (!$params || 'req' === $params[0]->getName()) {
39✔
368
            return [$this->req, $this->res];
21✔
369
        }
370

371
        $args = [];
18✔
372
        foreach ($params as $param) {
18✔
373
            $args[] = $this->buildActionArg($param);
18✔
374
        }
375
        return $args;
15✔
376
    }
377

378
    /**
379
     * @param ReflectionParameter $param
380
     * @return mixed
381
     * @throws ReflectionException
382
     */
383
    protected function buildActionArg(ReflectionParameter $param)
384
    {
385
        /** @link https://github.com/phpstan/phpstan/issues/1133 */
386
        /** @var \ReflectionNamedType|null $type */
387
        $type = $param->getType();
18✔
388

389
        // Handle Model class
390
        if (
391
            $type
18✔
392
            && !$type->isBuiltin()
18✔
393
            && is_a($type->getName(), BaseModel::class, true)
18✔
394
        ) {
395
            return $type->getName()::findOrFail($this->req['id']);
3✔
396
        }
397

398
        // Handle other class
399
        if ($type && !$type->isBuiltin()) {
15✔
400
            throw new Exception('Unsupported action parameter type: ' . $type);
×
401
        }
402

403
        // TODO Throw exception for unsupported builtin type
404
        // Handle builtin type
405
        $arg = $this->req[$param->getName()];
15✔
406
        if (null === $arg) {
15✔
407
            if ($param->isDefaultValueAvailable()) {
9✔
408
                $arg = $param->getDefaultValue();
6✔
409
            } else {
410
                throw new Exception('Missing required parameter: ' . $param->getName(), 400);
9✔
411
            }
412
        } elseif ($type) {
9✔
413
            settype($arg, $type->getName());
6✔
414
        }
415

416
        return $arg;
12✔
417
    }
418

419
    /**
420
     * 转换Ret结构为response
421
     *
422
     * @param array|Ret $ret
423
     * @return Res
424
     * @throws Exception
425
     */
426
    protected function handleRet($ret)
427
    {
428
        if (is_array($ret)) {
12✔
429
            if (1 === $ret['code']) {
3✔
430
                $ret = Ret::suc($ret);
3✔
431
            } else {
432
                $ret = Ret::err($ret);
×
433
            }
434
        }
435
        return $ret->toRes($this->req, $this->res);
12✔
436
    }
437

438
    /**
439
     * 检查是否返回了Ret结构
440
     *
441
     * @param mixed $response
442
     * @return bool
443
     */
444
    protected function isRet($response)
445
    {
446
        return is_array($response)
36✔
447
            && array_key_exists('code', $response)
36✔
448
            && array_key_exists('message', $response);
36✔
449
    }
450

451
    /**
452
     * Detect the id of application
453
     *
454
     * @return string
455
     */
456
    protected function detectId(): string
457
    {
458
        // 1. Domain
459
        if ($id = $this->getIdByDomain()) {
3✔
460
            return $id;
3✔
461
        }
462

463
        // 2. Request parameter
464
        if ($id = $this->req->get('appId')) {
3✔
465
            return $id;
×
466
        }
467

468
        // 3. First id from database
469
        return $this->cache->remember('app:firstId', 86400, static function () {
2✔
470
            return AppModel::select('id')->asc('id')->fetchColumn();
×
471
        });
3✔
472
    }
473

474
    /**
475
     * 根据域名查找应用编号
476
     *
477
     * @return string|null
478
     */
479
    protected function getIdByDomain(): ?string
480
    {
481
        $domain = $this->req->getHost();
3✔
482
        if (!$domain) {
3✔
483
            // CLI 下默认没有域名,直接返回
484
            return null;
3✔
485
        }
486

487
        if (in_array($domain, $this->domains, true)) {
3✔
488
            return null;
×
489
        }
490

491
        return $this->cache->remember('appDomain:' . $domain, 86400, static function () use ($domain) {
2✔
492
            $app = AppModel::select('id')->fetch('domain', $domain);
3✔
493
            return $app ? $app['id'] : null;
3✔
494
        });
3✔
495
    }
496

497
    /**
498
     * 根据请求设置跨域标头信息
499
     *
500
     * @return void
501
     * @internal
502
     */
503
    public function prepareHeaders()
504
    {
505
        $this->res->setHeader('Access-Control-Allow-Origin', $this->accessControlAllowOrigin);
×
506
        if ($this->req->isPreflight()) {
×
507
            $this->res
×
508
                ->setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
×
509
                // NOTE: antd upload 组件上传会加上 XMLHttpRequest 头
510
                ->setHeader('Access-Control-Allow-Headers', 'Origin, Content-Type, Authorization, X-Requested-With')
×
511
                ->setHeader('Access-Control-Max-Age', 0);
×
512
        }
513
    }
514
}
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