Commit 9e481998a280cab7788fea85e0331a587e606f73

Authored by Mihail
1 parent d682b074

add auto upload action and reorganized ParserController

backend/.gitignore
1   -/temp
2 1 \ No newline at end of file
  2 +/temp
  3 +/uploads
3 4 \ No newline at end of file
... ...
backend/components/parsers/CustomCsvParser.php
... ... @@ -11,7 +11,7 @@ namespace backend\components\parsers;
11 11  
12 12 class CustomCsvParser extends \yii\multiparser\CsvParser {
13 13  
14   - //public $last_line = 10;
  14 + public $last_line = 10;
15 15 //public $hasHeaderRow = true;
16 16 // public $keys = ['first','second', 'third', 'forth', 'fifth'];
17 17 public function setupConverter()
... ...
backend/controllers/CheckPriceController.php 0 → 100644
  1 +<?php
  2 +namespace backend\controllers;
  3 +
  4 +use Yii;
  5 +use yii\bootstrap\Modal;
  6 +use yii\data\ActiveDataProvider;
  7 +use yii\filters\AccessControl;
  8 +use backend\components\base\BaseController;
  9 +use yii\filters\VerbFilter;
  10 +use backend\models\Details;
  11 +use backend\models\ImporterFiles;
  12 +use backend\models\Importer;
  13 +use yii\base\ErrorException;
  14 +
  15 +use common\components\CustomVarDamp;
  16 +
  17 +/**
  18 + * Parser controller
  19 + */
  20 +class CheckPriceController extends BaseController
  21 +{
  22 + public $layout = "/column";
  23 +
  24 + /**
  25 + * @inheritdoc
  26 + */
  27 + public function behaviors()
  28 + {
  29 + return [
  30 + 'access' => [
  31 + 'class' => AccessControl::className(),
  32 + 'rules' => [
  33 + [
  34 + 'actions' => ['index', 'view'],
  35 + 'allow' => true,
  36 + 'roles' => ['@'],
  37 + ],
  38 + ],
  39 + ],
  40 +// 'verbs' => [
  41 +// 'class' => VerbFilter::className(),
  42 +// 'actions' => [
  43 +// 'logout' => ['post'],
  44 +// ],
  45 +// ],
  46 + ];
  47 + }
  48 +
  49 + /**
  50 + * @inheritdoc
  51 + */
  52 + public function actions()
  53 + {
  54 + return [
  55 + 'error' => [
  56 + 'class' => 'yii\web\ErrorAction',
  57 + ],
  58 + ];
  59 + }
  60 +
  61 +
  62 + public function actionIndex()
  63 + {
  64 +
  65 + if(Yii::$app->request->isAjax){
  66 + CustomVarDamp::dumpAndDie(1);
  67 + }
  68 +
  69 + //$query = (new Query())->select('*')->from('{{%importer_files}}')->where(['not', ['time_end' => null]])->orderBy(['upload_time' => SORT_DESC]);
  70 + $query = Importer::find()->where(['active' => true])->orderBy(['price_date_update' => SORT_DESC]);
  71 +
  72 + $provider = new ActiveDataProvider([
  73 + 'query' => $query,
  74 + 'pagination' => [
  75 + 'pageSize' => 10,
  76 + ],
  77 + ]);
  78 + return $this->render('index',
  79 + [
  80 + 'dataProvider' => $provider,
  81 + ]);
  82 + }
  83 +
  84 +
  85 + public function actionView ($id)
  86 + {
  87 + $query = Details::find()->where(['IMPORT_ID' => $id])->orderBy(['timestamp' => SORT_DESC]);
  88 +
  89 + $provider = new ActiveDataProvider([
  90 + 'query' => $query,
  91 + 'pagination' => [
  92 + 'pageSize' => 16,
  93 + ],
  94 + ]);
  95 + return $this->render('view',
  96 + ['dataProvider' => $provider]);
  97 + }
  98 +}
... ...
backend/controllers/ParserController.php
... ... @@ -36,7 +36,7 @@ class ParserController extends BaseController
36 36 'class' => AccessControl::className(),
37 37 'rules' => [
38 38 [
39   - 'actions' => ['index', 'results', 'write','check_price'],
  39 + 'actions' => ['index', 'results', 'write', 'check_price'],
40 40 'allow' => true,
41 41 'roles' => ['@'],
42 42 ],
... ... @@ -64,24 +64,57 @@ class ParserController extends BaseController
64 64 }
65 65  
66 66  
67   - public function actionIndex()
  67 + public function actionIndex($mode = 0)
68 68 {
69 69 $model = new UploadFileParsingForm();
  70 + // установим режим, 0 - ручная загрузка, 1 - автозагрузка
  71 + $model->mode = $mode;
70 72  
71 73 return $this->render('index', ['model' => $model]);
72 74 }
73 75  
74   - public function actionResults()
  76 + public function actionResults($mode = 0)
75 77 {
76   - $model = new UploadFileParsingForm();
  78 +
  79 + $model = new UploadFileParsingForm(['mode' => $mode]);
77 80 $data = [];
78 81 if ($model->load(Yii::$app->request->post())) {
79 82 $model->file = UploadedFile::getInstance($model, 'file');
80   - // первый проход - валидируем, сохраняем файл, ложим в кеш отпарсенные данные и параметры модели (потом при записи в базу данных они пригодятся)
  83 + // первый проход - валидируем, сохраняем файл, ложим в кеш (для ручной загрузки) отпарсенные данные и параметры модели (потом при записи в базу данных они пригодятся)
81 84 if ($model->validate()) {
82   - $model->file_path = Yii::getAlias('@webroot') . '/uploads/' . $model->file->baseName . '.' . $model->file->extension;
  85 + // запишем дату загрузки файла в таблицу файлов поставщика (ImportersFiles)
  86 + $files_model = new ImporterFiles();
  87 + // id поставщика получим из конфигурации
  88 + $files_model->load(['ImporterFiles' => $model->toArray()]);
  89 + try {
  90 + $files_model->save();
  91 + } catch (ErrorException $e) {
  92 + CustomVarDamp::dump($e->getMessage());
  93 + }
  94 + // получим id только что записанной записи - его запишем в название файла
  95 + $id = $files_model->find()
  96 + ->where(['importer_id' => $files_model->importer_id])
  97 + ->orderBy(['id' => SORT_DESC])
  98 + ->one()
  99 + ->id;
  100 +
  101 + $file_name = $id . '.' . $model->file->extension;
  102 +
  103 + if ($model->mode) {
  104 + $model->file_path = Yii::getAlias('@auto_upload') . '/' . $file_name;
  105 + } else {
  106 + $model->file_path = Yii::getAlias('@manual_upload') . '/' . $file_name;
  107 + }
83 108  
84 109 $model->file->saveAs($model->file_path);
  110 +
  111 + // для авто загрузки, обработка завершена
  112 + if ($model->mode) {
  113 + $model->success = true;
  114 + return $this->render('index', ['model' => $model]);
  115 + }
  116 +
  117 + // === ручная загрузка ===========
85 118 //запускаем парсинг
86 119 $data = $model->readFile();
87 120 // сохраняем в кеш отпарсенные даные
... ... @@ -90,6 +123,14 @@ class ParserController extends BaseController
90 123 Yii::$app->getCache()->set('parser_configuration', serialize($model));
91 124  
92 125  
  126 + } else {
  127 + // не прошла валидация форма загрузки файлов
  128 + //@todo - отправка на страницу ошибок
  129 + $errors_arr = $model->getErrors();
  130 + foreach ($errors_arr as $error) {
  131 + CustomVarDamp::dump(array_values($error));
  132 + }
  133 + die;
93 134 }
94 135 // листаем пагинатором, или повторно вызываем - считываем из кеша отпрасенные данные
95 136 } else if (Yii::$app->getCache()->get('parser_data')) {
... ... @@ -148,69 +189,63 @@ class ParserController extends BaseController
148 189  
149 190 // 1. запишем дату старта в таблицу файлов поставщика (ImportersFiles)
150 191 $files_model = new ImporterFiles();
151   - // id поставщика получим из конфигурации
  192 + // id поставщика и id загруженного файла получим из конфигурации
152 193 $files_model->load(['ImporterFiles' => $configuration->toArray()]);
153   - if ($files_model->validate()) {
  194 +
  195 + $update_date = date('Y-m-d H:i:s');
  196 + $files_model->time_start = $update_date;
  197 + // запишем дату начала загрузки
  198 + if (!$files_model->save()) {
  199 + CustomVarDamp::dumpAndDie($files_model->getErrors());
  200 + }
  201 +
  202 +
  203 + // 2. запишем полученные данные в таблицу товаров (Details)
  204 + $details_model = new Details();
  205 + // проверим все ли обязательные колонки были указаны пользователем
  206 + $details_model->load(['Details' => $data[0]]);
  207 + if ($details_model->validate()) {
  208 + // дополним данные значением импортера и даты обновления цены
  209 + $data = \Yii::$app->multiparser->addColumns($data, ['IMPORT_ID' => $configuration['importer_id'], 'timestamp' => $update_date]);
  210 +
154 211 try {
155   - $files_model->save();
156   - $update_date = $files_model->find()
157   - ->where(['importer_id' => $files_model->importer_id])
158   - ->orderBy(['id' => SORT_DESC])
159   - ->one()
160   - ->upload_time;
161   - $update_date = date('Y-m-d H:i:s',strtotime($update_date));
162   - // CustomVarDamp::dumpAndDie(strtotime($update_date));
163   - } catch (ErrorException $e) {
164   - CustomVarDamp::dump($e->getMessage());
165   - }
  212 + //@todo add transaction
  213 + // попытаемся вставить данные в БД с апдейтом по ключам
  214 + $details_model->ManualInsert($data);
166 215  
  216 + // 3. зафиксируем дату конца загрузки в файлах поставщика
167 217  
168   - // 2. запишем полученные данные в таблицу товаров (Details)
169   - $details_model = new Details();
170   - // проверим все ли обязательные колонки были указаны пользователем
171   - $details_model->load(['Details' => $data[0]]);
172   - if ($details_model->validate()) {
173   - // дополним данные значением импортера и даты обновления цены
174   - $data = \Yii::$app->multiparser->addColumns( $data, ['IMPORT_ID' => $configuration['importer_id'],'timestamp' => $update_date] );
175   -
176   - try {
177   - //@todo add transaction
178   - // попытаемся вставить данные в БД с апдейтом по ключам
179   - $details_model->ManualInsert($data);
180   -
181   - // 3. зафиксируем дату начала и конца загрузки в файлах поставщика (для ручной загрузки начало приравниваем time_start и update_date)
182   - $files_model->time_start = $update_date;
183   - $files_model->time_end = date('Y-m-d H:i:s');
184   - // CustomVarDamp::dumpAndDie($files_model);
185   - $files_model->save();
186   -
187   - // 4. зафиксируем дату загрузки в таблице поставщиков
188   - $imp_model = Importer::findOne( $configuration['importer_id'] );
189   - $imp_model->price_date_update = $update_date;
190   -
191   - if (!$imp_model->save()) {
192   - CustomVarDamp::dumpAndDie( $imp_model->getErrors() );
193   - }
194   - $configuration['success'] = true;
195   - // все прошло успешно - очищаем кеш
196   - Yii::$app->getCache()->delete('parser_data');
197   - Yii::$app->getCache()->delete('parser_configuration');
198   - // @todo - Delete the file
199   -
200   - // все успешно - возвращаемся в начало
201   - return $this->render('index', ['model' => $configuration]);
202   - } catch (ErrorException $e) {
203   - CustomVarDamp::dump($e->getMessage());
  218 + $files_model->time_end = date('Y-m-d H:i:s');
  219 + // CustomVarDamp::dumpAndDie($files_model);
  220 + if (!$files_model->save()) {
  221 + CustomVarDamp::dumpAndDie($files_model->getErrors());
204 222 }
205   - }
206   - if ($details_model->hasErrors()) {
207   - $errors_arr = $details_model->getErrors();
208   - foreach ($errors_arr as $error) {
209   - CustomVarDamp::dump(array_values($error));
  223 +
  224 + // 4. зафиксируем дату загрузки в таблице поставщиков
  225 + $imp_model = Importer::findOne($configuration['importer_id']);
  226 + $imp_model->price_date_update = $update_date;
  227 +
  228 + if (!$imp_model->save()) {
  229 + CustomVarDamp::dumpAndDie($imp_model->getErrors());
210 230 }
  231 + $configuration['success'] = true;
  232 + // все прошло успешно - очищаем кеш
  233 + Yii::$app->getCache()->delete('parser_data');
  234 + Yii::$app->getCache()->delete('parser_configuration');
211 235  
212   - }
  236 + unlink($configuration['file_path']);
213 237  
  238 + return $this->render('index', ['model' => $configuration]);
  239 +
  240 + } catch (ErrorException $e) {
  241 + CustomVarDamp::dump($e->getMessage());
  242 + }
  243 + }
  244 + if ($details_model->hasErrors()) {
  245 + $errors_arr = $details_model->getErrors();
  246 + foreach ($errors_arr as $error) {
  247 + CustomVarDamp::dump(array_values($error));
  248 + }
214 249  
215 250 }
216 251  
... ... @@ -219,19 +254,19 @@ class ParserController extends BaseController
219 254  
220 255 }
221 256  
222   -// public function actionCheck_price ()
223   -// {
224   -// //$query = (new Query())->select('*')->from('{{%importer_files}}')->where(['not', ['time_end' => null]])->orderBy(['upload_time' => SORT_DESC]);
225   -// $query = Importer::find()->where(['active' => true])->orderBy(['price_date_update' => SORT_DESC]);
226   -//
227   -// $provider = new ActiveDataProvider([
228   -// 'query' => $query,
229   -// 'pagination' => [
230   -// 'pageSize' => 10,
231   -// ],
232   -// ]);
233   -// return $this->render('check_price',
234   -// [
235   -// 'dataProvider' => $provider]);
236   -// }
  257 + public function actionAutoUpload()
  258 + {
  259 + //$query = (new Query())->select('*')->from('{{%importer_files}}')->where(['not', ['time_end' => null]])->orderBy(['upload_time' => SORT_DESC]);
  260 + $query = Importer::find()->where(['active' => true])->orderBy(['price_date_update' => SORT_DESC]);
  261 +
  262 + $provider = new ActiveDataProvider([
  263 + 'query' => $query,
  264 + 'pagination' => [
  265 + 'pageSize' => 10,
  266 + ],
  267 + ]);
  268 + return $this->render('check_price',
  269 + [
  270 + 'dataProvider' => $provider]);
  271 + }
237 272 }
... ...
backend/models/Details.php
... ... @@ -70,6 +70,7 @@ class Details extends BaseActiveRecord
70 70  
71 71 public function ManualInsert ($data)
72 72 {
  73 + // \common\components\CustomVarDamp::dumpAndDie($data);
73 74 $table_name = self::tableName();
74 75 $keys_arr = array_keys( $data[0] );
75 76 // найдем те поля которые не являются ключами. Их нужно будет при дубляже апдейтить
... ...
backend/models/UploadFileParsingForm.php
... ... @@ -25,10 +25,22 @@ class UploadFileParsingForm extends Model
25 25 // служебные атрибуты
26 26 public $file_path;
27 27 public $success;
  28 + public $mode; //0 - режим ручной загрузки, 1 - режим автозагрузки
28 29  
29 30 /**
30 31 * @return array the validation rules.
31 32 */
  33 + public function __construct($config = [])
  34 + {
  35 + parent::__construct($config);
  36 + if ( $this->mode ) {
  37 + // автозагрузка, проставим сценарий
  38 + $this->scenario = 'auto';
  39 + }
  40 +
  41 + }
  42 +
  43 +
32 44 public function rules()
33 45 {
34 46 return [
... ... @@ -38,8 +50,9 @@ class UploadFileParsingForm extends Model
38 50 [['file'], 'file'],// 'extensions' => ['csv', 'xml'] ],
39 51 // 'wrongMimeType' => 'Указан неподдерживаемый тип файла. Можно выбирать csv, xml файлы.' ],
40 52 ['importer_id', 'integer','max' => 999999, 'min' => 0 ],
41   - [['action','delete_prefix', 'delete_price', 'success'], 'boolean'],
  53 + [['action','delete_prefix', 'delete_price', 'success'], 'boolean', 'except' => 'auto' ], // только для ручной загрузки
42 54 ['delimiter', 'string', 'max' => 1],
  55 + ['mode', 'safe'],
43 56 ['delimiter', 'default', 'value' => ';'],
44 57 [ 'success', 'default', 'value' => false]
45 58  
... ... @@ -65,4 +78,21 @@ class UploadFileParsingForm extends Model
65 78 return $data;
66 79 }
67 80  
  81 + public function fields()
  82 + {
  83 + return [
  84 +
  85 + 'importer_id',
  86 + 'delimiter',
  87 + 'delete_price',
  88 + 'delete_prefix',
  89 + 'file_path',
  90 + // id записи таблицы ImportersFiles, получаем из имени загруженного файла
  91 + 'id' => function () {
  92 + return $this->file->getBaseName();
  93 + },
  94 + ];
  95 + }
  96 +
  97 +
68 98 }
69 99 \ No newline at end of file
... ...
backend/views/check-price/index.php 0 → 100644
  1 +<?php
  2 +use yii\helpers\Html;
  3 +use yii\grid\GridView;
  4 +use yii\grid\SerialColumn;
  5 +use yii\grid\ActionColumn;
  6 +use yii\widgets\Pjax;
  7 +
  8 +
  9 +/* @var $this yii\web\View */
  10 +/* @var $searchModel backend\models\CatalogSearch */
  11 +/* @var $dataProvider yii\data\ActiveDataProvider */
  12 +
  13 +$this->title = 'Проверка прайсов';
  14 +$this->params['breadcrumbs'][] = $this->title;
  15 +?>
  16 +<div class="catalog-index">
  17 +
  18 + <h1><?= Html::encode($this->title) ?></h1>
  19 +
  20 +
  21 + <?= GridView::widget( ['dataProvider' => $dataProvider,
  22 + 'columns' => [['class' => SerialColumn::className()],
  23 + [
  24 + 'class' => ActionColumn::className(),
  25 + 'template'=>'{view}',
  26 + 'contentOptions' => function ($model, $key, $index, $column){
  27 + return ['data' => ['id' => $model->id, 'date' => $model->price_date_update]];
  28 + }
  29 + ],
  30 + [
  31 + 'label' =>'Поставщик',
  32 + 'value' => function ($data) {
  33 + return '№ ' .$data->id . ' ' . $data->name;
  34 + },
  35 + ],
  36 + ['label' =>'Дата обновления',
  37 + 'attribute' => 'price_date_update' ],
  38 + ['label' => 'Кол-во дней',
  39 + 'value' => function ($data) {
  40 + $date1 = new DateTime("now");
  41 + $date2 = new DateTime( $data->price_date_update );
  42 + $quo_days = $date2->diff($date1)->format('%R%a');
  43 + // уберем первый символ - там знак "+"
  44 + $quo_days = substr( $quo_days, 1, strlen($quo_days) );
  45 + $quo_days = (int) $quo_days;
  46 +
  47 + if($quo_days > 15)
  48 + $quo_days = '>15';
  49 +
  50 + return $quo_days;
  51 + }
  52 + ],
  53 + ]] );?>
  54 +
  55 +
  56 +
  57 +
  58 +</div>
0 59 \ No newline at end of file
... ...
backend/views/check-price/view.php 0 → 100644
  1 +<?php
  2 +use yii\helpers\Html;
  3 +use yii\grid\GridView;
  4 +use yii\grid\SerialColumn;
  5 +use yii\bootstrap\Modal;
  6 +
  7 +
  8 +/* @var $this yii\web\View */
  9 +/* @var $searchModel backend\models\CatalogSearch */
  10 +/* @var $dataProvider yii\data\ActiveDataProvider */
  11 +
  12 +$this->title = 'Проверка прайсов';
  13 +$this->params['breadcrumbs'][] = $this->title;
  14 +
  15 +?>
  16 +<div class="catalog-index">
  17 +
  18 + <h1><?= Html::encode($this->title) ?></h1>
  19 +
  20 + <?= GridView::widget( ['dataProvider' => $dataProvider,
  21 +
  22 + ] );
  23 +
  24 +
  25 + ?>
  26 +
  27 +
  28 +
  29 +</div>
  30 +<?php
  31 +
  32 +?>
0 33 \ No newline at end of file
... ...
backend/views/layouts/column.php
... ... @@ -283,8 +283,9 @@ $this-&gt;beginContent(&#39;@app/views/layouts/main.php&#39;);
283 283 'options' => ['class' => 'sidebar-menu'],
284 284 'items' => [
285 285 ['label' => "Загрузка файлов", 'url' => ['#'], 'items' => [
  286 + ['label' => 'Загрузить файл на сервер', 'url' => ['parser/index', 'mode' => 1]],
286 287 ['label' => 'Ручная загрузка', 'url' => ['parser/index']],
287   - ['label' => 'Проверка прайс файлов', 'url' => ['check_price/index']],
  288 + ['label' => 'Проверка прайс файлов', 'url' => ['check-price/index']],
288 289 ],
289 290 ],
290 291 ['label' => 'Управление ролями', 'url' => ['#'], 'items' => [
... ...
backend/views/parser/index.php
... ... @@ -3,16 +3,24 @@ use yii\widgets\ActiveForm;
3 3 use yii\helpers\Html;
4 4 use backend\models\Importer;
5 5 use yii\helpers\ArrayHelper;
  6 +if ( $model->mode ) {
  7 + // авто загрузка
  8 + $mode = 1;
  9 + $button_label = 'Загрузить';
  10 +} else {
  11 + // ручная загрузка
  12 + $mode = 0;
  13 + $button_label = 'Прочитать';
  14 +}
6 15  
7 16 ?>
8 17 <div class="row">
9 18 <div class="col-lg-5">
10   - <?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data',],'action'=>['parser/results']]);
  19 + <?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data',],'action'=>['parser/results', 'mode' => $mode]]);
11 20 if (!$model->action) {
12 21 $model->action = 1;
13 22 }
14 23 if ($model->success) { // вернулись после успешной загрузки данного файла
15   - //echo "<h4>Файл успешно загружен</h4>";
16 24 echo Html::tag('h3', 'Файл успешно загружен',['class'=>'bg-success']);
17 25 }
18 26 ?>
... ... @@ -20,16 +28,25 @@ use yii\helpers\ArrayHelper;
20 28  
21 29  
22 30 <?= $form->field($model, 'importer_id')->dropDownList(ArrayHelper::map( Importer::find()->all(), 'id','name' )); ?>
23   - <?= $form->field($model, 'delete_price')->checkbox(['label' => 'Загрузить с удалением старого прайса']) ?>
  31 +
  32 + <?php if ( !$mode ) {
  33 + echo $form->field($model, 'delete_price')->checkbox(['label' => 'Загрузить с удалением старого прайса']);
  34 + }
  35 + ?>
  36 +
24 37 <?= $form->field($model, 'file')->fileInput()->label(false) ?>
25   - <?= $form->field($model, 'action')->radioList([1 => 'Стандартная обработка', 0 => 'С разделителем'])->label(false) ?>
26   - <?= $form->field($model, 'delimiter', ['inputOptions' => ['value' => ';']]) ?>
27 38  
28   - <?= $form->field($model, 'delete_prefix')->checkbox(['label' => 'Удалять префикс']) ?>
  39 + <?php if ( !$mode ) {
  40 + echo $form->field($model, 'action')->radioList([1 => 'Стандартная обработка', 0 => 'С разделителем'])->label(false);
  41 + echo $form->field($model, 'delimiter', ['inputOptions' => ['value' => ';']]);
  42 +
  43 + echo $form->field($model, 'delete_prefix')->checkbox(['label' => 'Удалять префикс']);
  44 + }
  45 + ?>
29 46  
30 47  
31 48 <div class="form-group">
32   - <?= Html::submitButton(Yii::t('app', 'Прочитать'), ['class' => 'btn btn-primary']) ?>
  49 + <?= Html::submitButton(Yii::t( 'app', $button_label ), ['class' => 'btn btn-primary']) ?>
33 50 </div>
34 51  
35 52 <?php ActiveForm::end() ?>
... ...
common/config/bootstrap.php
... ... @@ -3,3 +3,5 @@ Yii::setAlias(&#39;common&#39;, dirname(__DIR__));
3 3 Yii::setAlias('frontend', dirname(dirname(__DIR__)) . '/frontend');
4 4 Yii::setAlias('backend', dirname(dirname(__DIR__)) . '/backend');
5 5 Yii::setAlias('console', dirname(dirname(__DIR__)) . '/console');
  6 +Yii::setAlias('auto_upload', dirname(dirname(__DIR__)) . '/backend/uploads/auto');
  7 +Yii::setAlias('manual_upload', dirname(dirname(__DIR__)) . '/backend/uploads/manual');
... ...