Completed
Push — master ( 263d0b...00eb34 )
by Basil
02:23
created

ThemeManager::getThemeDefinitions()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.6026
c 0
b 0
f 0
cc 7
nc 5
nop 0
1
<?php
2
3
namespace luya\theme;
4
5
use luya\base\PackageConfig;
6
use luya\Exception;
7
use luya\helpers\Json;
8
use Yii;
9
use yii\base\InvalidArgumentException;
10
use yii\base\InvalidConfigException;
11
12
/**
13
 * Core theme manager for LUYA.
14
 *
15
 * This component manage available themes via file system and the actual display themes.
16
 *
17
 * @property bool $hasActiveTheme
18
 * @property Theme $activeTheme
19
 *
20
 * @author Bennet Klarhölter <[email protected]>
21
 * @since  1.1.0
22
 */
23
class ThemeManager extends \yii\base\Component
24
{
25
    /**
26
     * Name of the event before the active theme will be setup.
27
     */
28
    const EVENT_BEFORE_SETUP = 'eventBeforeSetup';
29
    
30
    /**
31
     * @var string Name of the theme which should be activated on setup. This is commonly used to defined the active theme when **not**
32
     * using the CMS ThemeManager to switch between themes.
33
     */
34
    public $activeThemeName;
35
    
36
    /**
37
     * @var ThemeConfig[]
38
     */
39
    private $_themes = [];
40
    
41
    /**
42
     * Read the theme.json and create a new \luya\theme\ThemeConfig for the given base path.
43
     *
44
     * @param string $basePath Base path can either be a path to a folder with theme.json files or an absolute path to a theme.json file
45
     * @return ThemeConfig
46
     * @throws Exception
47
     * @throws InvalidConfigException
48
     */
49
    public function loadThemeConfig(string $basePath)
50
    {
51
        if (strpos($basePath, '@') === 0) {
52
            $dir = Yii::getAlias($basePath);
53
        } elseif (strpos($basePath, '/') === 0) {
54
            // absolute path
55
            $dir = $basePath;
56
        } else {
57
            // relative path
58
            throw new InvalidConfigException('Theme base path have to be absolute or alias: ' . $basePath);
59
        }
60
61
        // $basePath is an absolute path = /VENDOR/NAME/theme.json
62
        if (is_file($basePath) && file_exists($basePath)) {
63
            $themeFile = $basePath;
64
            // if basePath is the theme file itself and existing process:
65
            $basePath = pathinfo($basePath, PATHINFO_DIRNAME);
66
        } else {
67
            if (!is_dir($dir) || !is_readable($dir)) {
68
                throw new Exception('Theme directory not exists or readable: ' . $dir);
69
            }
70
71
            $themeFile = $dir . DIRECTORY_SEPARATOR . 'theme.json';
72
            if (!file_exists($themeFile)) {
73
                throw new InvalidConfigException('Theme config file missing at: ' . $themeFile);
74
            }
75
        }
76
        
77
        $config = Json::decode(file_get_contents($themeFile)) ?: [];
78
    
79
        return new ThemeConfig($basePath, $config);
80
    }
81
    
82
    /**
83
     * Setup active theme
84
     *
85
     * @throws Exception
86
     * @throws InvalidConfigException
87
     * @throws \yii\db\Exception
88
     */
89
    public function setup()
90
    {
91
        if ($this->activeTheme instanceof Theme) {
92
            // Active theme already loaded
93
            return;
94
        }
95
        
96
        $basePath = $this->getActiveThemeBasePath();
97
        $this->beforeSetup($basePath);
0 ignored issues
show
Security Bug introduced by
It seems like $basePath defined by $this->getActiveThemeBasePath() on line 96 can also be of type false; however, luya\theme\ThemeManager::beforeSetup() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
98
        
99
        if ($basePath) {
100
            $themeConfig = $this->getThemeByBasePath($basePath);
101
            $theme = new Theme($themeConfig);
102
            $this->activate($theme);
103
        }
104
    }
105
    
106
    /**
107
     * Trigger the {{\luya\theme\ThemeManager::EVENT_BEFORE_SETUP}} event.
108
     *
109
     * @param string $basePath
110
     */
111
    protected function beforeSetup(string &$basePath)
112
    {
113
        $event = new SetupEvent();
114
        $event->basePath = $basePath;
115
        $this->trigger(self::EVENT_BEFORE_SETUP, $event);
116
        
117
        $basePath = $event->basePath;
118
    }
119
    
120
    /**
121
     * Get base path of active theme.
122
     *
123
     * @return string
124
     * @throws \yii\db\Exception
125
     */
126
    protected function getActiveThemeBasePath()
127
    {
128
        if (!empty($this->activeThemeName) && is_string($this->activeThemeName)) {
129
            return $this->activeThemeName;
130
        }
131
        
132
        return false;
133
    }
134
    
135
    /**
136
     * Get all theme configs as array list.
137
     *
138
     * @param bool $throwException Whether an exception should by throw or not. By version 1.0.24 this is disabled by default.
139
     * @return ThemeConfig[]
140
     * @throws \yii\base\Exception
141
     */
142
    public function getThemes($throwException = false)
143
    {
144
        if ($this->_themes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_themes of type luya\theme\ThemeConfig[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
145
            return $this->_themes;
146
        }
147
        
148
        $themeDefinitions = $this->getThemeDefinitions();
149
150
        foreach ($themeDefinitions as $themeDefinition) {
151
            try {
152
                $themeConfig = $this->loadThemeConfig($themeDefinition);
153
                $this->registerTheme($themeConfig);
154
            } catch (\yii\base\Exception $ex) {
155
                if ($throwException) {
156
                    throw $ex;
157
                }
158
            }
159
        }
160
        
161
        return $this->_themes;
162
    }
163
    
164
    /**
165
     * Get theme definitions by search in `@app/themes` and the `Yii::$app->getPackageInstaller()`
166
     *
167
     * @return string[]
168
     */
169
    protected function getThemeDefinitions()
170
    {
171
        $themeDefinitions = [];
172
        
173
        if (file_exists(Yii::getAlias('@app/themes'))) {
174
            foreach (glob(Yii::getAlias('@app/themes/*')) as $dirPath) {
175
                $themeDefinitions[] = "@app/themes/" . basename($dirPath);
176
            }
177
        }
178
        
179
        foreach (Yii::$app->getPackageInstaller()->getConfigs() as $config) {
180
    
181
            /** @var PackageConfig $config */
182
            foreach ($config->themes as $theme) {
183
                if (strpos($theme, '@') === 0 || strpos($theme, '/') === 0) {
184
                    $themeDefinitions[] = $theme;
185
                } else {
186
                    $themeDefinitions[] = preg_replace('#^vendor/#', '@vendor/', $theme);
187
                }
188
            }
189
        }
190
        
191
        return $themeDefinitions;
192
    }
193
    
194
    /**
195
     * @param string $basePath
196
     * @param bool $throwException
197
     *
198
     * @return ThemeConfig
199
     * @throws \yii\base\Exception
200
     * @throws InvalidConfigException
201
     */
202
    public function getThemeByBasePath(string $basePath, $throwException = false)
203
    {
204
        $themes = $this->getThemes($throwException);
205
        
206
        if (!isset($themes[$basePath])) {
207
            throw new InvalidArgumentException("Theme $basePath could not loaded.");
208
        }
209
        
210
        return $themes[$basePath];
211
    }
212
    
213
    /**
214
     * Register a theme config and set the path alias with the name of the theme.
215
     *
216
     * @param ThemeConfig $themeConfig Base path of the theme.
217
     *
218
     * @throws InvalidConfigException
219
     */
220
    protected function registerTheme(ThemeConfig $themeConfig)
221
    {
222
        $basePath = $themeConfig->getBasePath();
223
        if (isset($this->_themes[$basePath])) {
224
            throw new InvalidArgumentException("Theme $basePath already registered.");
225
        }
226
        
227
        $this->_themes[$basePath] = $themeConfig;
228
        
229
        Yii::setAlias('@' . basename($basePath) . 'Theme', $basePath);
230
    }
231
    
232
    /**
233
     * Change the active theme in the \yii\base\View component and set the `activeTheme ` alias to new theme base path.
234
     *
235
     * @param Theme $theme
236
     */
237
    protected function activate(Theme $theme)
238
    {
239
        Yii::$app->view->theme = $theme;
240
        Yii::setAlias('activeTheme', $theme->basePath);
241
        
242
        $this->setActiveTheme($theme);
243
    }
244
    
245
    /**
246
     * @var Theme|null
247
     */
248
    private $_activeTheme;
249
    
250
    /**
251
     * Get the active theme. Null if no theme is activated.
252
     *
253
     * @return Theme|null
254
     */
255
    public function getActiveTheme()
256
    {
257
        return $this->_activeTheme;
258
    }
259
    
260
    /**
261
     * Change the active theme.
262
     *
263
     * @param Theme $theme
264
     */
265
    protected function setActiveTheme(Theme $theme)
266
    {
267
        $this->_activeTheme = $theme;
268
    }
269
    
270
    /**
271
     * Check if a theme is activated.
272
     *
273
     * @return bool
274
     */
275
    public function getHasActiveTheme()
276
    {
277
        return $this->getActiveTheme() !== null;
278
    }
279
}
280