ProductQuestion.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. <?php
  2. namespace Webkul\BagistoApi\Models;
  3. use ApiPlatform\Metadata\ApiResource;
  4. use ApiPlatform\Metadata\Delete;
  5. use ApiPlatform\Metadata\Get;
  6. use ApiPlatform\Metadata\GetCollection;
  7. use ApiPlatform\Metadata\Link;
  8. use ApiPlatform\Metadata\Patch;
  9. use ApiPlatform\Metadata\Post;
  10. use ApiPlatform\Metadata\GraphQl\DeleteMutation;
  11. use ApiPlatform\Metadata\GraphQl\Mutation;
  12. use ApiPlatform\Metadata\GraphQl\Query;
  13. use ApiPlatform\Metadata\GraphQl\QueryCollection;
  14. use ApiPlatform\OpenApi\Model\Operation;
  15. use Webkul\BagistoApi\Dto\CreateProductQuestionInput;
  16. use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
  17. use Webkul\BagistoApi\State\ProductQuestionProvider;
  18. use Webkul\BagistoApi\State\ProductQuestionProcessor;
  19. /**
  20. * Product Q&A — Questions resource
  21. *
  22. * Storefront: list approved questions, post new questions.
  23. * Admin: update status (approve/reject), delete.
  24. */
  25. #[ApiResource(
  26. routePrefix: '/api/shop',
  27. shortName: 'ProductQuestion',
  28. uriTemplate: '/questions',
  29. operations: [
  30. new GetCollection(
  31. uriTemplate: '/questions',
  32. provider: ProductQuestionProvider::class,
  33. openapi: new Operation(
  34. tags: ['Product Q&A'],
  35. summary: 'List product questions',
  36. description: 'Returns product questions. Defaults to `approved` status. Supports `product_id`, `status`, `page`, `per_page` filters.',
  37. parameters: [
  38. new \ApiPlatform\OpenApi\Model\Parameter(name: 'product_id', in: 'query', required: false, schema: ['type' => 'integer']),
  39. new \ApiPlatform\OpenApi\Model\Parameter(name: 'status', in: 'query', required: false, schema: ['type' => 'string', 'enum' => ['pending', 'approved', 'rejected'], 'default' => 'approved']),
  40. new \ApiPlatform\OpenApi\Model\Parameter(name: 'page', in: 'query', required: false, schema: ['type' => 'integer', 'default' => 1]),
  41. new \ApiPlatform\OpenApi\Model\Parameter(name: 'per_page', in: 'query', required: false, schema: ['type' => 'integer', 'default' => 30, 'maximum' => 50]),
  42. new \ApiPlatform\OpenApi\Model\Parameter(
  43. name: 'with_answers', in: 'query', required: false,
  44. description: 'Embed top approved answers inside each question. Default: `true`. Pass `false` to skip.',
  45. schema: ['type' => 'boolean', 'default' => true],
  46. ),
  47. new \ApiPlatform\OpenApi\Model\Parameter(
  48. name: 'answers_per_question', in: 'query', required: false,
  49. description: 'How many answers to embed per question (1–20). Default: 5. Pinned answers appear first, then sorted by `useful_count` descending.',
  50. schema: ['type' => 'integer', 'default' => 5, 'minimum' => 1, 'maximum' => 20],
  51. ),
  52. new \ApiPlatform\OpenApi\Model\Parameter(
  53. name: 'answer_status', in: 'query', required: false,
  54. description: 'Status of embedded answers to return. Default: `approved`.',
  55. schema: ['type' => 'string', 'enum' => ['approved', 'pending', 'rejected'], 'default' => 'approved'],
  56. ),
  57. ],
  58. ),
  59. ),
  60. new Get(
  61. uriTemplate: '/questions/{id}',
  62. openapi: new Operation(
  63. tags: ['Product Q&A'],
  64. summary: 'Get a single question by ID',
  65. ),
  66. ),
  67. new Post(
  68. uriTemplate: '/questions',
  69. processor: ProductQuestionProcessor::class,
  70. denormalizationContext: ['groups' => ['mutation'], 'allow_extra_attributes' => true],
  71. openapi: new Operation(
  72. tags: ['Product Q&A'],
  73. summary: 'Submit a product question',
  74. description: 'Submit a question about a product. **Requires authentication** (Bearer token). New questions start in `pending` status pending admin approval.',
  75. requestBody: new \ApiPlatform\OpenApi\Model\RequestBody(
  76. required: true,
  77. content: new \ArrayObject([
  78. 'application/json' => [
  79. 'schema' => [
  80. 'type' => 'object',
  81. 'required' => ['product_id', 'question', 'customer_name'],
  82. 'properties' => [
  83. 'product_id' => ['type' => 'integer', 'example' => 1],
  84. 'question' => ['type' => 'string', 'example' => 'Does this come in blue?'],
  85. 'customer_name' => ['type' => 'string', 'example' => 'Jane Doe'],
  86. 'customer_email'=> ['type' => 'string', 'format' => 'email', 'example' => 'jane@example.com'],
  87. ],
  88. ],
  89. ],
  90. ]),
  91. ),
  92. responses: [
  93. '201' => new \ApiPlatform\OpenApi\Model\Response(description: 'Question submitted. Starts in `pending` status.'),
  94. '422' => new \ApiPlatform\OpenApi\Model\Response(description: 'Validation error.'),
  95. ],
  96. ),
  97. ),
  98. new Patch(
  99. uriTemplate: '/questions/{id}',
  100. processor: ProductQuestionProcessor::class,
  101. denormalizationContext: ['groups' => ['mutation'], 'allow_extra_attributes' => true],
  102. openapi: new Operation(
  103. tags: ['Product Q&A — Admin'],
  104. summary: 'Update question status (admin)',
  105. description: 'Approve or reject a question. Intended for admin use.',
  106. requestBody: new \ApiPlatform\OpenApi\Model\RequestBody(
  107. required: true,
  108. content: new \ArrayObject([
  109. 'application/merge-patch+json' => [
  110. 'schema' => [
  111. 'type' => 'object',
  112. 'properties' => [
  113. 'status' => ['type' => 'string', 'enum' => ['pending', 'approved', 'rejected']],
  114. ],
  115. ],
  116. ],
  117. ]),
  118. ),
  119. ),
  120. ),
  121. new Delete(
  122. uriTemplate: '/questions/{id}',
  123. processor: ProductQuestionProcessor::class,
  124. openapi: new Operation(
  125. tags: ['Product Q&A — Admin'],
  126. summary: 'Delete a question',
  127. responses: [
  128. '204' => new \ApiPlatform\OpenApi\Model\Response(description: 'Deleted.'),
  129. '404' => new \ApiPlatform\OpenApi\Model\Response(description: 'Not found.'),
  130. ],
  131. ),
  132. ),
  133. ],
  134. graphQlOperations: [
  135. new QueryCollection(
  136. provider: ProductQuestionProvider::class,
  137. args: [
  138. 'product_id' => ['type' => 'Int', 'description' => 'Filter by product ID'],
  139. 'status' => ['type' => 'String', 'description' => 'Filter by status (approved/pending/rejected)'],
  140. 'first' => ['type' => 'Int'],
  141. 'last' => ['type' => 'Int'],
  142. 'after' => ['type' => 'String'],
  143. 'before' => ['type' => 'String'],
  144. ]
  145. ),
  146. new Query(resolver: BaseQueryItemResolver::class),
  147. new Mutation(
  148. name: 'create',
  149. input: CreateProductQuestionInput::class,
  150. output: self::class,
  151. processor: ProductQuestionProcessor::class,
  152. ),
  153. new Mutation(
  154. name: 'updateStatus',
  155. processor: ProductQuestionProcessor::class,
  156. description: 'Update question status (admin)',
  157. ),
  158. new DeleteMutation(
  159. name: 'delete',
  160. description: 'Delete a question',
  161. ),
  162. ]
  163. )]
  164. #[ApiResource(
  165. routePrefix: '/api/shop',
  166. shortName: 'ProductQuestion',
  167. uriTemplate: '/products/{productId}/questions',
  168. uriVariables: [
  169. 'productId' => new Link(
  170. fromClass: Product::class,
  171. fromProperty: 'questions',
  172. identifiers: ['id']
  173. ),
  174. ],
  175. operations: [
  176. new GetCollection(
  177. provider: ProductQuestionProvider::class,
  178. openapi: new Operation(
  179. tags: ['Product Q&A'],
  180. summary: 'List approved questions for a product',
  181. parameters: [
  182. new \ApiPlatform\OpenApi\Model\Parameter(name: 'status', in: 'query', required: false, schema: ['type' => 'string', 'default' => 'approved']),
  183. new \ApiPlatform\OpenApi\Model\Parameter(name: 'page', in: 'query', required: false, schema: ['type' => 'integer', 'default' => 1]),
  184. new \ApiPlatform\OpenApi\Model\Parameter(name: 'per_page', in: 'query', required: false, schema: ['type' => 'integer', 'default' => 30, 'maximum' => 50]),
  185. new \ApiPlatform\OpenApi\Model\Parameter(
  186. name: 'with_answers', in: 'query', required: false,
  187. description: 'Embed top approved answers inside each question. Default: `true`.',
  188. schema: ['type' => 'boolean', 'default' => true],
  189. ),
  190. new \ApiPlatform\OpenApi\Model\Parameter(
  191. name: 'answers_per_question', in: 'query', required: false,
  192. description: 'Max answers embedded per question (1–20). Default: 5.',
  193. schema: ['type' => 'integer', 'default' => 5, 'minimum' => 1, 'maximum' => 20],
  194. ),
  195. new \ApiPlatform\OpenApi\Model\Parameter(
  196. name: 'answer_status', in: 'query', required: false,
  197. description: 'Status of embedded answers. Default: `approved`.',
  198. schema: ['type' => 'string', 'enum' => ['approved', 'pending', 'rejected'], 'default' => 'approved'],
  199. ),
  200. ],
  201. ),
  202. ),
  203. ],
  204. graphQlOperations: []
  205. )]
  206. class ProductQuestion extends \Webkul\Product\Models\ProductQuestion
  207. {
  208. protected $fillable = [
  209. 'product_id',
  210. 'customer_id',
  211. 'customer_name',
  212. 'customer_email',
  213. 'question',
  214. 'status',
  215. 'answers_count',
  216. ];
  217. protected $casts = [
  218. 'id' => 'int',
  219. 'product_id' => 'int',
  220. 'customer_id' => 'int',
  221. 'answers_count' => 'int',
  222. 'question' => 'string',
  223. 'customer_name' => 'string',
  224. 'status' => 'string',
  225. 'created_at' => 'datetime',
  226. 'updated_at' => 'datetime',
  227. ];
  228. public function __get($key)
  229. {
  230. if ($this->hasAttribute($key)) {
  231. return $this->getAttribute($key);
  232. }
  233. return parent::__get($key);
  234. }
  235. public function __isset($key)
  236. {
  237. if ($this->hasAttribute($key)) {
  238. return true;
  239. }
  240. return parent::__isset($key);
  241. }
  242. public function __set($key, $value)
  243. {
  244. $own = ['id', 'product_id', 'customer_id', 'customer_name', 'customer_email', 'question', 'status', 'answers_count', 'created_at', 'updated_at'];
  245. if (in_array($key, $own)) {
  246. $this->setAttribute($key, $value);
  247. } else {
  248. parent::__set($key, $value);
  249. }
  250. }
  251. public function getId(): int
  252. {
  253. return (int) $this->getAttribute('id');
  254. }
  255. }