Completed
Push — master ( 8fe13c...948046 )
by Kevin
13:36
created

RelevantSelectorParser::prepareSelectorArray()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 14
rs 9.4285
cc 3
eloc 6
nc 4
nop 3
1
<?php namespace Luminaire\Premailer\Parser;
2
3
/**
4
 * Created by Sublime Text 3
5
 *
6
 * @user     Kevin Tanjung
7
 * @website  http://kevintanjung.github.io
8
 * @email    [email protected]
9
 * @date     04/08/2016
10
 * @time     09:09
11
 */
12
13
use Crossjoin\Css\Reader\CssString as StylesheetReader;
14
use Crossjoin\Css\Format\Rule\AtMedia\MediaQuery;
15
use Crossjoin\Css\Format\Rule\AtMedia\MediaRule;
16
use Crossjoin\Css\Format\Rule\Style\StyleRuleSet;
17
use Crossjoin\Css\Format\Rule\Style\StyleSelector;
18
use Illuminate\Support\Arr;
19
use InvalidArgumentException;
20
21
/**
22
 * Retrieve relevant CSS selector from a given CSS rules
23
 *
24
 * @package  \Luminaire\Poseidon\Parser
25
 */
26
class RelevantSelectorParser
27
{
28
29
    /**
30
     * The pseudo classes that can be set in a style attribute and that are
31
     * supported by the Symfony CssSelector (doesn't support CSS4 yet).
32
     *
33
     * @var array
34
     */
35
    protected $allowed_pseudo_classes = [
36
        StyleSelector::PSEUDO_CLASS_FIRST_CHILD,
37
        StyleSelector::PSEUDO_CLASS_ROOT,
38
        StyleSelector::PSEUDO_CLASS_NTH_CHILD,
39
        StyleSelector::PSEUDO_CLASS_NTH_LAST_CHILD,
40
        StyleSelector::PSEUDO_CLASS_NTH_OF_TYPE,
41
        StyleSelector::PSEUDO_CLASS_NTH_LAST_OF_TYPE,
42
        StyleSelector::PSEUDO_CLASS_LAST_CHILD,
43
        StyleSelector::PSEUDO_CLASS_FIRST_OF_TYPE,
44
        StyleSelector::PSEUDO_CLASS_LAST_OF_TYPE,
45
        StyleSelector::PSEUDO_CLASS_ONLY_CHILD,
46
        StyleSelector::PSEUDO_CLASS_ONLY_OF_TYPE,
47
        StyleSelector::PSEUDO_CLASS_EMPTY,
48
        StyleSelector::PSEUDO_CLASS_NOT,
49
    ];
50
51
    /**
52
     * The stylesheet reader instance
53
     *
54
     * @var \Crossjoin\Css\Reader\CssString
55
     */
56
    protected $stylesheet;
57
58
    /**
59
     * The charset of the stylesheet
60
     *
61
     * @var string
62
     */
63
    protected $charset;
64
65
    /**
66
     * Create a new instance of "Relevant Selector Parser"
67
     *
68
     * @param  \Crossjoin\Css\Reader\CssString|string|null  $stylesheet
69
     * @param  string                                       $charset
70
     */
71
    public function __construct($stylesheet = null, $charset = 'UTF-8')
72
    {
73
        $this->charset = $charset;
74
75
        if ( ! is_null($stylesheet))
76
        {
77
            $this->setStylesheetReader($stylesheet);
78
        }
79
    }
80
81
    /**
82
     * Get the relevant selectors
83
     *
84
     * @param  \Crossjoin\Css\Reader\CssString  $reader
0 ignored issues
show
Bug introduced by
There is no parameter named $reader. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
85
     * @return array
86
     */
87
    public function extract()
88
    {
89
        $selectors = [];
90
        $rules     = $this->reader->getStyleSheet()->getRules();
0 ignored issues
show
Bug introduced by
The property reader does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
91
        $relevant  = $this->getRelevantStyleRules($rules);
92
93
        foreach ($relevant as $rule)
94
        {
95
            $this->populateSelectors($selectors, $rule);
96
        }
97
98
        return $selectors;
99
    }
100
101
    /**
102
     * Store the selectors from the rule to the tank
103
     *
104
     * @param  array   &$tank
105
     * @param  array   $selectors
0 ignored issues
show
Bug introduced by
There is no parameter named $selectors. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
106
     * @return void
107
     */
108
    protected function populateSelectors(array &$tank, $rule)
109
    {
110
        foreach ($rule->getSelectors() as $selector)
111
        {
112
            if ( ! $this->isPseudoClassAllowed($selector))
113
            {
114
                continue;
115
            }
116
117
            $position = $this->prepareSelectorArray(
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $position is correct as $this->prepareSelectorAr... $selector->getValue()) (which targets Luminaire\Premailer\Pars...:prepareSelectorArray()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
118
                $tank,
119
                $selector->getSpecificity(),
120
                $selector->getValue()
121
            );
122
123
            foreach ($rule->getDeclarations() as $declaration)
124
            {
125
                $this->storeDeclaration($position, $declaration);
0 ignored issues
show
Bug introduced by
It seems like $position defined by $this->prepareSelectorAr... $selector->getValue()) on line 117 can also be of type null; however, Luminaire\Premailer\Pars...ser::storeDeclaration() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
126
            }
127
        }
128
    }
129
130
    protected function storeDeclaration(array &$tank, $declaration)
131
    {
132
        $tank[] = $declaration;
133
    }
134
135
    /**
136
     * Before building the dictionary of declaration we will need to make sure
137
     * there are an empty array.
138
     *
139
     * @param  array   &$selectors
140
     * @param  string  $specifity
141
     * @param  string  $name
142
     * @return void
143
     */
144
    protected function prepareSelectorArray(array &$selectors, $specifity, $name)
145
    {
146
        if ( ! isset($selectors[$specifity]))
147
        {
148
            $selectors[$specifity] = [];
149
        }
150
151
        if ( ! isset($selectors[$specifity][$name]))
152
        {
153
            $selectors[$specifity][$name] = [];
154
        }
155
156
        return $selectors[$specifity][$name];
157
    }
158
159
    /**
160
     * Set the stylesheet reader instance
161
     *
162
     * @param  \Crossjoin\Css\Reader\CssString|string  $stylesheet
163
     * @return $this
164
     *
165
     * @throws \InvalidArgumentException
166
     */
167
    public function setStylesheetReader($stylesheet)
168
    {
169
        if (is_string($stylesheet))
170
        {
171
            $stylesheet = new StylesheetReader($stylesheet);
172
        }
173
174
        if ( ! $stylesheet instanceof StylesheetReader)
175
        {
176
            throw new InvalidArgumentException('The argument 0 of the [setStylesheetReader] method expects to be a string of CSS or a [Crossjoin\Css\Reader\CssString]');
177
        }
178
179
        $this->reader = $stylesheet;
180
        $this->reader->setEnvironmentEncoding($this->getCharset());
181
182
        return $this;
183
    }
184
185
    /**
186
     * Get the stylesheet reader instance
187
     *
188
     * @return \Crossjoin\Css\Reader\CssString|null
189
     */
190
    public function getStylesheetReader()
191
    {
192
        return $this->reader;
193
    }
194
195
    /**
196
     * Set the charset of the stylesheet
197
     *
198
     * @param  string  $charset
199
     * @return $this
200
     */
201
    public function setCharset($charset)
202
    {
203
        $this->charset = $charset;
204
205
        return $this;
206
    }
207
208
    /**
209
     * Get the charset of the stylesheet
210
     *
211
     * @return string
212
     */
213
    public function getCharset()
214
    {
215
        return $this->charset;
216
    }
217
218
    /**
219
     * Check if a Selector has a valid Pseudo Class
220
     *
221
     * @param  \Crossjoin\Css\Format\Rule\Style\StyleSelector  $selector
222
     * @return bool
223
     */
224
    public function isPseudoClassAllowed(StyleSelector $selector)
225
    {
226
        foreach ($selector->getPseudoClasses() as $pseudo_class)
227
        {
228
            if ( ! in_array($pseudo_class, $this->allowed_pseudo_classes))
229
            {
230
                return false;
231
            }
232
        }
233
234
        return true;
235
    }
236
237
    /**
238
     * Gets all generally relevant style rules
239
     *
240
     * @param  RuleAbstract[]  $rules
241
     * @return StyleRuleSet[]
242
     */
243
    protected function getRelevantStyleRules(array $rules)
244
    {
245
        $style_rules = [];
246
247
        foreach ($rules as $rule)
248
        {
249
            if ($rule instanceof StyleRuleSet)
250
            {
251
                $style_rules[] = $rule;
252
            }
253
254
            if ($rule instanceof MediaRule)
255
            {
256
                $this->getRelevantMediaRule($rule, $style_rules);
257
            }
258
        }
259
260
        return $style_rules;
261
    }
262
263
    /**
264
     * Gets the relevant style rules from a media rule
265
     *
266
     * @param  \Crossjoin\Css\Format\Rule\AtMedia\MediaRule  $rule
267
     * @param  array                                         &$collection
268
     * @return void
269
     */
270
    protected function getRelevantMediaRule(MediaRule $rule, &$collection)
271
    {
272
        foreach ($rule->getQueries() as $media_query)
273
        {
274
            if ( ! $this->isAllowedMediaRule($media_query))
275
            {
276
                continue;
277
            }
278
279
            foreach ($this->getRelevantStyleRules($rule->getRules()) as $style_rule)
0 ignored issues
show
Documentation introduced by
$rule->getRules() is of type array<integer,object<Cro...mat\Rule\RuleAbstract>>, but the function expects a array<integer,object<Lum...r\Parser\RuleAbstract>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
280
            {
281
                $collection[] = $style_rule;
282
            }
283
284
            break;
285
        }
286
    }
287
288
    protected function isAllowedMediaRule($media_query)
289
    {
290
        $type      = $media_query->getType();
291
        $condition = count($media_query->getConditions());
292
293
        return ($type === MediaQuery::TYPE_ALL || $type === MediaQuery::TYPE_SCREEN) && $condition === 0;
294
    }
295
296
}
297