Completed
Pull Request — master (#2002)
by
unknown
02:41
created

ThemeManager::getThemes()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 9.2728
c 0
b 0
f 0
cc 5
nc 7
nop 1
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
            /** @var PackageConfig $config */
181
            foreach ($config->themes as $theme) {
182
                if (strpos($theme, '@') === 0 || strpos($theme, '/') === 0) {
183
                    $themeDefinitions[] = $theme;
184
                } else {
185
                    $themeDefinitions[] = '@vendor' . DIRECTORY_SEPARATOR . $config->package['packageFolder'] . DIRECTORY_SEPARATOR . $theme;
186
                }
187
            }
188
        }
189
        
190
        return $themeDefinitions;
191
    }
192
    
193
    /**
194
     * @param string $basePath
195
     * @param bool $throwException
196
     *
197
     * @return ThemeConfig
198
     * @throws \yii\base\Exception
199
     * @throws InvalidConfigException
200
     */
201
    public function getThemeByBasePath(string $basePath, $throwException = false)
202
    {
203
        $themes = $this->getThemes($throwException);
204
        
205
        if (!isset($themes[$basePath])) {
206
            throw new InvalidArgumentException("Theme $basePath could not loaded.");
207
        }
208
        
209
        return $themes[$basePath];
210
    }
211
    
212
    /**
213
     * Register a theme config and set the path alias with the name of the theme.
214
     *
215
     * @param ThemeConfig $themeConfig Base path of the theme.
216
     *
217
     * @throws InvalidConfigException
218
     */
219
    protected function registerTheme(ThemeConfig $themeConfig)
220
    {
221
        $basePath = $themeConfig->getBasePath();
222
        if (isset($this->_themes[$basePath])) {
223
            throw new InvalidArgumentException("Theme $basePath already registered.");
224
        }
225
        
226
        $this->_themes[$basePath] = $themeConfig;
227
        
228
        Yii::setAlias('@' . basename($basePath) . 'Theme', $basePath);
229
    }
230
    
231
    /**
232
     * Change the active theme in the \yii\base\View component and set the `activeTheme ` alias to new theme base path.
233
     *
234
     * @param Theme $theme
235
     */
236
    protected function activate(Theme $theme)
237
    {
238
        Yii::$app->view->theme = $theme;
239
        Yii::setAlias('activeTheme', $theme->basePath);
240
        
241
        $this->setActiveTheme($theme);
242
    }
243
    
244
    /**
245
     * @var Theme|null
246
     */
247
    private $_activeTheme;
248
    
249
    /**
250
     * Get the active theme. Null if no theme is activated.
251
     *
252
     * @return Theme|null
253
     */
254
    public function getActiveTheme()
255
    {
256
        return $this->_activeTheme;
257
    }
258
    
259
    /**
260
     * Change the active theme.
261
     *
262
     * @param Theme $theme
263
     */
264
    protected function setActiveTheme(Theme $theme)
265
    {
266
        $this->_activeTheme = $theme;
267
    }
268
    
269
    /**
270
     * Check if a theme is activated.
271
     *
272
     * @return bool
273
     */
274
    public function getHasActiveTheme()
275
    {
276
        return $this->getActiveTheme() !== null;
277
    }
278
}
279