Complex classes like Analytics 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 Analytics, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
159 | class Analytics |
||
160 | { |
||
161 | /** |
||
162 | * URI scheme for the GA API. |
||
163 | * |
||
164 | * @var string |
||
165 | */ |
||
166 | private $uriScheme = 'http'; |
||
167 | |||
168 | /** |
||
169 | * Indicates if the request to GA will be asynchronous (non-blocking). |
||
170 | * |
||
171 | * @var boolean |
||
172 | */ |
||
173 | private $isAsyncRequest = false; |
||
174 | |||
175 | /** |
||
176 | * Endpoint to connect to when sending data to GA. |
||
177 | * |
||
178 | * @var string |
||
179 | */ |
||
180 | private $endpoint = '://www.google-analytics.com/collect'; |
||
181 | |||
182 | /** |
||
183 | * Endpoint to connect to when validating hits. |
||
184 | * @link https://developers.google.com/analytics/devguides/collection/protocol/v1/validating-hits |
||
185 | * |
||
186 | * @var string |
||
187 | */ |
||
188 | private $debugEndpoint = '://www.google-analytics.com/debug/collect'; |
||
189 | |||
190 | /** |
||
191 | * Indicates if the request is in debug mode(validating hits). |
||
192 | * |
||
193 | * @var boolean |
||
194 | */ |
||
195 | private $isDebug = false; |
||
196 | |||
197 | /** |
||
198 | * Holds the single parameters added to the hit. |
||
199 | * |
||
200 | * @var SingleParameter[] |
||
201 | */ |
||
202 | private $singleParameters = []; |
||
203 | |||
204 | /** |
||
205 | * Holds the compound parameters collections added to the hit. |
||
206 | * |
||
207 | * @var CompoundParameterCollection[] |
||
208 | */ |
||
209 | private $compoundParametersCollections = []; |
||
210 | |||
211 | /** |
||
212 | * Holds the HTTP client used to connect to GA. |
||
213 | * |
||
214 | * @var HttpClient |
||
215 | */ |
||
216 | private $httpClient; |
||
217 | |||
218 | /** |
||
219 | * Initializes to a list of all the available parameters to be sent in a hit. |
||
220 | * |
||
221 | * @var array |
||
222 | */ |
||
223 | private $availableParameters = [ |
||
224 | 'ApplicationId' => 'AppTracking\\ApplicationId', |
||
225 | 'ApplicationInstallerId' => 'AppTracking\\ApplicationInstallerId', |
||
226 | 'ApplicationName' => 'AppTracking\\ApplicationName', |
||
227 | 'ApplicationVersion' => 'AppTracking\\ApplicationVersion', |
||
228 | 'ExperimentId' => 'ContentExperiments\\ExperimentId', |
||
229 | 'ExperimentVariant' => 'ContentExperiments\\ExperimentVariant', |
||
230 | 'ContentGroup' => 'ContentGrouping\\ContentGroup', |
||
231 | 'DocumentHostName' => 'ContentInformation\\DocumentHostName', |
||
232 | 'DocumentLocationUrl' => 'ContentInformation\\DocumentLocationUrl', |
||
233 | 'DocumentPath' => 'ContentInformation\\DocumentPath', |
||
234 | 'DocumentTitle' => 'ContentInformation\\DocumentTitle', |
||
235 | 'LinkId' => 'ContentInformation\\LinkId', |
||
236 | 'ScreenName' => 'ContentInformation\\ScreenName', |
||
237 | 'CustomDimension' => 'CustomDimensionsMetrics\\CustomDimension', |
||
238 | 'CustomMetric' => 'CustomDimensionsMetrics\\CustomMetric', |
||
239 | 'CurrencyCode' => 'Ecommerce\\CurrencyCode', |
||
240 | 'ItemCategory' => 'Ecommerce\\ItemCategory', |
||
241 | 'ItemCode' => 'Ecommerce\\ItemCode', |
||
242 | 'ItemName' => 'Ecommerce\\ItemName', |
||
243 | 'ItemPrice' => 'Ecommerce\\ItemPrice', |
||
244 | 'ItemQuantity' => 'Ecommerce\\ItemQuantity', |
||
245 | 'Affiliation' => 'EnhancedEcommerce\\Affiliation', |
||
246 | 'CheckoutStep' => 'EnhancedEcommerce\\CheckoutStep', |
||
247 | 'CheckoutStepOption' => 'EnhancedEcommerce\\CheckoutStepOption', |
||
248 | 'CouponCode' => 'EnhancedEcommerce\\CouponCode', |
||
249 | 'Product' => 'EnhancedEcommerce\\Product', |
||
250 | 'ProductAction' => 'EnhancedEcommerce\\ProductAction', |
||
251 | 'ProductActionList' => 'EnhancedEcommerce\\ProductActionList', |
||
252 | 'ProductCollection' => 'EnhancedEcommerce\\ProductCollection', |
||
253 | 'ProductImpression' => 'EnhancedEcommerce\\ProductImpression', |
||
254 | 'ProductImpressionCollection' => 'EnhancedEcommerce\\ProductImpressionCollection', |
||
255 | 'ProductImpressionListName' => 'EnhancedEcommerce\\ProductImpressionListName', |
||
256 | 'Promotion' => 'EnhancedEcommerce\\Promotion', |
||
257 | 'PromotionAction' => 'EnhancedEcommerce\\PromotionAction', |
||
258 | 'PromotionCollection' => 'EnhancedEcommerce\\PromotionCollection', |
||
259 | 'Revenue' => 'EnhancedEcommerce\\Revenue', |
||
260 | 'Shipping' => 'EnhancedEcommerce\\Shipping', |
||
261 | 'Tax' => 'EnhancedEcommerce\\Tax', |
||
262 | 'TransactionId' => 'EnhancedEcommerce\\TransactionId', |
||
263 | 'EventAction' => 'Event\\EventAction', |
||
264 | 'EventCategory' => 'Event\\EventCategory', |
||
265 | 'EventLabel' => 'Event\\EventLabel', |
||
266 | 'EventValue' => 'Event\\EventValue', |
||
267 | 'ExceptionDescription' => 'Exceptions\\ExceptionDescription', |
||
268 | 'IsExceptionFatal' => 'Exceptions\\IsExceptionFatal', |
||
269 | 'AnonymizeIp' => 'General\\AnonymizeIp', |
||
270 | 'CacheBuster' => 'General\\CacheBuster', |
||
271 | 'DataSource' => 'General\\DataSource', |
||
272 | 'ProtocolVersion' => 'General\\ProtocolVersion', |
||
273 | 'QueueTime' => 'General\\QueueTime', |
||
274 | 'TrackingId' => 'General\\TrackingId', |
||
275 | 'HitType' => 'Hit\\HitType', |
||
276 | 'NonInteractionHit' => 'Hit\\NonInteractionHit', |
||
277 | 'GeographicalOverride' => 'Session\\GeographicalOverride', |
||
278 | 'IpOverride' => 'Session\\IpOverride', |
||
279 | 'SessionControl' => 'Session\\SessionControl', |
||
280 | 'UserAgentOverride' => 'Session\\UserAgentOverride', |
||
281 | 'SocialAction' => 'SocialInteractions\\SocialAction', |
||
282 | 'SocialActionTarget' => 'SocialInteractions\\SocialActionTarget', |
||
283 | 'SocialNetwork' => 'SocialInteractions\\SocialNetwork', |
||
284 | 'DocumentEncoding' => 'SystemInfo\\DocumentEncoding', |
||
285 | 'FlashVersion' => 'SystemInfo\\FlashVersion', |
||
286 | 'JavaEnabled' => 'SystemInfo\\JavaEnabled', |
||
287 | 'ScreenColors' => 'SystemInfo\\ScreenColors', |
||
288 | 'ScreenResolution' => 'SystemInfo\\ScreenResolution', |
||
289 | 'UserLanguage' => 'SystemInfo\\UserLanguage', |
||
290 | 'ViewportSize' => 'SystemInfo\\ViewportSize', |
||
291 | 'ContentLoadTime' => 'Timing\\ContentLoadTime', |
||
292 | 'DnsTime' => 'Timing\\DnsTime', |
||
293 | 'DomInteractiveTime' => 'Timing\\DomInteractiveTime', |
||
294 | 'PageDownloadTime' => 'Timing\\PageDownloadTime', |
||
295 | 'PageLoadTime' => 'Timing\\PageLoadTime', |
||
296 | 'RedirectResponseTime' => 'Timing\\RedirectResponseTime', |
||
297 | 'ServerResponseTime' => 'Timing\\ServerResponseTime', |
||
298 | 'TcpConnectTime' => 'Timing\\TcpConnectTime', |
||
299 | 'UserTimingCategory' => 'Timing\\UserTimingCategory', |
||
300 | 'UserTimingLabel' => 'Timing\\UserTimingLabel', |
||
301 | 'UserTimingTime' => 'Timing\\UserTimingTime', |
||
302 | 'UserTimingVariableName' => 'Timing\\UserTimingVariableName', |
||
303 | 'CampaignContent' => 'TrafficSources\\CampaignContent', |
||
304 | 'CampaignId' => 'TrafficSources\\CampaignId', |
||
305 | 'CampaignKeyword' => 'TrafficSources\\CampaignKeyword', |
||
306 | 'CampaignMedium' => 'TrafficSources\\CampaignMedium', |
||
307 | 'CampaignName' => 'TrafficSources\\CampaignName', |
||
308 | 'CampaignSource' => 'TrafficSources\\CampaignSource', |
||
309 | 'DocumentReferrer' => 'TrafficSources\\DocumentReferrer', |
||
310 | 'GoogleAdwordsId' => 'TrafficSources\\GoogleAdwordsId', |
||
311 | 'GoogleDisplayAdsId' => 'TrafficSources\\GoogleDisplayAdsId', |
||
312 | 'ClientId' => 'User\\ClientId', |
||
313 | 'UserId' => 'User\\UserId', |
||
314 | ]; |
||
315 | |||
316 | /** |
||
317 | * When passed with an argument of TRUE, it will send the hit using HTTPS instead of plain HTTP. |
||
318 | * It parses the available parameters. |
||
319 | * |
||
320 | * @param bool $isSsl |
||
321 | * @throws \InvalidArgumentException |
||
322 | */ |
||
323 | public function __construct($isSsl = false) |
||
334 | |||
335 | /** |
||
336 | * Sets a request to be either synchronous or asynchronous (non-blocking). |
||
337 | * |
||
338 | * @param boolean $isAsyncRequest |
||
339 | * @return $this |
||
340 | */ |
||
341 | public function setAsyncRequest($isAsyncRequest) |
||
347 | |||
348 | /** |
||
349 | * Makes the request to GA asynchronous (non-blocking). |
||
350 | * |
||
351 | * @deprecated Use setAsyncRequest(boolean $isAsyncRequest) instead. To be removed in next major version. |
||
352 | * |
||
353 | * @return $this |
||
354 | */ |
||
355 | public function makeNonBlocking() |
||
361 | |||
362 | /** |
||
363 | * Sets the HttpClient. |
||
364 | * |
||
365 | * @param HttpClient $httpClient |
||
366 | * @return $this |
||
367 | */ |
||
368 | public function setHttpClient(HttpClient $httpClient) |
||
374 | |||
375 | /** |
||
376 | * Gets the HttpClient. |
||
377 | * |
||
378 | * @return HttpClient |
||
379 | */ |
||
380 | private function getHttpClient() |
||
390 | |||
391 | /** |
||
392 | * Gets the full endpoint to GA. |
||
393 | * |
||
394 | * @return string |
||
395 | */ |
||
396 | private function getEndpoint() |
||
400 | |||
401 | /** |
||
402 | * Sets debug mode to true or false. |
||
403 | * |
||
404 | * @param bool $value |
||
405 | * @return \TheIconic\Tracking\GoogleAnalytics\Analytics |
||
406 | */ |
||
407 | public function setDebug($value) |
||
413 | |||
414 | /** |
||
415 | * Sends a hit to GA. The hit will contain in the payload all the parameters added before. |
||
416 | * |
||
417 | * @param $methodName |
||
418 | * @return AnalyticsResponse |
||
419 | * @throws Exception\InvalidPayloadDataException |
||
420 | */ |
||
421 | private function sendHit($methodName) |
||
422 | { |
||
423 | $hitType = strtoupper(substr($methodName, 4)); |
||
424 | |||
425 | $hitConstant = $this->getParameterClassConstant( |
||
426 | 'TheIconic\Tracking\GoogleAnalytics\Parameters\Hit\HitType::HIT_TYPE_' . $hitType, |
||
427 | 'Hit type ' . $hitType . ' is not defined, check spelling' |
||
428 | ); |
||
429 | |||
430 | $this->setHitType($hitConstant); |
||
431 | |||
432 | if (!$this->hasMinimumRequiredParameters()) { |
||
433 | throw new InvalidPayloadDataException(); |
||
434 | } |
||
435 | |||
436 | return $this->getHttpClient()->post( |
||
437 | $this->getUrl(), |
||
438 | $this->isAsyncRequest |
||
439 | ); |
||
440 | } |
||
441 | |||
442 | /** |
||
443 | * Build and returns URL used to send to Google Analytics. |
||
444 | * |
||
445 | * @return string |
||
446 | */ |
||
447 | public function getUrl() |
||
457 | |||
458 | /** |
||
459 | * Validates the minimum required parameters for every GA hit are being sent. |
||
460 | * |
||
461 | * @SuppressWarnings(PHPMD.LongVariable) |
||
462 | * |
||
463 | * @return bool |
||
464 | */ |
||
465 | private function hasMinimumRequiredParameters() |
||
482 | |||
483 | /** |
||
484 | * Sets a parameter action to the value specified by the method call. |
||
485 | * |
||
486 | * @param $parameter |
||
487 | * @param $action |
||
488 | * @return $this |
||
489 | */ |
||
490 | private function setParameterActionTo($parameter, $action) |
||
504 | |||
505 | /** |
||
506 | * Gets a contant from a class dynamically. |
||
507 | * |
||
508 | * @param $constant |
||
509 | * @param $exceptionMsg |
||
510 | * @return mixed |
||
511 | * @throws \BadMethodCallException |
||
512 | */ |
||
513 | private function getParameterClassConstant($constant, $exceptionMsg) |
||
521 | |||
522 | /** |
||
523 | * Sets the value for a parameter. |
||
524 | * |
||
525 | * @param $methodName |
||
526 | * @param array $methodArguments |
||
527 | * @return $this |
||
528 | * @throws \InvalidArgumentException |
||
529 | */ |
||
530 | private function setParameter($methodName, array $methodArguments) |
||
553 | |||
554 | /** |
||
555 | * Adds an item to a compund parameter collection. |
||
556 | * |
||
557 | * @SuppressWarnings(PHPMD.LongVariable) |
||
558 | * |
||
559 | * @param $methodName |
||
560 | * @param array $methodArguments |
||
561 | * @return $this |
||
562 | * @throws \InvalidArgumentException |
||
563 | */ |
||
564 | private function addItem($methodName, array $methodArguments) |
||
595 | |||
596 | /** |
||
597 | * Gets the value for a parameter. |
||
598 | * |
||
599 | * @param $methodName |
||
600 | * @param array $methodArguments |
||
601 | * @return $this |
||
602 | * @throws \InvalidArgumentException |
||
603 | */ |
||
604 | private function getParameter($methodName, array $methodArguments) |
||
641 | |||
642 | /** |
||
643 | * Gets the index value from the arguments. |
||
644 | * |
||
645 | * @param $methodArguments |
||
646 | * @return string |
||
647 | */ |
||
648 | private function getIndexFromArguments($methodArguments) |
||
657 | |||
658 | /** |
||
659 | * Gets the fully qualified name for a parameter. |
||
660 | * |
||
661 | * @param $parameterClass |
||
662 | * @param $methodName |
||
663 | * @return string |
||
664 | * @throws \BadMethodCallException |
||
665 | */ |
||
666 | private function getFullParameterClass($parameterClass, $methodName) |
||
674 | |||
675 | /** |
||
676 | * Routes the method call to the adequate private method. |
||
677 | * |
||
678 | * @param $methodName |
||
679 | * @param array $methodArguments |
||
680 | * @return $this|AnalyticsResponse |
||
681 | * @throws \BadMethodCallException |
||
682 | */ |
||
683 | public function __call($methodName, array $methodArguments) |
||
710 | |||
711 | /** |
||
712 | * Fix typos that went into releases, this way we ensure we don't break scripts in production. |
||
713 | * |
||
714 | * @param string $methodName |
||
715 | * @return string |
||
716 | */ |
||
717 | private function fixTypos($methodName) |
||
726 | } |
||
727 |
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.