Completed
Push — master ( 13bd05...68fc02 )
by Alex
04:24
created

Parser   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 352
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 7

Test Coverage

Coverage 100%

Importance

Changes 12
Bugs 2 Features 1
Metric Value
wmc 40
c 12
b 2
f 1
lcom 4
cbo 7
dl 0
loc 352
ccs 104
cts 104
cp 1
rs 8.2609

21 Methods

Rating   Name   Duplication   Size   Complexity  
A parse() 0 12 2
A checkBodyStructure() 0 17 4
A setDateFormats() 0 4 1
A guessDateFormat() 0 11 3
A getFactory() 0 4 1
A setFactory() 0 6 1
A addAcceptableItem() 0 8 1
A addValidItem() 0 8 2
A isValid() 0 11 4
A convertToDateTime() 0 12 2
A resetTimezone() 0 4 1
A getAttributeValue() 0 11 3
A createMedia() 0 9 1
A searchAttributeValue() 0 11 3
canHandle() 0 1 ?
parseBody() 0 1 ?
handleEnclosure() 0 1 ?
A getSystemTimezone() 0 8 2
A newItem() 0 8 2
A registerNamespaces() 0 11 3
A getAdditionalNamespacesElements() 0 14 4

How to fix   Complexity   

Complex Class

Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Rss/Atom Bundle for Symfony 2.
5
 *
6
 *
7
 * @license http://opensource.org/licenses/lgpl-3.0.html LGPL
8
 * @copyright (c) 2013, Alexandre Debril
9
 */
10
namespace Debril\RssAtomBundle\Protocol;
11
12
use Debril\RssAtomBundle\Protocol\Filter\ModifiedSince;
13
use Debril\RssAtomBundle\Protocol\Parser\Item;
14
use Debril\RssAtomBundle\Exception\ParserException;
15
use Debril\RssAtomBundle\Protocol\Parser\Factory;
16
use Debril\RssAtomBundle\Protocol\Parser\Media;
17
18
/**
19
 * Class Parser.
20
 */
21
abstract class Parser
22
{
23
    /**
24
     * System's time zone.
25
     *
26
     * @var \DateTimeZone
27
     */
28
    protected static $timezone;
29
30
    /**
31
     * List of mandatory fields.
32
     *
33
     * @var string[]
34
     */
35
    protected $mandatoryFields = array();
36
37
    /**
38
     * Feed's date format.
39
     *
40
     * @var string[]
41
     */
42
    protected $dateFormats = array();
43
44
    /**
45
     * @var Factory
46
     */
47
    protected $factory;
48
49
    /**
50
     * Parses the feed's body to create a FeedContent instance.
51
     *
52
     * @param \SimpleXMLElement                            $xmlBody
53
     * @param \Debril\RssAtomBundle\Protocol\FeedInterface $feed
54
     * @param array                                        $filters
55
     *
56
     * @throws ParserException
57
     *
58
     * @return FeedInterface
59
     */
60 5
    public function parse(\SimpleXMLElement $xmlBody, FeedInterface $feed, array $filters = array())
61
    {
62 5
        if (!$this->canHandle($xmlBody)) {
63 1
            throw new ParserException('this is not a supported format');
64
        }
65
66 4
        $this->checkBodyStructure($xmlBody);
67
68 4
        $xmlBody = $this->registerNamespaces($xmlBody);
69
70 4
        return $this->parseBody($xmlBody, $feed, $filters);
71
    }
72
73
    /**
74
     * @param \SimpleXMLElement $body
75
     *
76
     * @throws ParserException
77
     */
78 5
    protected function checkBodyStructure(\SimpleXMLElement $body)
79
    {
80 5
        $errors = array();
81
82 5
        foreach ($this->mandatoryFields as $field) {
83 5
            if (!isset($body->$field)) {
84 2
                $errors[] = "missing {$field}";
85 2
            }
86 5
        }
87
88 5
        if (0 < count($errors)) {
89 2
            $report = implode(', ', $errors);
90 2
            throw new ParserException(
91 2
                "error while parsing the feed : {$report}"
92 2
            );
93
        }
94 3
    }
95
96
    /**
97
     * @param array $dates
98
     */
99 10
    public function setDateFormats(array $dates)
100
    {
101 10
        $this->dateFormats = $dates;
102 10
    }
103
104
    /**
105
     * @param string $date
106
     *
107
     * @return string date Format
108
     *
109
     * @throws ParserException
110
     */
111 9
    public function guessDateFormat($date)
112
    {
113 9
        foreach ($this->dateFormats as $format) {
114 9
            $test = \DateTime::createFromFormat($format, $date);
115 9
            if ($test instanceof \DateTime) {
116 6
                return $format;
117
            }
118 6
        }
119
120 3
        throw new ParserException('Impossible to guess date format : '.$date);
121
    }
122
123
    /**
124
     * @return ItemInInterface
125
     */
126 3
    public function newItem()
127
    {
128 3
        if ($this->getFactory() instanceof Factory) {
129 1
            return $this->getFactory()->newItem();
130
        }
131
132 2
        return new Item();
133
    }
134
135
    /**
136
     * @return Factory
137
     */
138 3
    public function getFactory()
139
    {
140 3
        return $this->factory;
141
    }
142
143
    /**
144
     * @param Factory $factory
145
     *
146
     * @return Parser
147
     */
148 2
    public function setFactory(Factory $factory)
149
    {
150 2
        $this->factory = $factory;
151
152 2
        return $this;
153
    }
154
155
    /**
156
     * @deprecated since 1.3.0 replaced by addValidItem
157
     *
158
     * @param FeedInInterface $feed
159
     * @param ItemInInterface $item
160
     * @param \DateTime       $modifiedSince
161
     *
162
     * @return $this
163
     */
164
    public function addAcceptableItem(FeedInInterface $feed, ItemInInterface $item, \DateTime $modifiedSince)
165
    {
166
        $filters = array(
167
            new ModifiedSince($modifiedSince),
168
        );
169
170
        return $this->addValidItem($feed, $item, $filters);
171
    }
172
173
    /**
174
     * @param FeedInInterface $feed
175
     * @param ItemInInterface $item
176
     * @param array           $filters
177
     *
178
     * @return $this
179
     */
180 3
    public function addValidItem(FeedInInterface $feed, ItemInInterface $item, array $filters = array())
181
    {
182 3
        if ($this->isValid($item, $filters)) {
183 3
            $feed->addItem($item);
184 3
        }
185
186 3
        return $this;
187
    }
188
189
    /**
190
     * @param ItemInInterface $item
191
     * @param array           $filters
192
     *
193
     * @return bool
194
     */
195 3
    public function isValid(ItemInInterface $item, array $filters = array())
196
    {
197 3
        $valid = true;
198 3
        foreach ($filters as $filter) {
199 3
            if ($filter instanceof FilterInterface) {
200 3
                $valid = $filter->isValid($item) ? $valid : false;
0 ignored issues
show
Compatibility introduced by
$item of type object<Debril\RssAtomBun...otocol\ItemInInterface> is not a sub-type of object<Debril\RssAtomBundle\Protocol\Parser\Item>. It seems like you assume a concrete implementation of the interface Debril\RssAtomBundle\Protocol\ItemInInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
201 3
            }
202 3
        }
203
204 3
        return $valid;
205
    }
206
207
    /**
208
     * Creates a DateTime instance for the given string. Default format is RFC2822.
209
     *
210
     * @param string $string
211
     * @param string $format
212
     *
213
     * @return \DateTime
214
     */
215 5
    public static function convertToDateTime($string, $format = \DateTime::RFC2822)
216
    {
217 5
        $date = \DateTime::createFromFormat($format, $string);
218
219 5
        if (!$date instanceof \DateTime) {
220 1
            throw new ParserException("date is the wrong format : {$string} - expected {$format}");
221
        }
222
223 4
        $date->setTimezone(static::getSystemTimezone());
224
225 4
        return $date;
226
    }
227
228
    /**
229
     * Returns the system's timezone.
230
     *
231
     * @return \DateTimeZone
232
     */
233 4
    public static function getSystemTimezone()
234
    {
235 4
        if (is_null(static::$timezone)) {
236 1
            static::$timezone = new \DateTimeZone(date_default_timezone_get());
237 1
        }
238
239 4
        return static::$timezone;
240
    }
241
242
    /**
243
     * Reset the system's time zone.
244
     */
245 1
    public static function resetTimezone()
246
    {
247 1
        static::$timezone = null;
248 1
    }
249
250
    /**
251
     * register Namespaces.
252
     *
253
     * @param \SimpleXMLElement $xmlBody
254
     *
255
     * @return \SimpleXMLElement
256
     */
257 3
    protected function registerNamespaces(\SimpleXMLElement $xmlBody)
258
    {
259 3
        $namespaces = $xmlBody->getNamespaces(true);
260 3
        foreach ($namespaces as $prefix => $ns) {
261 3
            if ($prefix != '') {
262 1
                $xmlBody->registerXPathNamespace($prefix, $ns);
263 1
            }
264 3
        }
265
266 3
        return $xmlBody;
267
    }
268
269
    /**
270
     * @param \SimpleXMLElement $xmlElement
271
     * @param array             $namespaces
272
     *
273
     * @return array
274
     */
275 3
    protected function getAdditionalNamespacesElements(\SimpleXMLElement $xmlElement, $namespaces)
276
    {
277 3
        $additional = array();
278 3
        foreach ($namespaces as $prefix => $ns) {
279 3
            if ($prefix != '') {
280 1
                $additionalElement = $xmlElement->children($ns);
281 1
                if (!empty($additionalElement)) {
282 1
                    $additional[$prefix] = $additionalElement;
283 1
                }
284 1
            }
285 3
        }
286
287 3
        return $additional;
288
    }
289
290
    /**
291
     * @param \SimpleXMLElement $element
292
     * @param string            $attributeName
293
     *
294
     * @return string|null
295
     */
296 6
    public function getAttributeValue(\SimpleXMLElement $element, $attributeName)
297
    {
298 6
        $attributes = $element[0]->attributes();
299 6
        foreach ($attributes as $name => $value) {
300 6
            if (strcasecmp($name, $attributeName) === 0) {
301 6
                return (string) $value;
302
            }
303 6
        }
304
305 5
        return;
306
    }
307
308
    /**
309
     * @param \SimpleXMLElement $element
310
     *
311
     * @return Media
312
     */
313 2
    public function createMedia(\SimpleXMLElement $element)
314
    {
315 2
        $media = new Media();
316 2
        $media->setUrl($this->searchAttributeValue($element, array('url', 'href', 'link')))
317 2
              ->setType($this->getAttributeValue($element, 'type'))
318 2
              ->setLength($this->getAttributeValue($element, 'length'));
319
320 2
        return $media;
321
    }
322
323
    /**
324
     * Looks for an attribute value under different possible names.
325
     *
326
     * @param \SimpleXMLElement $element
327
     * @param array             $names
328
     *
329
     * @return null|string|void
330
     */
331 3
    public function searchAttributeValue(\SimpleXMLElement $element, array $names)
332
    {
333 3
        foreach ($names as $name) {
334 3
            $value = $this->getAttributeValue($element, $name);
335 3
            if (!is_null($value)) {
336 3
                return (string) $value;
337
            }
338 3
        }
339
340 2
        return;
341
    }
342
343
    /**
344
     * Tells if the parser can handle the feed or not.
345
     *
346
     * @param \SimpleXMLElement $xmlBody
347
     *
348
     * @return bool
349
     */
350
    abstract public function canHandle(\SimpleXMLElement $xmlBody);
351
352
    /**
353
     * Performs the actual conversion into a FeedContent instance.
354
     *
355
     * @param \SimpleXMLElement $body
356
     * @param FeedInterface     $feed
357
     * @param array             $filters
358
     *
359
     * @return FeedInInterface
360
     */
361
    abstract protected function parseBody(\SimpleXMLElement $body, FeedInterface $feed, array $filters);
362
363
    /**
364
     * Handles enclosures if any.
365
     *
366
     * @param \SimpleXMLElement $element
367
     * @param ItemInInterface   $item
368
     *
369
     * @return $this
370
     */
371
    abstract protected function handleEnclosure(\SimpleXMLElement $element, ItemInInterface $item);
372
}
373