Passed
Push — master ( 264ba0...aa6c9a )
by Aleksandr
01:35
created

ApiController::parseProductOffer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 2
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
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
37
    const EVENT_BEFORE_UPDATE_PRODUCT = 'beforeUpdateProduct';
38
    const EVENT_AFTER_UPDATE_PRODUCT = 'afterUpdateProduct';
39
    const EVENT_BEFORE_UPDATE_OFFER = 'beforeUpdateOffer';
40
    const EVENT_AFTER_UPDATE_OFFER = 'afterUpdateOffer';
41
    const EVENT_BEFORE_PRODUCT_SYNC = 'beforeProductSync';
42
    const EVENT_AFTER_PRODUCT_SYNC = 'afterProductSync';
43
    const EVENT_BEFORE_OFFER_SYNC = 'beforeOfferSync';
44
    const EVENT_AFTER_OFFER_SYNC = 'afterOfferSync';
45
    const EVENT_AFTER_FINISH_UPLOAD_FILE = 'afterFinishUploadFile';
46
    const EVENT_AFTER_EXPORT_ORDERS = 'afterExportOrders';
47
48
    private $_ids;
49
50
    public function init()
51
    {
52
        set_time_limit($this->module->timeLimit);
53
        if ($this->module->memoryLimit) {
54
            ini_set('memory_limit', $this->module->memoryLimit);
55
        }
56
        parent::init();
57
    }
58
59
60
    /**
61
     * @return array
62
     */
63
    public function behaviors()
64
    {
65
        return array_merge(parent::behaviors(), [
66
            'bom' => [
67
                'class' => BomBehavior::class,
68
                'only' => ['query'],
69
            ],
70
        ]);
71
    }
72
73
    /**
74
     * @param \yii\base\Action $action
75
     * @param mixed $result
76
     * @return mixed|string
77
     */
78
    public function afterAction($action, $result)
79
    {
80
        Yii::$app->response->headers->set('uid', Yii::$app->user->getId());
81
        if (is_bool($result)) {
82
            return $result ? "success" : "failure";
83
        } elseif (is_array($result)) {
84
            $r = [];
85
            foreach ($result as $key => $value) {
86
                $r[] = is_int($key) ? $value : $key . '=' . $value;
87
            }
88
            return join("\n", $r);
89
        } else {
90
            return parent::afterAction($action, $result);
91
        }
92
    }
93
94
    /**
95
     * @param $type
96
     * @return array|bool
97
     */
98
    public function actionCheckauth($type)
0 ignored issues
show
Unused Code introduced by
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

98
    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...
99
    {
100
        if (Yii::$app->user->isGuest) {
101
            return false;
102
        } else {
103
            return [
104
                "success",
105
                "PHPSESSID",
106
                Yii::$app->session->getId(),
107
            ];
108
        }
109
    }
110
111
    /**
112
     * @return float|int
113
     */
114
    protected function getFileLimit()
115
    {
116
        $limit = ByteHelper::maximum_upload_size();
117
        if (!($limit % 2)) {
118
            $limit--;
119
        }
120
        return $limit;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $limit returns the type false which is incompatible with the documented return type integer|double.
Loading history...
121
    }
122
123
    /**
124
     * @return array
125
     */
126
    public function actionInit()
127
    {
128
        return [
129
            "zip" => class_exists('ZipArchive') && $this->module->useZip ? "yes" : "no",
130
            "file_limit" => $this->getFileLimit(),
131
        ];
132
    }
133
134
    /**
135
     * @param $type
136
     * @param $filename
137
     * @return bool
138
     */
139
    public function actionFile($type, $filename)
0 ignored issues
show
Unused Code introduced by
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

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

283
    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...
284
    {
285
        /**
286
         * @var DocumentInterface $document
287
         */
288
        $response = Yii::$app->response;
0 ignored issues
show
Documentation Bug introduced by
It seems like Yii::app->response can also be of type yii\web\Response. However, the property $response is declared as type yii\console\Response. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
289
        $response->format = Response::FORMAT_RAW;
290
        $response->getHeaders()->set('Content-Type', 'application/xml; charset=windows-1251');
291
292
        $root = new \SimpleXMLElement('<КоммерческаяИнформация></КоммерческаяИнформация>');
293
        $root->addAttribute('ВерсияСхемы', '2.10');
294
        $root->addAttribute('ДатаФормирования', date('Y-m-d\TH:i:s'));
295
296
        $ids = [];
297
        if ($this->module->exchangeDocuments) {
298
            $document = $this->module->documentClass;
299
            foreach ($document::findDocuments1c() as $order) {
300
                $ids[] = $order->getPrimaryKey();
301
                NodeHelper::appendNode($root, SerializeHelper::serializeDocument($order));
302
            }
303
            if ($this->module->debug) {
304
                $xml = $root->asXML();
305
                $xml = html_entity_decode($xml, ENT_NOQUOTES, 'UTF-8');
306
                file_put_contents($this->module->getTmpDir() . '/query.xml', $xml);
307
            }
308
        }
309
        $this->afterExportOrders($ids);
310
        return $root->asXML();
311
    }
312
313
    /**
314
     * @param $type
315
     * @return bool
316
     */
317
    public function actionSuccess($type)
0 ignored issues
show
Unused Code introduced by
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

317
    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...
318
    {
319
        return true;
320
    }
321
322
    /**
323
     * @param $name
324
     * @param $value
325
     */
326
    protected static function setData($name, $value)
327
    {
328
        Yii::$app->session->set($name, $value);
329
    }
330
331
    /**
332
     * @param $name
333
     * @param null $default
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $default is correct as it would always require null to be passed?
Loading history...
334
     * @return mixed
335
     */
336
    protected static function getData($name, $default = null)
337
    {
338
        return Yii::$app->session->get($name, $default);
339
    }
340
341
    /**
342
     * @return bool
343
     */
344
    protected static function clearData()
345
    {
346
        return Yii::$app->session->closeSession();
347
    }
348
349
    /**
350
     * @param ProductInterface $model
351
     * @param \Zenwalker\CommerceML\Model\Product $product
352
     */
353
    protected function parseProduct($model, $product)
354
    {
355
        $this->beforeUpdateProduct($model);
356
        $model->setRaw1cData($product->owner, $product);
357
        $this->parseGroups($model, $product);
358
        $this->parseProperties($model, $product);
359
        $this->parseRequisites($model, $product);
360
        $this->parseImage($model, $product);
361
        $this->afterUpdateProduct($model);
362
        unset($group);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $group seems to be never defined.
Loading history...
363
    }
364
365
    /**
366
     * @param OfferInterface $model
367
     * @param Offer $offer
368
     */
369
    protected function parseProductOffer($model, $offer)
370
    {
371
        $this->beforeUpdateOffer($model, $offer);
372
        $this->parseSpecifications($model, $offer);
373
        $this->parsePrice($model, $offer);
374
        $model->{$model::getIdFieldName1c()} = $offer->id;
375
        $model->save();
376
        $this->afterUpdateOffer($model, $offer);
377
        unset($model);
378
    }
379
380
    /**
381
     * @param string $id
382
     *
383
     * @return ProductInterface|null
384
     */
385
    protected function findProductModelById($id)
386
    {
387
        /**
388
         * @var $class ProductInterface
389
         */
390
        $class = $this->getProductClass();
391
        return $class::find()->andWhere([$class::getIdFieldName1c() => $id])->one();
392
    }
393
394
    /**
395
     * @param Offer $offer
396
     *
397
     * @return OfferInterface|null
398
     */
399
    protected function findOfferModel($offer)
400
    {
401
        /**
402
         * @var $class ProductInterface
403
         */
404
        $class = $this->getOfferClass();
405
        return $class::find()->andWhere([$class::getIdFieldName1c() => $offer->id])->one();
406
    }
407
408
    /**
409
     * @return ActiveRecord
410
     */
411
    protected function createProductModel($data)
412
    {
413
        $class = $this->getProductClass();
414
        if ($model = $class::createModel1c($data)) {
415
            return $model;
416
        } else {
417
            return Yii::createObject(['class' => $class]);
418
        }
419
    }
420
421
    /**
422
     * @param OfferInterface $model
423
     * @param Offer $offer
424
     */
425
    protected function parsePrice($model, $offer)
426
    {
427
        foreach ($offer->getPrices() as $price) {
428
            $model->setPrice1c($price);
429
        }
430
    }
431
432
    /**
433
     * @param ProductInterface $model
434
     * @param Product $product
435
     */
436
    protected function parseImage($model, $product)
437
    {
438
        $images = $product->getImages();
439
        foreach ($images as $image) {
440
            $path = realpath($this->module->getTmpDir() . DIRECTORY_SEPARATOR . $image->path);
441
            if (file_exists($path)) {
442
                $model->addImage1c($path, $image->caption);
443
            }
444
        }
445
    }
446
447
    /**
448
     * @param ProductInterface $model
449
     * @param Product $product
450
     */
451
    protected function parseGroups($model, $product)
452
    {
453
        $group = $product->getGroup();
454
        $model->setGroup1c($group);
455
    }
456
457
    /**
458
     * @param ProductInterface $model
459
     * @param Product $product
460
     */
461
    protected function parseRequisites($model, $product)
462
    {
463
        $requisites = $product->getRequisites();
464
        foreach ($requisites as $requisite) {
465
            $model->setRequisite1c($requisite->name, $requisite->value);
466
        }
467
    }
468
469
    /**
470
     * @param OfferInterface $model
471
     * @param Offer $offer
472
     */
473
    protected function parseSpecifications($model, $offer)
474
    {
475
        foreach ($offer->getSpecifications() as $specification) {
476
            $model->setSpecification1c($specification);
477
        }
478
    }
479
480
    /**
481
     * @param ProductInterface $model
482
     * @param Product $product
483
     */
484
    protected function parseProperties($model, $product)
485
    {
486
        $properties = $product->getProperties();
487
        foreach ($properties as $property) {
488
            $model->setProperty1c($property);
489
        }
490
    }
491
492
    /**
493
     * @return OfferInterface
494
     */
495
    protected function getOfferClass()
496
    {
497
        return $this->module->offerClass;
498
    }
499
500
    /**
501
     * @return ProductInterface
502
     */
503
    protected function getProductClass()
504
    {
505
        return $this->module->productClass;
506
    }
507
508
    /**
509
     * @return DocumentInterface
510
     */
511
    protected function getDocumentClass()
512
    {
513
        return $this->module->documentClass;
514
    }
515
516
    /**
517
     * @return \carono\exchange1c\interfaces\GroupInterface
518
     */
519
    protected function getGroupClass()
520
    {
521
        return $this->module->groupClass;
522
    }
523
524
    /**
525
     * @return bool
526
     */
527
    public function actionError()
528
    {
529
        return false;
530
    }
531
532
    /**
533
     * @param $filePath
534
     */
535
    public function afterFinishUploadFile($filePath)
0 ignored issues
show
Unused Code introduced by
The parameter $filePath 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

535
    public function afterFinishUploadFile(/** @scrutinizer ignore-unused */ $filePath)

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...
536
    {
537
        $this->module->trigger(self::EVENT_AFTER_FINISH_UPLOAD_FILE, new ExchangeEvent());
538
    }
539
540
    public function beforeProductSync()
541
    {
542
        $this->module->trigger(self::EVENT_BEFORE_PRODUCT_SYNC, new ExchangeEvent());
543
    }
544
545
    public function afterProductSync()
546
    {
547
        $this->module->trigger(self::EVENT_AFTER_PRODUCT_SYNC, new ExchangeEvent(['ids' => $this->_ids]));
548
    }
549
550
    public function beforeOfferSync()
551
    {
552
        $this->module->trigger(self::EVENT_BEFORE_OFFER_SYNC, new ExchangeEvent());
553
    }
554
555
    public function afterOfferSync()
556
    {
557
        $this->module->trigger(self::EVENT_AFTER_OFFER_SYNC, new ExchangeEvent(['ids' => $this->_ids]));
558
    }
559
560
    /**
561
     * @param $model
562
     */
563
    public function afterUpdateProduct($model)
564
    {
565
        $this->module->trigger(self::EVENT_AFTER_UPDATE_PRODUCT, new ExchangeEvent(['model' => $model]));
566
    }
567
568
    /**
569
     * @param $model
570
     */
571
    public function beforeUpdateProduct($model)
572
    {
573
        $this->module->trigger(self::EVENT_BEFORE_UPDATE_PRODUCT, new ExchangeEvent(['model' => $model]));
574
    }
575
576
    /**
577
     * @param $model
578
     * @param $offer
579
     */
580
    public function beforeUpdateOffer($model, $offer)
581
    {
582
        $this->module->trigger(self::EVENT_BEFORE_UPDATE_OFFER, new ExchangeEvent([
583
            'model' => $model,
584
            'ml' => $offer,
585
        ]));
586
    }
587
588
    /**
589
     * @param $model
590
     * @param $offer
591
     */
592
    public function afterUpdateOffer($model, $offer)
593
    {
594
        $this->module->trigger(self::EVENT_AFTER_UPDATE_OFFER, new ExchangeEvent(['model' => $model, 'ml' => $offer]));
595
    }
596
597
    /**
598
     * @param $ids
599
     */
600
    public function afterExportOrders($ids)
601
    {
602
        $this->module->trigger(self::EVENT_AFTER_EXPORT_ORDERS, new ExchangeEvent(['ids' => $ids]));
603
    }
604
}
605