Complex classes like FeedFetcher 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 FeedFetcher, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
39 | class FeedFetcher implements IFeedFetcher { |
||
40 | |||
41 | private $faviconFactory; |
||
42 | private $reader; |
||
43 | private $l10n; |
||
44 | private $time; |
||
45 | |||
46 | public function __construct(Reader $reader, |
||
55 | |||
56 | |||
57 | /** |
||
58 | * This fetcher handles all the remaining urls therefore always returns true |
||
59 | */ |
||
60 | public function canHandle($url) { |
||
63 | |||
64 | |||
65 | /** |
||
66 | * Fetch a feed from remote |
||
67 | * @param string $url remote url of the feed |
||
68 | * @param boolean $getFavicon if the favicon should also be fetched, |
||
69 | * defaults to true |
||
70 | * @param string $lastModified a last modified value from an http header |
||
71 | * defaults to false. If lastModified matches the http header from the feed |
||
72 | * no results are fetched |
||
73 | * @param string $etag an etag from an http header. |
||
74 | * If lastModified matches the http header from the feed |
||
75 | * no results are fetched |
||
76 | * @param bool fullTextEnabled if true tells the fetcher to enhance the |
||
77 | * articles by fetching custom enhanced content |
||
78 | * @param string $basicAuthUser if given, basic auth is set for this feed |
||
79 | * @param string $basicAuthPassword if given, basic auth is set for this |
||
80 | * feed. Ignored if user is null or an empty string |
||
81 | * @throws FetcherException if it fails |
||
82 | * @return array an array containing the new feed and its items, first |
||
83 | * element being the Feed and second element being an array of Items |
||
84 | */ |
||
85 | public function fetch($url, $getFavicon = true, $lastModified = null, |
||
86 | $etag = null, $fullTextEnabled = false, |
||
87 | $basicAuthUser = null, $basicAuthPassword = null) { |
||
88 | try { |
||
89 | if ($basicAuthUser !== null && trim($basicAuthUser) !== '') { |
||
90 | $resource = $this->reader->discover($url, $lastModified, $etag, |
||
91 | $basicAuthUser, |
||
92 | $basicAuthPassword); |
||
93 | } else { |
||
94 | $resource = $this->reader->discover($url, $lastModified, $etag); |
||
95 | } |
||
96 | |||
97 | if (!$resource->isModified()) { |
||
98 | return [null, null]; |
||
99 | } |
||
100 | |||
101 | $location = $resource->getUrl(); |
||
102 | $etag = $resource->getEtag(); |
||
103 | $content = $resource->getContent(); |
||
104 | $encoding = $resource->getEncoding(); |
||
105 | $lastModified = $resource->getLastModified(); |
||
106 | |||
107 | $parser = $this->reader->getParser($location, $content, $encoding); |
||
108 | |||
109 | if ($fullTextEnabled) { |
||
110 | $parser->enableContentGrabber(); |
||
111 | } |
||
112 | |||
113 | $parsedFeed = $parser->execute(); |
||
114 | |||
115 | $feed = $this->buildFeed( |
||
116 | $parsedFeed, $url, $getFavicon, $lastModified, $etag, $location |
||
117 | ); |
||
118 | |||
119 | $items = []; |
||
120 | foreach ($parsedFeed->getItems() as $item) { |
||
121 | $items[] = $this->buildItem($item, $parsedFeed); |
||
122 | } |
||
123 | |||
124 | return [$feed, $items]; |
||
125 | |||
126 | } catch (Exception $ex) { |
||
127 | $this->handleError($ex, $url); |
||
128 | } |
||
129 | |||
130 | } |
||
131 | |||
132 | |||
133 | private function handleError(Exception $ex, $url) { |
||
134 | $msg = $ex->getMessage(); |
||
135 | |||
136 | if ($ex instanceof MalFormedXmlException) { |
||
137 | $msg = $this->l10n->t('Feed contains invalid XML'); |
||
138 | } else if ($ex instanceof SubscriptionNotFoundException) { |
||
139 | $msg = $this->l10n->t('Feed not found: either the website ' . |
||
140 | 'does not provide a feed or blocks access. To rule out ' . |
||
141 | 'blocking, try to download the feed on your server\'s ' . |
||
142 | 'command line using curl: curl ' . $url); |
||
143 | } else if ($ex instanceof UnsupportedFeedFormatException) { |
||
144 | $msg = $this->l10n->t('Detected feed format is not supported'); |
||
145 | } else if ($ex instanceof InvalidCertificateException) { |
||
146 | $msg = $this->buildCurlSslErrorMessage($ex->getCode()); |
||
147 | } else if ($ex instanceof InvalidUrlException) { |
||
148 | $msg = $this->l10n->t('Website not found'); |
||
149 | } else if ($ex instanceof MaxRedirectException) { |
||
150 | $msg = $this->l10n->t('More redirects than allowed, aborting'); |
||
151 | } else if ($ex instanceof MaxSizeException) { |
||
152 | $msg = $this->l10n->t('Bigger than maximum allowed size'); |
||
153 | } else if ($ex instanceof TimeoutException) { |
||
154 | $msg = $this->l10n->t('Request timed out'); |
||
155 | } else if ($ex instanceof UnauthorizedException) { |
||
156 | $msg = $this->l10n->t('Required credentials for feed were ' . |
||
157 | 'either missing or incorrect'); |
||
158 | } else if ($ex instanceof ForbiddenException) { |
||
159 | $msg = $this->l10n->t('Forbidden to access feed'); |
||
160 | } |
||
161 | |||
162 | throw new FetcherException($msg); |
||
163 | } |
||
164 | |||
165 | private function buildCurlSslErrorMessage($errorCode) { |
||
166 | switch ($errorCode) { |
||
167 | case 35: // CURLE_SSL_CONNECT_ERROR |
||
168 | return $this->l10n->t( |
||
169 | 'Certificate error: A problem occurred ' . |
||
170 | 'somewhere in the SSL/TLS handshake. Could be ' . |
||
171 | 'certificates (file formats, paths, permissions), ' . |
||
172 | 'passwords, and others.' |
||
173 | ); |
||
174 | case 51: // CURLE_PEER_FAILED_VERIFICATION |
||
175 | return $this->l10n->t( |
||
176 | 'Certificate error: The remote server\'s SSL ' . |
||
177 | 'certificate or SSH md5 fingerprint was deemed not OK.' |
||
178 | ); |
||
179 | case 58: // CURLE_SSL_CERTPROBLEM |
||
180 | return $this->l10n->t( |
||
181 | 'Certificate error: Problem with the local client ' . |
||
182 | 'certificate.' |
||
183 | ); |
||
184 | case 59: // CURLE_SSL_CIPHER |
||
185 | return $this->l10n->t( |
||
186 | 'Certificate error: Couldn\'t use specified cipher.' |
||
187 | ); |
||
188 | case 60: // CURLE_SSL_CACERT |
||
189 | return $this->l10n->t( |
||
190 | 'Certificate error: Peer certificate cannot be ' . |
||
191 | 'authenticated with known CA certificates.' |
||
192 | ); |
||
193 | case 64: // CURLE_USE_SSL_FAILED |
||
194 | return $this->l10n->t( |
||
195 | 'Certificate error: Requested FTP SSL level failed.' |
||
196 | ); |
||
197 | case 66: // CURLE_SSL_ENGINE_INITFAILED |
||
198 | return $this->l10n->t( |
||
199 | 'Certificate error: Initiating the SSL Engine failed.' |
||
200 | ); |
||
201 | case 77: // CURLE_SSL_CACERT_BADFILE |
||
202 | return $this->l10n->t( |
||
203 | 'Certificate error: Problem with reading the SSL CA ' . |
||
204 | 'cert (path? access rights?)' |
||
205 | ); |
||
206 | case 83: // CURLE_SSL_ISSUER_ERROR |
||
207 | return $this->l10n->t( |
||
208 | 'Certificate error: Issuer check failed' |
||
209 | ); |
||
210 | default: |
||
211 | return $this->l10n->t('Unknown SSL certificate error!'); |
||
212 | } |
||
213 | } |
||
214 | |||
215 | private function decodeTwice($string) { |
||
216 | return html_entity_decode( |
||
217 | html_entity_decode( |
||
218 | $string, ENT_QUOTES | ENT_HTML5, 'UTF-8' |
||
219 | ), |
||
220 | ENT_QUOTES | ENT_HTML5, 'UTF-8' |
||
221 | ); |
||
222 | } |
||
223 | |||
224 | |||
225 | protected function determineRtl($parsedItem, $parsedFeed) { |
||
235 | |||
236 | |||
237 | protected function buildItem($parsedItem, $parsedFeed) { |
||
238 | $item = new Item(); |
||
239 | $item->setUnread(); |
||
272 | |||
273 | |||
274 | protected function buildFeed($parsedFeed, $url, $getFavicon, $modified, |
||
302 | |||
303 | } |
||
304 |