1 <?php
2
3 /* --------------------------------------------------------------
4 ProductsApiV2Controller.inc.php 2016-03-07
5 Gambio GmbH
6 http://www.gambio.de
7 Copyright (c) 2016 Gambio GmbH
8 Released under the GNU General Public License (Version 2)
9 [http://www.gnu.org/licenses/gpl-2.0.html]
10 --------------------------------------------------------------
11 */
12
13 MainFactory::load_class('HttpApiV2Controller');
14
15 /**
16 * Class ProductsApiV2Controller
17 *
18 * Provides a gateway to the ProductWriteService and ProductReadService classes, which handle the shop
19 * product resources.
20 *
21 * @category System
22 * @package ApiV2Controllers
23 */
24 class ProductsApiV2Controller extends HttpApiV2Controller
25 {
26 /**
27 * Product write service.
28 *
29 * @var ProductWriteService
30 */
31 protected $productWriteService;
32
33 /**
34 * Product read service.
35 *
36 * @var ProductReadService
37 */
38 protected $productReadService;
39
40 /**
41 * Product JSON serializer.
42 *
43 * @var ProductJsonSerializer
44 */
45 protected $productJsonSerializer;
46
47 /**
48 * Product list item JSON serializer.
49 *
50 * @var ProductListItemJsonSerializer
51 */
52 protected $productListItemJsonSerializer;
53
54 /**
55 * Sub resources.
56 *
57 * @var array
58 */
59 protected $subresource;
60
61
62 /**
63 * Initializes API Controller
64 */
65 protected function __initialize()
66 {
67 $this->productWriteService = StaticGXCoreLoader::getService('ProductWrite');
68 $this->productReadService = StaticGXCoreLoader::getService('ProductRead');
69 $this->productJsonSerializer = MainFactory::create('ProductJsonSerializer');
70 $this->productListItemJsonSerializer = MainFactory::create('ProductListItemJsonSerializer');
71 $this->subresource = array(
72 'links' => 'ProductsLinksApiV2Controller'
73 );
74 }
75
76
77 /**
78 * @api {post} /products Create Product
79 * @apiVersion 2.1.0
80 * @apiName CreateProduct
81 * @apiGroup Products
82 *
83 * @apiDescription
84 * Creates a new product record in the system. To see an example usage take a look at
85 * `docs/REST/samples/product-service/create_product.php`
86 *
87 * @apiParamExample {json} Request-Body
88 * {
89 * "isActive": false,
90 * "sortOrder": 0,
91 * "orderedCount": 1,
92 * "productModel": "ABC123",
93 * "ean": "",
94 * "price": 16.7983,
95 * "discountAllowed": 0,
96 * "taxClassId": 1,
97 * "quantity": 998,
98 * "weight": 0,
99 * "shippingCosts": 0,
100 * "shippingTimeId": 1,
101 * "productTypeId": 1,
102 * "manufacturerId": 0,
103 * "isFsk18": false,
104 * "isVpeActive": false,
105 * "vpeID": 0,
106 * "vpeValue": 0,
107 * "name": {
108 * "en": "test article",
109 * "de": "Testartikel"
110 * },
111 * "description": {
112 * "en": "[TAB:Page 1] Test Product Description (Page 1) [TAB: Page 2] Test Product Description (Page 2)",
113 * "de": "[TAB:Seite 1] Testartikel Beschreibung (Seite 1) [TAB:Seite 2] Testartikel Beschreibung (Seite 2)"
114 * },
115 * "shortDescription": {
116 * "en": "<p>Test product short description.</p>",
117 * "de": "<p>Testartikel Kurzbeschreibung</p>"
118 * },
119 * "keywords": {
120 * "en": "",
121 * "de": ""
122 * },
123 * "metaTitle": {
124 * "en": "",
125 * "de": ""
126 * },
127 * "metaDescription": {
128 * "en": "",
129 * "de": ""
130 * },
131 * "metaKeywords": {
132 * "en": "",
133 * "de": ""
134 * },
135 * "url": {
136 * "en": "",
137 * "de": ""
138 * },
139 * "urlKeywords": {
140 * "en": "test-article",
141 * "de": "Testartikel"
142 * },
143 * "checkoutInformation": {
144 * "en": "",
145 * "de": ""
146 * },
147 * "viewedCount": {
148 * "en": 0,
149 * "de": 32
150 * },
151 * "images": [
152 * {
153 * "filename": "artikelbild_1_1.jpg",
154 * "isPrimary": false,
155 * "isVisible": true,
156 * "imageAltText": {
157 * "en": "",
158 * "de": ""
159 * }
160 * },
161 * {
162 * "filename": "artikelbild_1_2.jpg",
163 * "isPrimary": false,
164 * "isVisible": true,
165 * "imageAltText": {
166 * "en": "",
167 * "de": ""
168 * }
169 * },
170 * {
171 * "filename": "artikelbild_1_3.jpg",
172 * "isPrimary": false,
173 * "isVisible": true,
174 * "imageAltText": {
175 * "en": "",
176 * "de": ""
177 * }
178 * }
179 * ],
180 * "settings": {
181 * "detailsTemplate": "standard.html",
182 * "optionsDetailsTemplate": "product_options_dropdown.html",
183 * "optionsListingTemplate": "product_options_dropdown.html",
184 * "showOnStartpage": false,
185 * "showQuantityInfo": true,
186 * "showWeight": false,
187 * "showPriceOffer": true,
188 * "showAddedDateTime": false,
189 * "priceStatus": 0,
190 * "minOrder": 1,
191 * "graduatedQuantity": 1,
192 * "onSitemap": true,
193 * "sitemapPriority": "0.5",
194 * "sitemapChangeFrequency": "daily",
195 * "propertiesDropdownMode": "dropdown_mode_1",
196 * "startpageSortOrder": 0,
197 * "showPropertiesPrice": true,
198 * "usePropertiesCombisQuantity": false,
199 * "usePropertiesCombisShippingTime": true,
200 * "usePropertiesCombisWeight": false
201 * },
202 * "addonValues": {
203 * "productsImageWidth": "0",
204 * "productsImageHeight": "0"
205 * }
206 * }
207 *
208 * @apiParam {Boolean} isActive Whether the product is active.
209 * @apiParam {Number} sortOrder The sort order of the product.
210 * @apiParam {Number} orderedCount How many times the product was ordered.
211 * @apiParam {String} productModel Product's Model.
212 * @apiParam {String} ean European Article Number.
213 * @apiParam {Number} price Product's Price as float value.
214 * @apiParam {Number} discountAllowed Percentage of the allowed discount as float value.
215 * @apiParam {Number} taxClassId The tax class ID.
216 * @apiParam {Number} quantity Quantity in stock as float value.
217 * @apiParam {Number} weight The weight of the product as float value.
218 * @apiParam {Number} shippingCosts Additional shipping costs as float value.
219 * @apiParam {Number} shippingTimeId Must match a record from the shipping time entries.
220 * @apiParam {Number} productTypeId Must match a record from the product type entries.
221 * @apiParam {Number} manufacturerId Must match the ID of the manufacturer record.
222 * @apiParam {Boolean} isFsk18 Whether the product is FSK18.
223 * @apiParam {Boolean} isVpeActive Whether VPE is active.
224 * @apiParam {Number} vpeID The VPE ID of the product.
225 * @apiParam {Number} vpeValue The VPE value of the product as float value.
226 * @apiParam {Object} name Language specific object with the product's name.
227 * @apiParam {Object} description Language specific object with the product's description.
228 * @apiParam {Object} shortDescription Language specific object with the product's short description.
229 * @apiParam {Object} keywords Language specific object with the product's keywords.
230 * @apiParam {Object} metaTitle Language specific object with the product's meta title.
231 * @apiParam {Object} metaDescription Language specific object with the product's meta description.
232 * @apiParam {Object} metaKeywords Language specific object with the product's meta keywords.
233 * @apiParam {Object} url Language specific object with the product's url.
234 * @apiParam {Object} urlKeywords Language specific object with the product's url keywords.
235 * @apiParam {Object} checkoutInformation Language specific object with the product's checkout information.
236 * @apiParam {Object} viewedCount Language specific object with the product's viewed count.
237 * @apiParam {Array} images Contains the product images information.
238 * @apiParam {String} images.filename The product image file name (provide only the file name and not the whole
239 * path).
240 * @apiParam {Boolean} images.isPrimary Whether the image is the primary one.
241 * @apiParam {Boolean} images.isVisible Whether the image will be visible.
242 * @apiParam {Object} images.imageAltText Language specific object with the image alternative text.
243 * @apiParam {Object} settings Contains various product settings.
244 * @apiParam {String} settings.detailsTemplate Filename of the details HTML template.
245 * @apiParam {String} settings.optionsDetailsTemplate Filename of the options details HTML template.
246 * @apiParam {String} settings.optionsListingTemplate Filename of the options listing HTML template.
247 * @apiParam {Boolean} settings.showOnStartpage Whether to show the product on startpage.
248 * @apiParam {Boolean} settings.showQuantityInfo Whether to show quantity information.
249 * @apiParam {Boolean} settings.showWeight Whether to show the products weight.
250 * @apiParam {Boolean} settings.showPriceOffer Whether to show price offer.
251 * @apiParam {Boolean} settings.showAddedDateTime Whether to show the creation date-time of the product.
252 * @apiParam {Number} settings.priceStatus Must match a record from the price status entries.
253 * @apiParam {Number} settings.minOrder The minimum order of the product.
254 * @apiParam {Number} settings.graduatedQuantity Product's graduated quantity.
255 * @apiParam {Boolean} settings.onSitemap Whether to include the product in the sitemap.
256 * @apiParam {String} settings.sitemapPriority The sitemap priority (provide a decimal value as a string).
257 * @apiParam {String} settings.sitemapChangeFrequency Possible values can contain the `always`, `hourly`, `daily`,
258 * `weekly`, `monthly`, `yearly`, `never`.
259 * @apiParam {String} settings.propertiesDropdownMode Provide one of the following values: "" >> Default - all
260 * values are always selectable, `dropdown_mode_1` >> Any order, only possible values are selectable,
261 * `dropdown_mode_2` >> Specified order, only possible values are selectable.
262 * @apiParam {Number} settings.startpageSortOrder The sort order in the startpage.
263 * @apiParam {Boolean} settings.showPropertiesPrice Whether to show properties price.
264 * @apiParam {Boolean} settings.usePropertiesCombisQuantity Whether to use properties combis quantitity.
265 * @apiParam {Boolean} settings.usePropertiesCombisShippingTime Whether to use properties combis shipping time.
266 * @apiParam {Boolean} settings.usePropertiesCombisWeight Whether to use properties combis weight.
267 * @apiParam {Object} addonValues Contains some extra addon values.
268 * @apiParam {String} addonValues.productsImageWidth The CSS product image width (might contain size metrics).
269 * @apiParam {String} addonValues.productsImageHeight The CSS product image height (might contain size metrics).
270 *
271 * @apiSuccess (Success 201) Response-Body If successful, this method returns a complete Product resource in the
272 * response body.
273 *
274 * @apiError 400-BadRequest The body of the request was empty.
275 *
276 * @apiErrorExample Error-Response
277 * HTTP/1.1 400 Bad Request
278 * {
279 * "code": 400,
280 * "status": "error",
281 * "message": "The body of the request was empty."
282 * }
283 */
284 public function post()
285 {
286 if($this->_mapResponse($this->subresource))
287 {
288 return;
289 }
290
291 $productJsonString = $this->api->request->getBody();
292
293 if(isset($this->uri[1]) && is_numeric($this->uri[1])) // Duplicate Product
294 {
295 $productJsonObject = json_decode($productJsonString);
296
297 if($productJsonObject->categoryId === null || !is_numeric($productJsonObject->categoryId))
298 {
299 $productJsonObject = new stdClass;
300 $productJsonObject->categoryId = 0; // Default category value.
301 }
302
303 $productId = $this->productWriteService->duplicateProduct(new IdType($this->uri[1]),
304 new IdType($productJsonObject->categoryId));
305 }
306 else // Create New Product
307 {
308 $product = $this->productJsonSerializer->deserialize($productJsonString);
309 $productId = $this->productWriteService->createProduct($product);
310 }
311
312 $storedProduct = $this->productReadService->getProductById(new IdType($productId));
313 $response = $this->productJsonSerializer->serialize($storedProduct, false);
314 $this->_linkResponse($response);
315 $this->_locateResource('products', $productId);
316 $this->_writeResponse($response, 201);
317 }
318
319
320 /**
321 * @api {put} /products/:id Update Product
322 * @apiVersion 2.1.0
323 * @apiName ProductCategory
324 * @apiGroup Products
325 *
326 * @apiDescription
327 * Use this method to update an existing product record. Take a look in the POST method for more detailed
328 * explanation on every resource property. To see an example usage consider
329 * `docs/REST/samples/product-service/update_product.php`
330 *
331 * @apiSuccess Response-Body If successful, this method returns the updated Product resource in the response body.
332 *
333 * @apiError 400-BadRequest Product data were not provided.
334 * @apiErrorExample Error-Response (No data)
335 * HTTP/1.1 400 Bad Request
336 * {
337 * "code": 400,
338 * "status": "error",
339 * "message": "Product data were not provided."
340 * }
341 *
342 * @todo Error status code on not found entries should be 404 and not 400.
343 */
344 public function put()
345 {
346 if($this->_mapResponse($this->subresource))
347 {
348 return;
349 }
350
351 if(!isset($this->uri[1]) || !is_numeric($this->uri[1]))
352 {
353 throw new HttpApiV2Exception('Product record ID was not provided or is invalid: ' . gettype($this->uri[1]),
354 400);
355 }
356
357 $productJsonString = $this->api->request->getBody();
358
359 if(empty($productJsonString))
360 {
361 throw new HttpApiV2Exception('Product data were not provided.', 400);
362 }
363
364 $productId = new IdType($this->uri[1]);
365
366 // Ensure that the product has the correct product id of the request url
367 $productJsonString = $this->_setJsonValue($productJsonString, 'id', $productId->asInt());
368
369 $product = $this->productJsonSerializer->deserialize($productJsonString,
370 $this->productReadService->getProductById($productId));
371
372 $this->productWriteService->updateProduct($product);
373
374 $response = $this->productJsonSerializer->serialize($this->productReadService->getProductById($productId),
375 false);
376 $this->_linkResponse($response);
377 $this->_writeResponse($response, 200);
378 }
379
380
381 /**
382 * @api {delete} /products/:id Delete Product
383 * @apiVersion 2.1.0
384 * @apiName DeleteProduct
385 * @apiGroup Products
386 *
387 * @apiDescription
388 * Removes a product record from the database. To see an example usage take a look at
389 * `docs/REST/samples/product-service/remove_product.php`
390 *
391 * @apiExample {curl} Delete Product With ID = 24
392 * curl -X DELETE --user admin@shop.de:12345 http://shop.de/api.php/v2/products/24
393 *
394 * @apiSuccessExample {json} Success-Response
395 * {
396 * "code": 200,
397 * "status": "success",
398 * "action": "delete",
399 * "resource": "Product",
400 * "productId": 24
401 * }
402 *
403 * @apiError 400-BadRequest Product record ID was not provided in the resource URL.
404 * @apiErrorExample Error-Response
405 * HTTP/1.1 400 Bad Request
406 * {
407 * "code": 400,
408 * "status": "error",
409 * "message": "Product record ID was not provided in the resource URL."
410 * }
411 */
412 public function delete()
413 {
414 if($this->_mapResponse($this->subresource))
415 {
416 return;
417 }
418
419 // Check if record ID was provided.
420 if(!isset($this->uri[1]) || !is_numeric($this->uri[1]))
421 {
422 throw new HttpApiV2Exception('Product record ID was not provided in the resource URL.', 400);
423 }
424
425 // Remove product record from database.
426 $this->productWriteService->deleteProductById(new IdType($this->uri[1]));
427
428 // Return response JSON.
429 $response = array(
430 'code' => 200,
431 'status' => 'success',
432 'action' => 'delete',
433 'resource' => 'Product',
434 'productId' => (int)$this->uri[1]
435 );
436
437 $this->_writeResponse($response);
438 }
439
440
441 /**
442 * @api {get} /products/:id Get Products
443 * @apiVersion 2.1.0
444 * @apiName GetProduct
445 * @apiGroup Products
446 *
447 * @apiDescription
448 * Get multiple or a single product records through a GET request. This method supports all the GET parameters
449 * that are mentioned in the "Introduction" section of this documentation. To see an example usage take a look at
450 * `docs/REST/samples/product-service/remove_product.php`
451 *
452 * @apiExample {curl} Get All Products
453 * curl -i --user admin@shop.de:12345 http://shop.de/api.php/v2/products
454 *
455 * @apiExample {curl} Get Product With ID = 24
456 * curl -i --user admin@shop.de:12345 http://shop.de/api.php/v2/products/24
457 *
458 * @apiError 404-NotFound Product does not exist.
459 * @apiErrorExample Error-Response
460 * HTTP/1.1 404 Not Found
461 * {
462 * "code": 404,
463 * "status": "error",
464 * "message": "Product does not exist."
465 * }
466 */
467 public function get()
468 {
469 if($this->_mapResponse($this->subresource))
470 {
471 return;
472 }
473
474 if($this->uri[1] && is_numeric($this->uri[1])) // Get Single Record
475 {
476 try
477 {
478 $products = array($this->productReadService->getProductById(new IdType($this->uri[1])));
479 }
480 catch(UnexpectedValueException $e)
481 {
482 throw new HttpApiV2Exception('Product does not exist.', 404);
483 }
484 }
485 else
486 {
487 $langParameter = ($this->api->request->get('lang') !== null) ? $this->api->request->get('lang') : 'de';
488
489 $languageCode = new LanguageCode(new NonEmptyStringType($langParameter));
490
491 $products = $this->productReadService->getProductList($languageCode)->getArray();
492 }
493
494 $response = array();
495
496 foreach($products as $product)
497 {
498 if($product instanceof ProductInterface)
499 {
500 $serialized = $this->productJsonSerializer->serialize($product, false);
501 }
502 else
503 {
504 $serialized = $this->productListItemJsonSerializer->serialize($product, false);
505 }
506
507 $response[] = $serialized;
508 }
509
510 if($this->api->request->get('q') !== null)
511 {
512 $this->_searchResponse($response, $this->api->request->get('q'));
513 }
514
515 $this->_paginateResponse($response);
516 $this->_sortResponse($response);
517 $this->_minimizeResponse($response);
518 $this->_linkResponse($response);
519
520 // Return single resource to client and not array.
521 if(isset($this->uri[1]) && is_numeric($this->uri[1]) && count($response) > 0)
522 {
523 $response = $response[0];
524 }
525
526 $this->_writeResponse($response);
527 }
528 }
529