Issues (36)

controllers/ApiController.php (4 issues)

1
<?php
2
3
namespace carono\exchange1c\controllers;
4
5
use carono\exchange1c\behaviors\BomBehavior;
6
use carono\exchange1c\ExchangeEvent;
7
use carono\exchange1c\ExchangeModule;
8
use carono\exchange1c\helpers\ByteHelper;
9
use carono\exchange1c\helpers\NodeHelper;
10
use carono\exchange1c\helpers\SerializeHelper;
11
use carono\exchange1c\interfaces\DocumentInterface;
12
use carono\exchange1c\interfaces\OfferInterface;
13
use carono\exchange1c\interfaces\ProductInterface;
14
use Yii;
15
use yii\db\ActiveRecord;
16
use yii\helpers\FileHelper;
17
use yii\web\Response;
18
use Zenwalker\CommerceML\CommerceML;
19
use Zenwalker\CommerceML\Model\Classifier;
20
use Zenwalker\CommerceML\Model\Group;
21
use Zenwalker\CommerceML\Model\Image;
22
use Zenwalker\CommerceML\Model\Offer;
23
use Zenwalker\CommerceML\Model\Product;
24
use Zenwalker\CommerceML\Model\PropertyCollection;
25
use Zenwalker\CommerceML\Model\Simple;
26
use Zenwalker\CommerceML\Model\RequisiteCollection;
27
28
/**
29
 * Default controller for the `api` module
30
 *
31
 * @property ExchangeModule $module
32
 */
33
class ApiController extends Controller
34
{
35
    public $enableCsrfValidation = false;
36
    public $commerceMLVersion = '2.10';
37
38
    const EVENT_BEFORE_UPDATE_PRODUCT = 'beforeUpdateProduct';
39
    const EVENT_AFTER_UPDATE_PRODUCT = 'afterUpdateProduct';
40
    const EVENT_BEFORE_UPDATE_OFFER = 'beforeUpdateOffer';
41
    const EVENT_AFTER_UPDATE_OFFER = 'afterUpdateOffer';
42
    const EVENT_BEFORE_PRODUCT_SYNC = 'beforeProductSync';
43
    const EVENT_AFTER_PRODUCT_SYNC = 'afterProductSync';
44
    const EVENT_BEFORE_OFFER_SYNC = 'beforeOfferSync';
45
    const EVENT_AFTER_OFFER_SYNC = 'afterOfferSync';
46
    const EVENT_AFTER_FINISH_UPLOAD_FILE = 'afterFinishUploadFile';
47
    const EVENT_AFTER_EXPORT_ORDERS = 'afterExportOrders';
48
49
    protected $_ids;
50
51
    public function init()
52
    {
53
        set_time_limit($this->module->timeLimit);
54
        if ($this->module->memoryLimit) {
55
            ini_set('memory_limit', $this->module->memoryLimit);
56
        }
57
        parent::init();
58
    }
59
60
61
    /**
62
     * @return array
63
     */
64
    public function behaviors()
65
    {
66
        return array_merge(parent::behaviors(), [
67
            'bom' => [
68
                'class' => BomBehavior::class,
69
                'only' => ['query'],
70
            ],
71
        ]);
72
    }
73
74
    /**
75
     * @param \yii\base\Action $action
76
     * @param mixed $result
77
     * @return mixed|string
78
     */
79
    public function afterAction($action, $result)
80
    {
81
        Yii::$app->response->headers->set('uid', Yii::$app->user->getId());
82
        if (is_bool($result)) {
83
            return $result ? 'success' : 'failure';
84
        }
85
86
        if (is_array($result)) {
87
            $r = [];
88
            foreach ($result as $key => $value) {
89
                $r[] = is_int($key) ? $value : $key . '=' . $value;
90
            }
91
            return implode("\n", $r);
92
        }
93
94
        return parent::afterAction($action, $result);
95
    }
96
97
    /**
98
     * @param $type
99
     * @return array|bool
100
     */
101
    public function actionCheckauth($type)
0 ignored issues
show
The parameter $type is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

101
    public function actionCheckauth(/** @scrutinizer ignore-unused */ $type)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
102
    {
103
        if (Yii::$app->user->isGuest) {
104
            return false;
105
        }
106
107
        return [
108
            'success',
109
            'PHPSESSID',
110
            Yii::$app->session->getId(),
111
        ];
112
    }
113
114
    /**
115
     * @return float|int|false
116
     */
117
    protected function getFileLimit()
118
    {
119
        $limit = ByteHelper::maximum_upload_size();
120
        if (!($limit % 2)) {
121
            $limit--;
122
        }
123
        return $limit;
124
    }
125
126
    /**
127
     * @return array
128
     */
129
    public function actionInit($type)
130
    {
131
        switch ($type) {
132
            case 'catalog':
133
                $pattern = '*.xml';
134
                $dir = $this->module->getTmpDir() . DIRECTORY_SEPARATOR . $pattern;
135
                foreach (glob($dir) as $file) {
136
                    if ($this->module->debug) {
137
                        rename($file, str_replace('.xml', date("_Y-m-d H.i.s", filemtime($file)) . '.xml.bak', $file));
138
                    } else {
139
                        FileHelper::unlink($file);
140
                    }
141
                }
142
                break;
143
        }
144
        return [
145
            'zip' => class_exists('ZipArchive') && $this->module->useZip ? 'yes' : 'no',
146
            'file_limit' => $this->getFileLimit(),
147
        ];
148
    }
149
150
    /**
151
     * @param $type
152
     * @param $filename
153
     * @return bool
154
     */
155
    public function actionFile($type, $filename)
0 ignored issues
show
The parameter $type is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

155
    public function actionFile(/** @scrutinizer ignore-unused */ $type, $filename)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
156
    {
157
        $body = Yii::$app->request->getRawBody();
158
        $filePath = $this->module->getTmpDir() . DIRECTORY_SEPARATOR . $filename;
159
        if (!is_dir(dirname($filePath))) {
160
            FileHelper::createDirectory(dirname($filePath));
161
        }
162
        $isArchive = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)) === 'zip';
163
        file_put_contents($filePath, $body, FILE_APPEND);
164
        if ((int)Yii::$app->request->headers->get('Content-Length') != $this->getFileLimit()) {
165
            if ($isArchive) {
166
                $this->extractArchive($filePath);
167
            }
168
            $this->afterFinishUploadFile($filePath);
169
        }
170
        return true;
171
    }
172
173
    /**
174
     * @param $file
175
     */
176
    public function parsingImport($file)
177
    {
178
        $this->_ids = [];
179
        $commerce = new CommerceML();
180
        $commerce->loadImportXml($file);
181
        $classifierFile = Yii::getAlias($this->module->tmpDir . '/classifier.xml');
182
        if ($commerce->classifier->xml) {
183
            $commerce->classifier->xml->saveXML($classifierFile);
184
        } else {
185
            $commerce->classifier->xml = simplexml_load_string(file_get_contents($classifierFile));
186
        }
187
        $this->beforeProductSync();
188
        if ($groupClass = $this->getGroupClass()) {
189
            $groupClass::createTree1c($commerce->classifier->getGroups());
190
        }
191
        $productClass = $this->getProductClass();
192
        $productClass::createProperties1c($commerce->classifier->getProperties());
193
        foreach ($commerce->catalog->getProducts() as $product) {
194
            if (!$model = $productClass::createModel1c($product)) {
195
                Yii::error("Модель продукта не найдена, проверьте реализацию $productClass::createModel1c",
196
                    'exchange1c');
197
                continue;
198
            }
199
            $this->parseProduct($model, $product);
200
            $this->_ids[] = $model->getPrimaryKey();
201
            $model = null;
202
            unset($model, $product);
203
            gc_collect_cycles();
204
        }
205
        $this->afterProductSync();
206
    }
207
208
    /**
209
     * @param $file
210
     */
211
    public function parsingOffer($file)
212
    {
213
        $this->_ids = [];
214
        $commerce = new CommerceML();
215
        $commerce->loadOffersXml($file);
216
        if ($offerClass = $this->getOfferClass()) {
217
            $offerClass::createPriceTypes1c($commerce->offerPackage->getPriceTypes());
218
        }
219
        $this->beforeOfferSync();
220
        foreach ($commerce->offerPackage->getOffers() as $offer) {
221
            $product_id = $offer->getClearId();
222
            if ($product = $this->findProductModelById($product_id)) {
223
                $model = $product->getOffer1c($offer);
224
                $this->parseProductOffer($model, $offer);
225
                $this->_ids[] = $model->getPrimaryKey();
226
            } else {
227
                Yii::warning("Продукт $product_id не найден в базе", 'exchange1c');
228
                continue;
229
            }
230
            unset($model);
231
        }
232
        $this->afterOfferSync();
233
    }
234
235
    /**
236
     * @param $file
237
     */
238
    public function parsingOrder($file)
239
    {
240
        /**
241
         * @var DocumentInterface $documentModel
242
         */
243
        $commerce = new CommerceML();
244
        $commerce->addXmls(false, false, $file);
245
        $documentClass = $this->module->documentClass;
246
        foreach ($commerce->order->documents as $document) {
247
            if ($documentModel = $documentClass::findOne((string)$document->Номер)) {
248
                $documentModel->setRaw1cData($commerce, $document);
249
            }
250
        }
251
    }
252
253
    /**
254
     * @param $filePath
255
     */
256
    private function extractArchive($filePath)
257
    {
258
        $zip = new \ZipArchive();
259
        $zip->open($filePath);
260
        $zip->extractTo(dirname($filePath));
261
        $zip->close();
262
        if (!$this->module->debug) {
263
            FileHelper::unlink($filePath);
264
        }
265
    }
266
267
    /**
268
     * @param $type
269
     * @param $filename
270
     * @return bool
271
     */
272
    public function actionImport($type, $filename)
273
    {
274
        $file = $this->module->getTmpDir() . DIRECTORY_SEPARATOR . $filename;
275
        switch ($type) {
276
            case 'catalog':
277
                if (strpos($file, 'offer') !== false) {
278
                    $this->parsingOffer($file);
279
                } elseif (strpos($file, 'import') !== false) {
280
                    $this->parsingImport($file);
281
                }
282
                break;
283
            case 'sale':
284
                if (strpos($file, 'order') !== false) {
285
                    $this->parsingOrder($file);
286
                }
287
                break;
288
        }
289
        if (!$this->module->debug) {
290
            FileHelper::unlink($file);
291
        }
292
        return true;
293
    }
294
295
    protected function clearTmp()
296
    {
297
        FileHelper::removeDirectory($this->module->getTmpDir());
298
    }
299
300
    /**
301
     * @param $type
302
     * @return mixed
303
     */
304
    public function actionQuery($type)
0 ignored issues
show
The parameter $type is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

304
    public function actionQuery(/** @scrutinizer ignore-unused */ $type)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
305
    {
306
        /**
307
         * @var DocumentInterface $document
308
         */
309
        $response = Yii::$app->response;
310
        $response->format = Response::FORMAT_RAW;
311
        $response->getHeaders()->set('Content-Type', 'application/xml; charset=windows-1251');
312
313
        $root = new \SimpleXMLElement('<КоммерческаяИнформация></КоммерческаяИнформация>');
314
        $root->addAttribute('ВерсияСхемы', $this->commerceMLVersion);
315
        $root->addAttribute('ДатаФормирования', date('Y-m-d\TH:i:s'));
316
317
        $ids = [];
318
        if ($this->module->exchangeDocuments) {
319
            $document = $this->module->documentClass;
320
            foreach ($document::findDocuments1c() as $order) {
321
                $ids[] = $order->getPrimaryKey();
322
                NodeHelper::appendNode($root, SerializeHelper::serializeDocument($order));
323
            }
324
            if ($this->module->debug) {
325
                $xml = $root->asXML();
326
                $xml = html_entity_decode($xml, ENT_NOQUOTES, 'UTF-8');
327
                file_put_contents($this->module->getTmpDir() . '/query.xml', $xml);
328
            }
329
        }
330
        $this->afterExportOrders($ids);
331
        return $root->asXML();
332
    }
333
334
    /**
335
     * @param $type
336
     * @return bool
337
     */
338
    public function actionSuccess($type)
0 ignored issues
show
The parameter $type is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

338
    public function actionSuccess(/** @scrutinizer ignore-unused */ $type)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
339
    {
340
        return true;
341
    }
342
343
    /**
344
     * @param $name
345
     * @param $value
346
     */
347
    protected static function setData($name, $value)
348
    {
349
        Yii::$app->session->set($name, $value);
350
    }
351
352
    /**
353
     * @param $name
354
     * @param null $default
355
     * @return mixed
356
     */
357
    protected static function getData($name, $default = null)
358
    {
359
        return Yii::$app->session->get($name, $default);
360
    }
361
362
    /**
363
     * @return bool
364
     */
365
    protected static function clearData()
366
    {
367
        return Yii::$app->session->closeSession();
368
    }
369
370
    /**
371
     * @param ProductInterface $model
372
     * @param \Zenwalker\CommerceML\Model\Product $product
373
     */
374
    protected function parseProduct($model, $product)
375
    {
376
        $this->beforeUpdateProduct($model);
377
        $model->setRaw1cData($product->owner, $product);
378
        $this->parseGroups($model, $product);
379
        $this->parseProperties($model, $product);
380
        $this->parseRequisites($model, $product);
381
        $this->parseImage($model, $product);
382
        $this->afterUpdateProduct($model);
383
        unset($group);
384
    }
385
386
    /**
387
     * @param OfferInterface $model
388
     * @param Offer $offer
389
     */
390
    protected function parseProductOffer($model, $offer)
391
    {
392
        $this->beforeUpdateOffer($model, $offer);
393
        $this->parseSpecifications($model, $offer);
394
        $this->parsePrice($model, $offer);
395
        $model->{$model::getIdFieldName1c()} = $offer->id;
396
        $model->save();
397
        $this->afterUpdateOffer($model, $offer);
398
    }
399
400
    /**
401
     * @param string $id
402
     *
403
     * @return ProductInterface|null
404
     */
405
    protected function findProductModelById($id)
406
    {
407
        /**
408
         * @var $class ProductInterface
409
         */
410
        $class = $this->getProductClass();
411
        return $class::find()->andWhere([$class::getIdFieldName1c() => $id])->one();
412
    }
413
414
    /**
415
     * @param Offer $offer
416
     *
417
     * @return OfferInterface|null
418
     */
419
    protected function findOfferModel($offer)
420
    {
421
        /**
422
         * @var $class ProductInterface
423
         */
424
        $class = $this->getOfferClass();
425
        return $class::find()->andWhere([$class::getIdFieldName1c() => $offer->id])->one();
426
    }
427
428
    /**
429
     * @return ActiveRecord
430
     */
431
    protected function createProductModel($data)
432
    {
433
        $class = $this->getProductClass();
434
        if ($model = $class::createModel1c($data)) {
435
            return $model;
436
        }
437
438
        return Yii::createObject(['class' => $class]);
439
    }
440
441
    /**
442
     * @param OfferInterface $model
443
     * @param Offer $offer
444
     */
445
    protected function parsePrice($model, $offer)
446
    {
447
        foreach ($offer->getPrices() as $price) {
448
            $model->setPrice1c($price);
449
        }
450
    }
451
452
    /**
453
     * @param ProductInterface $model
454
     * @param Product $product
455
     */
456
    protected function parseImage($model, $product)
457
    {
458
        $images = $product->getImages();
459
        foreach ($images as $image) {
460
            $path = realpath($this->module->getTmpDir() . DIRECTORY_SEPARATOR . $image->path);
461
            if (file_exists($path)) {
462
                $model->addImage1c($path, $image->caption);
463
            }
464
        }
465
    }
466
467
    /**
468
     * @param ProductInterface $model
469
     * @param Product $product
470
     */
471
    protected function parseGroups($model, $product)
472
    {
473
        $group = $product->getGroup();
474
        $model->setGroup1c($group);
475
    }
476
477
    /**
478
     * @param ProductInterface $model
479
     * @param Product $product
480
     */
481
    protected function parseRequisites($model, $product)
482
    {
483
        $requisites = $product->getRequisites();
484
        foreach ($requisites as $requisite) {
485
            $model->setRequisite1c($requisite->name, $requisite->value);
486
        }
487
    }
488
489
    /**
490
     * @param OfferInterface $model
491
     * @param Offer $offer
492
     */
493
    protected function parseSpecifications($model, $offer)
494
    {
495
        foreach ($offer->getSpecifications() as $specification) {
496
            $model->setSpecification1c($specification);
497
        }
498
    }
499
500
    /**
501
     * @param ProductInterface $model
502
     * @param Product $product
503
     */
504
    protected function parseProperties($model, $product)
505
    {
506
        $properties = $product->getProperties();
507
        foreach ($properties as $property) {
508
            $model->setProperty1c($property);
509
        }
510
    }
511
512
    /**
513
     * @return OfferInterface
514
     */
515
    protected function getOfferClass()
516
    {
517
        return $this->module->offerClass;
518
    }
519
520
    /**
521
     * @return ProductInterface
522
     */
523
    protected function getProductClass()
524
    {
525
        return $this->module->productClass;
526
    }
527
528
    /**
529
     * @return DocumentInterface
530
     */
531
    protected function getDocumentClass()
532
    {
533
        return $this->module->documentClass;
534
    }
535
536
    /**
537
     * @return \carono\exchange1c\interfaces\GroupInterface
538
     */
539
    protected function getGroupClass()
540
    {
541
        return $this->module->groupClass;
542
    }
543
544
    /**
545
     * @return bool
546
     */
547
    public function actionError()
548
    {
549
        return false;
550
    }
551
552
    /**
553
     * @param $filePath
554
     */
555
    public function afterFinishUploadFile($filePath)
556
    {
557
        $this->module->trigger(self::EVENT_AFTER_FINISH_UPLOAD_FILE, new ExchangeEvent(['filePath' => $filePath]));
558
    }
559
560
    public function beforeProductSync()
561
    {
562
        $this->module->trigger(self::EVENT_BEFORE_PRODUCT_SYNC, new ExchangeEvent());
563
    }
564
565
    public function afterProductSync()
566
    {
567
        $this->module->trigger(self::EVENT_AFTER_PRODUCT_SYNC, new ExchangeEvent(['ids' => $this->_ids]));
568
    }
569
570
    public function beforeOfferSync()
571
    {
572
        $this->module->trigger(self::EVENT_BEFORE_OFFER_SYNC, new ExchangeEvent());
573
    }
574
575
    public function afterOfferSync()
576
    {
577
        $this->module->trigger(self::EVENT_AFTER_OFFER_SYNC, new ExchangeEvent(['ids' => $this->_ids]));
578
    }
579
580
    /**
581
     * @param $model
582
     */
583
    public function afterUpdateProduct($model)
584
    {
585
        $this->module->trigger(self::EVENT_AFTER_UPDATE_PRODUCT, new ExchangeEvent(['model' => $model]));
586
    }
587
588
    /**
589
     * @param $model
590
     */
591
    public function beforeUpdateProduct($model)
592
    {
593
        $this->module->trigger(self::EVENT_BEFORE_UPDATE_PRODUCT, new ExchangeEvent(['model' => $model]));
594
    }
595
596
    /**
597
     * @param $model
598
     * @param $offer
599
     */
600
    public function beforeUpdateOffer($model, $offer)
601
    {
602
        $this->module->trigger(self::EVENT_BEFORE_UPDATE_OFFER, new ExchangeEvent([
603
            'model' => $model,
604
            'ml' => $offer,
605
        ]));
606
    }
607
608
    /**
609
     * @param $model
610
     * @param $offer
611
     */
612
    public function afterUpdateOffer($model, $offer)
613
    {
614
        $this->module->trigger(self::EVENT_AFTER_UPDATE_OFFER, new ExchangeEvent(['model' => $model, 'ml' => $offer]));
615
    }
616
617
    /**
618
     * @param $ids
619
     */
620
    public function afterExportOrders($ids)
621
    {
622
        $this->module->trigger(self::EVENT_AFTER_EXPORT_ORDERS, new ExchangeEvent(['ids' => $ids]));
623
    }
624
}
625