|
|
@@ -0,0 +1,182 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace Webkul\BagistoApi\Http\Middleware;
|
|
|
+
|
|
|
+use Closure;
|
|
|
+use Illuminate\Http\Request;
|
|
|
+use Symfony\Component\HttpFoundation\Response;
|
|
|
+
|
|
|
+/**
|
|
|
+ * WrapApiResponse
|
|
|
+ *
|
|
|
+ * 将评论相关 REST 接口的响应统一包装为 ApiResponse 格式(与
|
|
|
+ * Longyi\RewardPoints\Helpers\ApiResponse 输出结构保持一致):
|
|
|
+ *
|
|
|
+ * 成功:
|
|
|
+ * {
|
|
|
+ * "success": true,
|
|
|
+ * "message": "Success",
|
|
|
+ * "data": [ ... ],
|
|
|
+ * "pagination": { ... } // 仅分页列表接口
|
|
|
+ * }
|
|
|
+ *
|
|
|
+ * 失败:
|
|
|
+ * {
|
|
|
+ * "success": false,
|
|
|
+ * "message": "...",
|
|
|
+ * "data": { ... }
|
|
|
+ * }
|
|
|
+ */
|
|
|
+class WrapApiResponse
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * 需要精确匹配的路径结尾(用于避免误伤 products 下的其它接口)。
|
|
|
+ */
|
|
|
+ private const REVIEW_PATH_SUFFIX = '/reviews';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Handle an incoming request.
|
|
|
+ */
|
|
|
+ public function handle(Request $request, Closure $next): Response
|
|
|
+ {
|
|
|
+ $response = $next($request);
|
|
|
+
|
|
|
+ if (! $this->shouldWrap($request)) {
|
|
|
+ return $response;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 仅处理 JSON 响应
|
|
|
+ if (! $this->isJsonResponse($response)) {
|
|
|
+ return $response;
|
|
|
+ }
|
|
|
+
|
|
|
+ $body = $this->decodeJson($response->getContent());
|
|
|
+
|
|
|
+ if ($body === null) {
|
|
|
+ return $response;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($response->isSuccessful()) {
|
|
|
+ $wrapped = $this->wrapSuccess($request, $body);
|
|
|
+ } else {
|
|
|
+ $wrapped = $this->wrapError($response, $body);
|
|
|
+ }
|
|
|
+
|
|
|
+ $response->setContent(json_encode($wrapped, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
|
|
+
|
|
|
+ return $response;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 包装成功响应。
|
|
|
+ */
|
|
|
+ private function wrapSuccess(Request $request, mixed $body): array
|
|
|
+ {
|
|
|
+ $wrapped = [
|
|
|
+ 'success' => true,
|
|
|
+ 'message' => 'Success',
|
|
|
+ 'data' => $this->toSnakeCase($body),
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 分页信息由 PaginationHeaderNormalizer 暂存在 request attributes 中
|
|
|
+ $meta = $request->attributes->get('bagistoapi.pagination');
|
|
|
+
|
|
|
+ if (is_array($meta)) {
|
|
|
+ $wrapped['pagination'] = [
|
|
|
+ 'current_page' => (int) $meta['page'],
|
|
|
+ 'per_page' => (int) $meta['per_page'],
|
|
|
+ 'total' => (int) $meta['total'],
|
|
|
+ 'last_page' => (int) $meta['total_pages'],
|
|
|
+ 'has_more' => $meta['has_next'] ?? null,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ return $wrapped;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 包装错误响应。
|
|
|
+ */
|
|
|
+ private function wrapError(Response $response, mixed $body): array
|
|
|
+ {
|
|
|
+ // Api Platform 错误响应通常为 RFC 7807 格式,detail 字段存放具体错误信息
|
|
|
+ $message = 'Error';
|
|
|
+
|
|
|
+ if (is_array($body)) {
|
|
|
+ $message = $body['detail']
|
|
|
+ ?? $body['message']
|
|
|
+ ?? $body['title']
|
|
|
+ ?? 'Error';
|
|
|
+ } elseif (is_string($body) && $body !== '') {
|
|
|
+ $message = $body;
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => false,
|
|
|
+ 'message' => $message,
|
|
|
+ 'data' => $this->toSnakeCase($body),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 递归将数组 key 转为 snake_case(与 ApiResponse 输出保持一致)。
|
|
|
+ */
|
|
|
+ private function toSnakeCase(mixed $data): mixed
|
|
|
+ {
|
|
|
+ if (! is_array($data)) {
|
|
|
+ return $data;
|
|
|
+ }
|
|
|
+
|
|
|
+ $result = [];
|
|
|
+
|
|
|
+ foreach ($data as $key => $value) {
|
|
|
+ $snakeKey = strtolower((string) preg_replace('/(?<!^)[A-Z]/', '_$0', (string) $key));
|
|
|
+
|
|
|
+ $result[$snakeKey] = is_array($value) ? $this->toSnakeCase($value) : $value;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断当前请求是否需要包装。
|
|
|
+ */
|
|
|
+ private function shouldWrap(Request $request): bool
|
|
|
+ {
|
|
|
+ $path = $request->path();
|
|
|
+
|
|
|
+ // 直接以 /reviews 开头的评论接口(列表/单条/创建/更新/删除)
|
|
|
+ if (str_starts_with($path, 'api/shop/reviews')) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 商品子资源评论列表:/api/shop/products/{productId}/reviews
|
|
|
+ if (str_starts_with($path, 'api/shop/products/')
|
|
|
+ && str_ends_with($path, self::REVIEW_PATH_SUFFIX)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断响应是否为 JSON。
|
|
|
+ */
|
|
|
+ private function isJsonResponse(Response $response): bool
|
|
|
+ {
|
|
|
+ $contentType = $response->headers->get('Content-Type', '');
|
|
|
+
|
|
|
+ return str_contains($contentType, 'application/json')
|
|
|
+ || str_contains($contentType, 'application/merge-patch+json');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解码 JSON 内容,失败时返回 null。
|
|
|
+ */
|
|
|
+ private function decodeJson(string $content): mixed
|
|
|
+ {
|
|
|
+ $decoded = json_decode($content, true);
|
|
|
+
|
|
|
+ return json_last_error() === JSON_ERROR_NONE ? $decoded : null;
|
|
|
+ }
|
|
|
+}
|