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

miaoxing / plugin / 3766633246

pending completion
3766633246

push

github

twinh
feat(plugin): `BasePage` 增加 `pageInit` 事件

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

84 existing lines in 24 files now uncovered.

902 of 2275 relevant lines covered (39.65%)

19.29 hits per line

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

68.89
/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 METHOD_NOT_ALLOWED = 405;
28

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

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

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

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

57
    /**
58
     * @var string
59
     */
60
    protected $fallbackPathInfo = 'app';
61

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

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

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

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

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

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

103
        // Load global config
104
        $this->config->preloadGlobal();
×
105

106
        $this->event->trigger('appInit');
×
107

108
        return $this->invokeApp($options);
×
109
    }
110

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

119
        $plugin = $this->getPlugin();
9✔
120

121
        return $plugin ? '@' . $plugin . '/' . $file : $file;
9✔
122
    }
123

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

135
    /**
136
     * 获取当前控制器下的视图文件,即可省略当前插件和控制器名称不写
137
     *
138
     * @param string $action
139
     * @return string
140
     * @deprecated
141
     */
142
    public function getControllerFile($action)
143
    {
144
        return $this->view->getFile($this->getDefaultTemplate(null, $action));
×
145
    }
146

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

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

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

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

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

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

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

252
    /**
253
     * 判断是否为API接口
254
     *
255
     * @return bool
256
     * @deprecated
257
     */
258
    public function isApi()
259
    {
260
        $pathInfo = $this->req->getRouterPathInfo();
×
261
        return 0 === strpos($pathInfo, '/api') || 0 === strpos($pathInfo, '/admin-api');
×
262
    }
263

264
    /**
265
     * 设置默认视图文件
266
     *
267
     * @param string $defaultViewFile
268
     * @return $this
269
     */
270
    public function setDefaultViewFile($defaultViewFile)
271
    {
272
        $this->defaultViewFile = $defaultViewFile;
9✔
273
        return $this;
9✔
274
    }
275

276
    /**
277
     * Returns the method name of specified acion
278
     *
279
     * @param string $action
280
     * @return string
281
     */
282
    public function getActionMethod($action)
283
    {
284
        return $action;
39✔
285
    }
286

287
    /**
288
     * Returns whether the application is in demo mode
289
     *
290
     * @return bool
291
     * @svc
292
     */
293
    protected function isDemo(): bool
294
    {
295
        return $this->isDemo;
×
296
    }
297

298
    protected function invokeApp(array $options = [])
299
    {
300
        $options && $this->setOption($options);
×
301

302
        $pathInfo = $this->req->getRouterPathInfo();
×
303
        $result = $this->pageRouter->match($pathInfo);
×
304
        if (!$result) {
×
305
            $result = $this->pageRouter->match($this->fallbackPathInfo);
×
306
        }
307

308
        $this->req->set($result['params']);
×
309
        $page = require $result['file'];
×
310

311
        $this->page = [
×
312
            'file' => $result['file'],
×
UNCOV
313
            'page' => $page,
×
314
        ];
315

316
        if ($this->req->isPreflight()) {
×
317
            return $this->res->send();
×
318
        }
319

320
        $method = $this->req->getMethod();
×
321
        if (!method_exists($page, $method)) {
×
322
            $this->res->setStatusCode(static::METHOD_NOT_ALLOWED);
×
323
            throw new \Exception('Method Not Allowed', static::METHOD_NOT_ALLOWED);
×
324
        }
325

326
        return $this->execute($page, $method);
×
327
    }
328

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

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

342
        $callback = function () use ($instance, $action) {
32✔
343
            $instance->before($this->req, $this->res);
39✔
344

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

354
            $instance->after($this->req, $response);
36✔
355

356
            return $response;
36✔
357
        };
48✔
358

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

369
            return $result;
45✔
370
        };
48✔
371

372
        return $this->handleResponse($next())->send();
48✔
373
    }
374

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

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

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

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

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

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

434
        return $arg;
12✔
435
    }
436

437
    /**
438
     * 转换Ret结构为response
439
     *
440
     * @param array|Ret $ret
441
     * @return Res
442
     * @throws Exception
443
     */
444
    protected function handleRet($ret)
445
    {
446
        if (is_array($ret)) {
12✔
447
            if (1 === $ret['code']) {
3✔
448
                $ret = Ret::suc($ret);
3✔
449
            } else {
450
                $ret = Ret::err($ret);
×
451
            }
452
        }
453
        return $ret->toRes($this->req, $this->res);
12✔
454
    }
455

456
    /**
457
     * 检查是否返回了Ret结构
458
     *
459
     * @param mixed $response
460
     * @return bool
461
     */
462
    protected function isRet($response)
463
    {
464
        return is_array($response)
36✔
465
            && array_key_exists('code', $response)
36✔
466
            && array_key_exists('message', $response);
36✔
467
    }
468

469
    /**
470
     * Detect the id of application
471
     *
472
     * @return string
473
     */
474
    protected function detectId(): string
475
    {
476
        // 1. Domain
477
        if ($id = $this->getIdByDomain()) {
3✔
478
            return $id;
3✔
479
        }
480

481
        // 2. Request parameter
482
        if ($id = $this->req->get('appId')) {
3✔
483
            return $id;
×
484
        }
485

486
        // 3. First id from database
487
        return $this->cache->remember('app:firstId', 86400, static function () {
2✔
488
            return AppModel::select('id')->asc('id')->fetchColumn();
×
489
        });
3✔
490
    }
491

492
    /**
493
     * 根据域名查找应用编号
494
     *
495
     * @return string|null
496
     */
497
    protected function getIdByDomain(): ?string
498
    {
499
        $domain = $this->req->getHost();
3✔
500
        if (!$domain) {
3✔
501
            // CLI 下默认没有域名,直接返回
502
            return null;
3✔
503
        }
504

505
        if (in_array($domain, $this->domains, true)) {
3✔
506
            return null;
×
507
        }
508

509
        return $this->cache->remember('appDomain:' . $domain, 86400, static function () use ($domain) {
2✔
510
            $app = AppModel::select('id')->fetch('domain', $domain);
3✔
511
            return $app ? $app['id'] : null;
3✔
512
        });
3✔
513
    }
514

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