Issues (1369)

classes/SkinV2.php (3 issues)

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
 * - Автоматическая поддержка WebP для браузеров, что поддерживают WebP с фоллбэком на обычный формат
48
 */
49
class SkinV2 implements SkinInterface {
50
  const PARAM_HTML = 'html';
51
  const PARAM_HTML_HEIGHT = 'height';
52
  const PARAM_HTML_WIDTH = 'width';
53
  // Use skin for image
54
  const PARAM_SKIN = 'skin';
55
  const WEBP_SUFFIX = '_webp';
56
57
  /**
58
   * @var string $iniFileName
59
   */
60
  protected $iniFileName = 'skin.ini';
61
62
  /**
63
   * @var SkinModel $model
64
   */
65
  protected $model;
66
67
68
  /**
69
   * Флаг инициализации статического объекта
70
   *
71
   * @var bool
72
   */
73
  protected static $is_init = false;
74
  /**
75
   * Список скинов
76
   * TODO Переделать под контейнер
77
   *
78
   * @var self[] $skin_list
79
   */
80
  protected static $skin_list = array();
81
  /**
82
   * Текущий скин
83
   *
84
   * @var self|null
85
   */
86
  protected static $active = null;
87
88
  /**
89
   * HTTP-путь к файлам скина относительно корня движка
90
   *
91
   * @var string
92
   */
93
  protected $root_http_relative = '';
94
  /**
95
   * Абсолютный физический путь к директории скина
96
   *
97
   * @var string
98
   */
99
  protected $root_physical_absolute = '';
100
  /**
101
   * Родительский скин
102
   *
103
   * @var SkinInterface|null
104
   */
105
  protected $parent = null;
106
  /**
107
   * Конфигурация скина - читается из INI-файла
108
   *
109
   * @var array
110
   */
111
  protected $config = array();
112
  /**
113
   * Сортированный список поддерживаемых параметров
114
   *
115
   * @var string[] $allowedParams
116
   */
117
  protected $allowedParams = array(
118
    self::PARAM_HTML        => '',
119
    // Will be dumped for all tags which have |html
120
    self::PARAM_HTML_HEIGHT => self::PARAM_HTML,
121
    self::PARAM_HTML_WIDTH  => self::PARAM_HTML,
122
123
    self::PARAM_SKIN => '',
124
  );
125
  /**
126
   * Список полностью отрендеренных путей
127
   *
128
   * @var string[] $container
129
   */
130
  protected $container = array();
131
  /**
132
   * Название скина
133
   *
134
   * @var string $name
135
   */
136
  protected $name = '';
137
138
  /**
139
   * Cached value of no image string
140
   *
141
   * @var string $noImage
142
   */
143
  protected $noImage;
144
145
  /*
146
147
  Класс будет хранить инфу о скинах и их наследовании в привязке к темплейту
148
149
  Должно быть статик-хранилище, которое будет хранить между экземплярами класса инфу о других скинах - для наследования
150
151
  Должен быть метод парсинга конфигурации скина
152
153
  Должен быть статик-метод, который будет вызываться из PTL для парсинга I_xxx тэгов
154
155
  Иконки перекрываются загрузкой нестандартных иконок, если чо
156
157
  Бэкграунд - с ним надо что-то порешать. Например - не использовать. Или тоже перекрывать в CSS
158
    Типа, сделать пустой скин.цсс для ЭпикБлю, основные цвета прописать в _template.css, а в остальных просто перекрывать
159
160
  */
161
162
  /**
163
   * Точка входа
164
   *
165
   * @param string   $image_tag
166
   * @param template $template
167
   *
168
   * @return string
169
   */
170
  public static function image_url($image_tag, $template) {
171
    return SN::$gc->skinModel->getImageCurrent($image_tag, $template);
172
  }
173
174
  /**
175
   * skin constructor.
176
   *
177
   * @param mixed|null|string $skinName
178
   * @param SkinModel         $skinModel
179
   */
180
  public function __construct($skinName = DEFAULT_SKINPATH, $skinModel) {
181
    $this->model = $skinModel;
182
    $this->name  = $skinName;
183
184
    $this->root_http_relative     = 'skins/' . $this->name . '/'; // Пока стоит base="" в body SN_ROOT_VIRTUAL - не нужен
185
    $this->root_physical_absolute = SN_ROOT_PHYSICAL . $this->root_http_relative;
186
    // Искать скин среди пользовательских - когда будет конструктор скинов
187
    // Может не быть файла конфигурации - тогда используется всё "по дефаулту". Т.е. поданная строка - это именно имя файла
188
189
    $this->loadIniFile();
190
    $this->setParentFromConfig();
191
192
    // Пытаемся скомпилировать _no_image заранее
193
    $model       = $this->model;
194
    $noImageID   = $model::NO_IMAGE_ID;
195
    $noImagePath = $model::NO_IMAGE_PATH;
196
197
    // Заглушка на самый крайний случай - когда скин является корневым и у него нет _no_image
198
    if (empty($this->config[$noImageID]) && !$this->parent) {
199
      // Если нет парента - берем хардкод
200
      // Используем стандартный файл из движка
201
      $this->container[$noImageID] = $this->compile_try_path($noImageID, $noImagePath);
202
      // Проверка, что файл - отсутствует. Если да - это повреждение движка
203
      // TODO - throw exception
204
      empty($this->container[$noImageID]) ? die('Game file missing: ' . $noImagePath) : false;
0 ignored issues
show
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...
205
    } else {
206
      $this->container[$noImageID] = $this->imageFromPTLTag(
207
        new PTLTag(SKIN_IMAGE_MISSED_FIELD, null, $this->allowedParams)
208
      );
209
    }
210
211
    $this->noImage = $this->container[$noImageID];
212
213
    return $this;
214
  }
215
216
  /**
217
   * @inheritdoc
218
   */
219
  public function getName() {
220
    return $this->name;
221
  }
222
223
  /**
224
   * @inheritdoc
225
   */
226
  public function imageFromStringTag($stringTag, $template = null) {
227
    return $this->imageFromPTLTag(new PTLTag($stringTag, $template, $this->allowedParams));
228
  }
229
230
  /**
231
   * Возвращает строку для вывода в компилированном темплейте PTL
232
   *
233
   * @param PTLTag $ptlTag
234
   *
235
   * @return string
236
   */
237
  public function imageFromPTLTag($ptlTag) {
238
    // Проверяем наличие ключа RIT в хранилища. В нём не может быть несуществующих файлов по построению
239
    $cacheKey = $ptlTag->getCacheKey();
240
    if (!empty($this->container[$cacheKey])) {
241
      return $this->container[$cacheKey];
242
    }
243
244
    // Шорткат
245
    $imageId = $ptlTag->resolved;
246
247
    $this->tryWebp($imageId);
248
249
    // Нет ключа RIT в контейнере - обсчёт пути для RIT из конфигурации
250
    empty($this->container[$imageId]) && !empty($this->config[$imageId])
251
      ? $this->compile_try_path($imageId, $this->config[$imageId])
252
      : false;
253
254
    // Всё еще пусто? Может у нас не image ID, а просто путь к файлу?
255
    empty($this->container[$imageId]) ? $this->compile_try_path($imageId, $imageId) : false;
256
257
    // Нет - image ID не является путём к файлу. Пора обратиться к предкам за помощью...
258
    // Пытаемся вытащить путь из родителя и применить к нему свои параметры
259
    // Тащим по ID изображения, а не по ТЭГУ - мало ли что там делает с путём родитель и как преобразовывает его в строку?
260
    if (empty($this->container[$imageId]) && !empty($this->parent)) {
261
      $this->container[$imageId] = $this->parent->imageFromPTLTag(new PTLTag($imageId, $ptlTag->template, $this->allowedParams));
262
    }
263
264
    // Если у родителя нет картинки - он вернет пустую строку. Тогда нам надо использовать заглушку - свою или родительскую
265
    empty($this->container[$imageId]) ? $this->container[$imageId] = $this->noImage : false;
266
267
    return !empty($this->container[$imageId]) ? $this->apply_params($ptlTag) : '';
268
  }
269
270
  /**
271
   * Проверка физического наличия файла с картинкой
272
   *
273
   * @param string $image_id
274
   * @param string $file_path
275
   *
276
   * @return string
277
   */
278
  protected function compile_try_path($image_id, $file_path) {
279
    $relative_path = strpos($file_path, '/') !== 0
280
      ? $this->root_http_relative . $file_path
281
      // Если первый символ пути '/' - значит это путь от HTTP-корня
282
      // Откусываем символ и пользуем остальное в качестве пути
283
      : substr($file_path, 1);
284
285
    return is_file(SN_ROOT_PHYSICAL . $relative_path) ? $this->container[$image_id] = SN_ROOT_VIRTUAL . $relative_path : '';
286
  }
287
288
  /**
289
   * @param PTLTag $ptlTag
290
   *
291
   * @return string
292
   */
293
  protected function apply_params(PTLTag $ptlTag) {
294
    if (!is_object($ptlTag) || empty($ptlTag->params) || !is_array($ptlTag->params)) {
295
      return $this->container[$ptlTag->resolved];
296
    }
297
298
    $params       = $ptlTag->params;
299
    $image_string = $this->container[$ptlTag->resolved];
300
301
    // Здесь автоматически произойдёт упорядочивание параметров
302
303
    // Параметр 'skin' - использовать изображение из другого скина
304
    if (array_key_exists(self::PARAM_SKIN, $params)) {
305
      if ($params[self::PARAM_SKIN] == $this->name) {
306
        // If skin - is this skin - then removing this param from list
307
        $ptlTag->removeParam(self::PARAM_SKIN);
308
      } else {
309
        $skin         = $this->model->getSkin($params[self::PARAM_SKIN]);
310
        $image_string = $skin->imageFromStringTag($ptlTag->resolved, $ptlTag->template);
311
      }
312
    }
313
314
    // Параметр 'html' - выводить изображение в виде HTML-тэга
315
    if (array_key_exists(self::PARAM_HTML, $params)) {
316
      $htmlParams   = '';
317
      $paramsNoHtml = $params;
318
      unset($paramsNoHtml[self::PARAM_HTML]);
319
      // Just dump other params
320
      foreach ($paramsNoHtml as $name => $data) {
321
        if ($this->allowedParams[$name] != self::PARAM_HTML) {
322
          continue;
323
        }
324
325
        $htmlParams .= ' ' . $name . '=' . $data;
326
      }
327
328
      $image_string = "<img src=\"{$image_string}\" {$htmlParams} />";
329
    }
330
331
    return $this->container[$ptlTag->getCacheKey()] = $image_string;
332
  }
333
334
  /**
335
   * Loads skin configuration
336
   */
337
  protected function loadIniFile() {
338
    // Проверка на корректность и существование пути
339
    if (!is_file($this->root_physical_absolute . $this->iniFileName)) {
340
      return;
341
    }
342
343
    // Пытаемся распарсить файл
344
    // По секциям? images и config? Что бы не копировать конфигурацию? Или просто unset(__inherit) а затем заново записать
345
    $aConfig = parse_ini_file($this->root_physical_absolute . $this->iniFileName);
346
    if (empty($aConfig)) {
347
      return;
348
    }
349
350
    $this->config = $aConfig;
351
  }
352
353
  protected function setParentFromConfig() {
354
    // Проверка на _inherit
355
    if (empty($this->config['_inherit'])) {
356
      return;
357
    }
358
359
    $parentName = $this->config['_inherit'];
360
    // Если скин наследует себя...
361
    if ($parentName == $this->name) {
362
      // TODO - определять более сложные случаи циклических ссылок в _inherit
363
      // TODO - throw exception
364
      die('">circular skin inheritance!');
0 ignored issues
show
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...
365
    }
366
367
    $this->parent = $this->model->getSkin($parentName);
368
  }
369
370
  /**
371
   * @param string $imageId Internal Image ID to try
372
   */
373
  private function tryWebp($imageId) {
374
    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...
375
      return;
376
    }
377
    if (!empty($this->container[$imageId])) {
378
      // Something already there - nothing to do
379
      return;
380
    }
381
382
    $webpImageId = $imageId . self::WEBP_SUFFIX;
383
    if (empty($this->config[$webpImageId])) {
384
      // No WebP alternative - nothing to do
385
      // We WILL NOT check for parent if there is no WebP alternative!
386
      return;
387
    }
388
389
    // Trying to use WebP variant as original image
390
    $this->compile_try_path($imageId, $this->config[$webpImageId]);
391
392
    // Ready or not - we're out of here
393
  }
394
395
}
396