Test Failed
Push — trunk ( e02d87...314b8c )
by SuperNova.WS
07:20
created

SkinV2::tryWebp()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 8
nc 4
nop 1
dl 0
loc 18
rs 9.6111
c 0
b 0
f 0
ccs 0
cts 0
cp 0
crap 30
1
<?php
2
3
/**
4
 * User: Gorlum
5
 * Date: 23.10.2015
6
 * Time: 19:54
7
 */
8
/*
9
10
INI-файл:
11
  Спецпараметры начинаются с _:
12
      _inherit - брать отсутствующие картинки из другого скина
13
  Изображения:
14
      eisenplanet01 = "planeten/eisenplanet01.jpg" - путь относительно локального скина
15
      В качестве ID изображения можно указывать путь:
16
        img/galaxie.gif = "img/galaxie.gif"
17
18
Вызов в темплейте
19
   {I_<id>|парам1|парам2|...} - {I_abort|html}
20
   {I_<путь к картинке от корня скина>|парам1|парам2|...} - {I_img/e.jpg|html}
21
   {I_<путь к картинке от корня движка>|парам1|парам2|...} - {I_/img/e.jpg|html}
22
   {I_[<имя переменной в темплейте>]} - будет подставлено имя соответствующей переменной в момент выполнения. Поддерживаются:
23
       - Корневые значения, например {I_[UNIT_ID]}
24
       - Значения в блоках, например {I_[production.ID]}
25
       - Корневые значения DEFINE, например {I_[$PLANET_GOVERNOR_ID]}
26
   Параметры вывода:
27
      html - отрендерить обрамление HTML-тэгом IMG: <img src="" />
28
*/
29
30
/**
31
 * Класс skin отвечает за работу скинов. В настоящее время - за маппинг {I_xxx} тэгов в HTTP-путь к файлу с картинкой
32
 *
33
 * Возможности:
34
 * - Поддержка конфигурации в файле skin.ini
35
 * - Работа через PTL тэги {I_xxx}
36
 * - Поддержка опций рендеринга через {I_xxx|param...}
37
 * - Поддержка абсолютных и относительных путей в skin.ini (абсоютный путь начинается с '/' - '/design/images/_no_image.png')
38
 *    - Относительные пути ресолвятся относительно корня скина - т.е. папки, где лежит skin.ini
39
 * - Подстановка значений переменных из класса template через {I_xxx[yyy]}:
40
 *    - Глобальные переменные - {I_xxx[UNIT_ID]}
41
 *    - Назначенные переменные - {I_xxx[$UNIT_ID]}
42
 *    - Переменные в блоках - {I_xxx[block.VAR]}
43
 * - Возможность указать в image-tag прямой путь - {I_/design/images/_no_image.png} - как абсолютный так и относительный
44
 * - Наследование скинов любой глубины вложенности (опция _inherit в skin.ini)
45
 * - Подстановка картинок из родителя при отсутствии данных в skin.ini или физическом отутствии файла
46
 * - Заглушка _NO_IMAGE при отсутствии картинки (опция _no_image в skin.ini)
47
 */
48
class SkinV2 implements SkinInterface {
49
  const PARAM_HTML = 'html';
50
  const PARAM_HTML_HEIGHT = 'height';
51
  const PARAM_HTML_WIDTH = 'width';
52
  // Use skin for image
53
  const PARAM_SKIN = 'skin';
54
  const WEBP_SUFFIX = '_webp';
55
56
  /**
57
   * @var string $iniFileName
58
   */
59
  protected $iniFileName = 'skin.ini';
60
61
  /**
62
   * @var SkinModel $model
63
   */
64
  protected $model;
65
66
67
  /**
68
   * Флаг инициализации статического объекта
69
   *
70
   * @var bool
71
   */
72
  protected static $is_init = false;
73
  /**
74
   * Список скинов
75
   * TODO Переделать под контейнер
76
   *
77
   * @var self[] $skin_list
78
   */
79
  protected static $skin_list = array();
80
  /**
81
   * Текущий скин
82
   *
83
   * @var self|null
84
   */
85
  protected static $active = null;
86
87
  /**
88
   * HTTP-путь к файлам скина относительно корня движка
89
   *
90
   * @var string
91
   */
92
  protected $root_http_relative = '';
93
  /**
94
   * Абсолютный физический путь к директории скина
95
   *
96
   * @var string
97
   */
98
  protected $root_physical_absolute = '';
99
  /**
100
   * Родительский скин
101
   *
102
   * @var SkinInterface|null
103
   */
104
  protected $parent = null;
105
  /**
106
   * Конфигурация скина - читается из INI-файла
107
   *
108
   * @var array
109
   */
110
  protected $config = array();
111
  /**
112
   * Сортированный список поддерживаемых параметров
113
   *
114
   * @var string[] $allowedParams
115
   */
116
  protected $allowedParams = array(
117
    self::PARAM_HTML        => '',
118
    // Will be dumped for all tags which have |html
119
    self::PARAM_HTML_HEIGHT => self::PARAM_HTML,
120
    self::PARAM_HTML_WIDTH  => self::PARAM_HTML,
121
122
    self::PARAM_SKIN => '',
123
  );
124
  /**
125
   * Список полностью отрендеренных путей
126
   *
127
   * @var string[] $container
128
   */
129
  protected $container = array();
130
  /**
131
   * Название скина
132
   *
133
   * @var string $name
134
   */
135
  protected $name = '';
136
137
  /**
138
   * Cached value of no image string
139
   *
140
   * @var string $noImage
141
   */
142
  protected $noImage;
143
144
  /*
145
146
  Класс будет хранить инфу о скинах и их наследовании в привязке к темплейту
147
148
  Должно быть статик-хранилище, которое будет хранить между экземплярами класса инфу о других скинах - для наследования
149
150
  Должен быть метод парсинга конфигурации скина
151
152
  Должен быть статик-метод, который будет вызываться из PTL для парсинга I_xxx тэгов
153
154
  Иконки перекрываются загрузкой нестандартных иконок, если чо
155
156
  Бэкграунд - с ним надо что-то порешать. Например - не использовать. Или тоже перекрывать в CSS
157
    Типа, сделать пустой скин.цсс для ЭпикБлю, основные цвета прописать в _template.css, а в остальных просто перекрывать
158
159
  */
160
161
  /**
162
   * Точка входа
163
   *
164
   * @param string   $image_tag
165
   * @param template $template
166
   *
167
   * @return string
168
   */
169
  public static function image_url($image_tag, $template) {
170
    return SN::$gc->skinModel->getImageCurrent($image_tag, $template);
171
  }
172
173
  /**
174
   * skin constructor.
175
   *
176
   * @param mixed|null|string $skinName
177
   * @param SkinModel         $skinModel
178
   */
179
  public function __construct($skinName = DEFAULT_SKINPATH, $skinModel) {
180
    $this->model = $skinModel;
181
    $this->name  = $skinName;
182
183
    $this->root_http_relative     = 'skins/' . $this->name . '/'; // Пока стоит base="" в body SN_ROOT_VIRTUAL - не нужен
184
    $this->root_physical_absolute = SN_ROOT_PHYSICAL . $this->root_http_relative;
185
    // Искать скин среди пользовательских - когда будет конструктор скинов
186
    // Может не быть файла конфигурации - тогда используется всё "по дефаулту". Т.е. поданная строка - это именно имя файла
187
188
    $this->loadIniFile();
189
    $this->setParentFromConfig();
190
191
    // Пытаемся скомпилировать _no_image заранее
192
    $model       = $this->model;
193
    $noImageID   = $model::NO_IMAGE_ID;
194
    $noImagePath = $model::NO_IMAGE_PATH;
195
196
    // Заглушка на самый крайний случай - когда скин является корневым и у него нет _no_image
197
    if (empty($this->config[$noImageID]) && !$this->parent) {
198
      // Если нет парента - берем хардкод
199
      // Используем стандартный файл из движка
200
      $this->container[$noImageID] = $this->compile_try_path($noImageID, $noImagePath);
201
      // Проверка, что файл - отсутствует. Если да - это повреждение движка
202
      // TODO - throw exception
203
      empty($this->container[$noImageID]) ? die('Game file missing: ' . $noImagePath) : false;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
204
    } else {
205
      $this->container[$noImageID] = $this->imageFromPTLTag(
206
        new PTLTag(SKIN_IMAGE_MISSED_FIELD, null, $this->allowedParams)
207
      );
208
    }
209
210
    $this->noImage = $this->container[$noImageID];
211
212
    return $this;
213
  }
214
215
  /**
216
   * @inheritdoc
217
   */
218
  public function getName() {
219
    return $this->name;
220
  }
221
222
  /**
223
   * @inheritdoc
224
   */
225
  public function imageFromStringTag($stringTag, $template = null) {
226
    return $this->imageFromPTLTag(new PTLTag($stringTag, $template, $this->allowedParams));
227
  }
228
229
  /**
230
   * Возвращает строку для вывода в компилированном темплейте PTL
231
   *
232
   * @param PTLTag $ptlTag
233
   *
234
   * @return string
235
   */
236
  public function imageFromPTLTag($ptlTag) {
237
    // Проверяем наличие ключа RIT в хранилища. В нём не может быть несуществующих файлов по построению
238
    $cacheKey = $ptlTag->getCacheKey();
239
    if (!empty($this->container[$cacheKey])) {
240
      return $this->container[$cacheKey];
241
    }
242
243
    // Шорткат
244
    $imageId = $ptlTag->resolved;
245
246
    $this->tryWebp($imageId);
247
248
    // Нет ключа RIT в контейнере - обсчёт пути для RIT из конфигурации
249
    empty($this->container[$imageId]) && !empty($this->config[$imageId])
250
      ? $this->compile_try_path($imageId, $this->config[$imageId])
251
      : false;
252
253
    // Всё еще пусто? Может у нас не image ID, а просто путь к файлу?
254
    empty($this->container[$imageId]) ? $this->compile_try_path($imageId, $imageId) : false;
255
256
    // Нет - image ID не является путём к файлу. Пора обратиться к предкам за помощью...
257
    // Пытаемся вытащить путь из родителя и применить к нему свои параметры
258
    // Тащим по ID изображения, а не по ТЭГУ - мало ли что там делает с путём родитель и как преобразовывает его в строку?
259
    if (empty($this->container[$imageId]) && !empty($this->parent)) {
260
      $this->container[$imageId] = $this->parent->imageFromPTLTag(new PTLTag($imageId, $ptlTag->template, $this->allowedParams));
261
    }
262
263
    // Если у родителя нет картинки - он вернет пустую строку. Тогда нам надо использовать заглушку - свою или родительскую
264
    empty($this->container[$imageId]) ? $this->container[$imageId] = $this->noImage : false;
265
266
    return !empty($this->container[$imageId]) ? $this->apply_params($ptlTag) : '';
267
  }
268
269
  /**
270
   * Проверка физического наличия файла с картинкой
271
   *
272
   * @param string $image_id
273
   * @param string $file_path
274
   *
275
   * @return string
276
   */
277
  protected function compile_try_path($image_id, $file_path) {
278
    $relative_path = strpos($file_path, '/') !== 0
279
      ? $this->root_http_relative . $file_path
280
      // Если первый символ пути '/' - значит это путь от HTTP-корня
281
      // Откусываем символ и пользуем остальное в качестве пути
282
      : substr($file_path, 1);
283
284
    return is_file(SN_ROOT_PHYSICAL . $relative_path) ? $this->container[$image_id] = SN_ROOT_VIRTUAL . $relative_path : '';
285
  }
286
287
  /**
288
   * @param PTLTag $ptlTag
289
   *
290
   * @return string
291
   */
292
  protected function apply_params(PTLTag $ptlTag) {
293
    if (!is_object($ptlTag) || empty($ptlTag->params) || !is_array($ptlTag->params)) {
294
      return $this->container[$ptlTag->resolved];
295
    }
296
297
    $params       = $ptlTag->params;
298
    $image_string = $this->container[$ptlTag->resolved];
299
300
    // Здесь автоматически произойдёт упорядочивание параметров
301
302
    // Параметр 'skin' - использовать изображение из другого скина
303
    if (array_key_exists(self::PARAM_SKIN, $params)) {
304
      if ($params[self::PARAM_SKIN] == $this->name) {
305
        // If skin - is this skin - then removing this param from list
306
        $ptlTag->removeParam(self::PARAM_SKIN);
307
      } else {
308
        $skin         = $this->model->getSkin($params[self::PARAM_SKIN]);
309
        $image_string = $skin->imageFromStringTag($ptlTag->resolved, $ptlTag->template);
310
      }
311
    }
312
313
    // Параметр 'html' - выводить изображение в виде HTML-тэга
314
    if (array_key_exists(self::PARAM_HTML, $params)) {
315
      $htmlParams   = '';
316
      $paramsNoHtml = $params;
317
      unset($paramsNoHtml[self::PARAM_HTML]);
318
      // Just dump other params
319
      foreach ($paramsNoHtml as $name => $data) {
320
        if ($this->allowedParams[$name] != self::PARAM_HTML) {
321
          continue;
322
        }
323
324
        $htmlParams .= ' ' . $name . '=' . $data;
325
      }
326
327
      $image_string = "<img src=\"{$image_string}\" {$htmlParams} />";
328
    }
329
330
    return $this->container[$ptlTag->getCacheKey()] = $image_string;
331
  }
332
333
  /**
334
   * Loads skin configuration
335
   */
336
  protected function loadIniFile() {
337
    // Проверка на корректность и существование пути
338
    if (!is_file($this->root_physical_absolute . $this->iniFileName)) {
339
      return;
340
    }
341
342
    // Пытаемся распарсить файл
343
    // По секциям? images и config? Что бы не копировать конфигурацию? Или просто unset(__inherit) а затем заново записать
344
    $aConfig = parse_ini_file($this->root_physical_absolute . $this->iniFileName);
345
    if (empty($aConfig)) {
346
      return;
347
    }
348
349
    $this->config = $aConfig;
350
  }
351
352
  protected function setParentFromConfig() {
353
    // Проверка на _inherit
354
    if (empty($this->config['_inherit'])) {
355
      return;
356
    }
357
358
    $parentName = $this->config['_inherit'];
359
    // Если скин наследует себя...
360
    if ($parentName == $this->name) {
361
      // TODO - определять более сложные случаи циклических ссылок в _inherit
362
      // TODO - throw exception
363
      die('">circular skin inheritance!');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
364
    }
365
366
    $this->parent = $this->model->getSkin($parentName);
367
  }
368
369
  /**
370
   * @param string $imageId Internal Image ID to try
371
   */
372
  private function tryWebp($imageId) {
373
    if (!is_object(SN::$gc->theUser) || !SN::$gc->theUser->isWebpSupported()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression SN::gc->theUser->isWebpSupported() of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
374
      return;
375
    }
376
    if (!empty($this->container[$imageId])) {
377
      // Something already there - nothing to do
378
      return;
379
    }
380
381
    $webpImageId = $imageId . self::WEBP_SUFFIX;
382
    if (empty($this->config[$webpImageId])) {
383
      // No WebP alternative - nothing to do
384
      // We WILL NOT check for parent if there is no WebP alternative!
385
      return;
386
    }
387
388
    // Trying to use WebP variant as original image
389
    $this->compile_try_path($imageId, $this->config[$webpImageId]);
390
391
    // Ready or not - we're out of here
392
  }
393
394
}
395