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 |
||
291 | class Analytics |
||
292 | { |
||
293 | /** |
||
294 | * URI scheme for the GA API. |
||
295 | * |
||
296 | * @var string |
||
297 | */ |
||
298 | protected $uriScheme = 'http'; |
||
299 | |||
300 | /** |
||
301 | * Indicates if the request to GA will be asynchronous (non-blocking). |
||
302 | * |
||
303 | * @var boolean |
||
304 | */ |
||
305 | protected $isAsyncRequest = false; |
||
306 | |||
307 | /** |
||
308 | * Endpoint to connect to when sending data to GA. |
||
309 | * |
||
310 | * @var string |
||
311 | */ |
||
312 | protected $endpoint = '://www.google-analytics.com/collect'; |
||
313 | |||
314 | /** |
||
315 | * Endpoint to connect to when validating hits. |
||
316 | * @link https://developers.google.com/analytics/devguides/collection/protocol/v1/validating-hits |
||
317 | * |
||
318 | * @var string |
||
319 | */ |
||
320 | protected $debugEndpoint = '://www.google-analytics.com/debug/collect'; |
||
321 | |||
322 | /** |
||
323 | * Indicates if the request is in debug mode(validating hits). |
||
324 | * |
||
325 | * @var boolean |
||
326 | */ |
||
327 | protected $isDebug = false; |
||
328 | |||
329 | /** |
||
330 | * Holds the single parameters added to the hit. |
||
331 | * |
||
332 | * @var SingleParameter[] |
||
333 | */ |
||
334 | protected $singleParameters = []; |
||
335 | |||
336 | /** |
||
337 | * Holds the compound parameters collections added to the hit. |
||
338 | * |
||
339 | * @var CompoundParameterCollection[] |
||
340 | */ |
||
341 | protected $compoundParametersCollections = []; |
||
342 | |||
343 | /** |
||
344 | * Holds the HTTP client used to connect to GA. |
||
345 | * |
||
346 | * @var HttpClient |
||
347 | */ |
||
348 | protected $httpClient; |
||
349 | |||
350 | /** |
||
351 | * Indicates if the request to GA will be executed (by default) or not. |
||
352 | * |
||
353 | * @var boolean |
||
354 | */ |
||
355 | protected $isDisabled = false; |
||
356 | |||
357 | |||
358 | /** |
||
359 | * Initializes to a list of all the available parameters to be sent in a hit. |
||
360 | * |
||
361 | * @var array |
||
362 | */ |
||
363 | protected $availableParameters = [ |
||
364 | 'ApplicationId' => 'AppTracking\\ApplicationId', |
||
365 | 'ApplicationInstallerId' => 'AppTracking\\ApplicationInstallerId', |
||
366 | 'ApplicationName' => 'AppTracking\\ApplicationName', |
||
367 | 'ApplicationVersion' => 'AppTracking\\ApplicationVersion', |
||
368 | 'ExperimentId' => 'ContentExperiments\\ExperimentId', |
||
369 | 'ExperimentVariant' => 'ContentExperiments\\ExperimentVariant', |
||
370 | 'ContentGroup' => 'ContentGrouping\\ContentGroup', |
||
371 | 'DocumentHostName' => 'ContentInformation\\DocumentHostName', |
||
372 | 'DocumentLocationUrl' => 'ContentInformation\\DocumentLocationUrl', |
||
373 | 'DocumentPath' => 'ContentInformation\\DocumentPath', |
||
374 | 'DocumentTitle' => 'ContentInformation\\DocumentTitle', |
||
375 | 'LinkId' => 'ContentInformation\\LinkId', |
||
376 | 'ScreenName' => 'ContentInformation\\ScreenName', |
||
377 | 'CustomDimension' => 'CustomDimensionsMetrics\\CustomDimension', |
||
378 | 'CustomMetric' => 'CustomDimensionsMetrics\\CustomMetric', |
||
379 | 'CurrencyCode' => 'Ecommerce\\CurrencyCode', |
||
380 | 'ItemCategory' => 'Ecommerce\\ItemCategory', |
||
381 | 'ItemCode' => 'Ecommerce\\ItemCode', |
||
382 | 'ItemName' => 'Ecommerce\\ItemName', |
||
383 | 'ItemPrice' => 'Ecommerce\\ItemPrice', |
||
384 | 'ItemQuantity' => 'Ecommerce\\ItemQuantity', |
||
385 | 'Affiliation' => 'EnhancedEcommerce\\Affiliation', |
||
386 | 'CheckoutStep' => 'EnhancedEcommerce\\CheckoutStep', |
||
387 | 'CheckoutStepOption' => 'EnhancedEcommerce\\CheckoutStepOption', |
||
388 | 'CouponCode' => 'EnhancedEcommerce\\CouponCode', |
||
389 | 'Product' => 'EnhancedEcommerce\\Product', |
||
390 | 'ProductAction' => 'EnhancedEcommerce\\ProductAction', |
||
391 | 'ProductActionList' => 'EnhancedEcommerce\\ProductActionList', |
||
392 | 'ProductCollection' => 'EnhancedEcommerce\\ProductCollection', |
||
393 | 'ProductImpression' => 'EnhancedEcommerce\\ProductImpression', |
||
394 | 'ProductImpressionCollection' => 'EnhancedEcommerce\\ProductImpressionCollection', |
||
395 | 'ProductImpressionListName' => 'EnhancedEcommerce\\ProductImpressionListName', |
||
396 | 'Promotion' => 'EnhancedEcommerce\\Promotion', |
||
397 | 'PromotionAction' => 'EnhancedEcommerce\\PromotionAction', |
||
398 | 'PromotionCollection' => 'EnhancedEcommerce\\PromotionCollection', |
||
399 | 'Revenue' => 'EnhancedEcommerce\\Revenue', |
||
400 | 'Shipping' => 'EnhancedEcommerce\\Shipping', |
||
401 | 'Tax' => 'EnhancedEcommerce\\Tax', |
||
402 | 'TransactionId' => 'EnhancedEcommerce\\TransactionId', |
||
403 | 'EventAction' => 'Event\\EventAction', |
||
404 | 'EventCategory' => 'Event\\EventCategory', |
||
405 | 'EventLabel' => 'Event\\EventLabel', |
||
406 | 'EventValue' => 'Event\\EventValue', |
||
407 | 'ExceptionDescription' => 'Exceptions\\ExceptionDescription', |
||
408 | 'IsExceptionFatal' => 'Exceptions\\IsExceptionFatal', |
||
409 | 'AnonymizeIp' => 'General\\AnonymizeIp', |
||
410 | 'CacheBuster' => 'General\\CacheBuster', |
||
411 | 'DataSource' => 'General\\DataSource', |
||
412 | 'ProtocolVersion' => 'General\\ProtocolVersion', |
||
413 | 'QueueTime' => 'General\\QueueTime', |
||
414 | 'TrackingId' => 'General\\TrackingId', |
||
415 | 'HitType' => 'Hit\\HitType', |
||
416 | 'NonInteractionHit' => 'Hit\\NonInteractionHit', |
||
417 | 'GeographicalOverride' => 'Session\\GeographicalOverride', |
||
418 | 'IpOverride' => 'Session\\IpOverride', |
||
419 | 'SessionControl' => 'Session\\SessionControl', |
||
420 | 'UserAgentOverride' => 'Session\\UserAgentOverride', |
||
421 | 'SocialAction' => 'SocialInteractions\\SocialAction', |
||
422 | 'SocialActionTarget' => 'SocialInteractions\\SocialActionTarget', |
||
423 | 'SocialNetwork' => 'SocialInteractions\\SocialNetwork', |
||
424 | 'DocumentEncoding' => 'SystemInfo\\DocumentEncoding', |
||
425 | 'FlashVersion' => 'SystemInfo\\FlashVersion', |
||
426 | 'JavaEnabled' => 'SystemInfo\\JavaEnabled', |
||
427 | 'ScreenColors' => 'SystemInfo\\ScreenColors', |
||
428 | 'ScreenResolution' => 'SystemInfo\\ScreenResolution', |
||
429 | 'UserLanguage' => 'SystemInfo\\UserLanguage', |
||
430 | 'ViewportSize' => 'SystemInfo\\ViewportSize', |
||
431 | 'ContentLoadTime' => 'Timing\\ContentLoadTime', |
||
432 | 'DnsTime' => 'Timing\\DnsTime', |
||
433 | 'DomInteractiveTime' => 'Timing\\DomInteractiveTime', |
||
434 | 'PageDownloadTime' => 'Timing\\PageDownloadTime', |
||
435 | 'PageLoadTime' => 'Timing\\PageLoadTime', |
||
436 | 'RedirectResponseTime' => 'Timing\\RedirectResponseTime', |
||
437 | 'ServerResponseTime' => 'Timing\\ServerResponseTime', |
||
438 | 'TcpConnectTime' => 'Timing\\TcpConnectTime', |
||
439 | 'UserTimingCategory' => 'Timing\\UserTimingCategory', |
||
440 | 'UserTimingLabel' => 'Timing\\UserTimingLabel', |
||
441 | 'UserTimingTime' => 'Timing\\UserTimingTime', |
||
442 | 'UserTimingVariableName' => 'Timing\\UserTimingVariableName', |
||
443 | 'CampaignContent' => 'TrafficSources\\CampaignContent', |
||
444 | 'CampaignId' => 'TrafficSources\\CampaignId', |
||
445 | 'CampaignKeyword' => 'TrafficSources\\CampaignKeyword', |
||
446 | 'CampaignMedium' => 'TrafficSources\\CampaignMedium', |
||
447 | 'CampaignName' => 'TrafficSources\\CampaignName', |
||
448 | 'CampaignSource' => 'TrafficSources\\CampaignSource', |
||
449 | 'DocumentReferrer' => 'TrafficSources\\DocumentReferrer', |
||
450 | 'GoogleAdwordsId' => 'TrafficSources\\GoogleAdwordsId', |
||
451 | 'GoogleDisplayAdsId' => 'TrafficSources\\GoogleDisplayAdsId', |
||
452 | 'ClientId' => 'User\\ClientId', |
||
453 | 'UserId' => 'User\\UserId', |
||
454 | ]; |
||
455 | |||
456 | /** |
||
457 | * When passed with an argument of TRUE, it will send the hit using HTTPS instead of plain HTTP. |
||
458 | * It parses the available parameters. |
||
459 | * |
||
460 | * @param bool $isSsl |
||
461 | * @throws \InvalidArgumentException |
||
462 | */ |
||
463 | public function __construct($isSsl = false, $isDisabled = false) |
||
464 | { |
||
465 | if (!is_bool($isSsl)) { |
||
466 | throw new \InvalidArgumentException('First constructor argument "isSSL" must be boolean'); |
||
467 | } |
||
468 | |||
469 | if (!is_bool($isDisabled)) { |
||
470 | throw new \InvalidArgumentException('Second constructor argument "isDisabled" must be boolean'); |
||
471 | } |
||
472 | |||
473 | if ($isSsl) { |
||
474 | $this->uriScheme .= 's'; |
||
475 | $this->endpoint = str_replace('www', 'ssl', $this->endpoint); |
||
476 | } |
||
477 | |||
478 | $this->isDisabled = $isDisabled; |
||
479 | } |
||
480 | |||
481 | /** |
||
482 | * Sets a request to be either synchronous or asynchronous (non-blocking). |
||
483 | * |
||
484 | * @api |
||
485 | * @param boolean $isAsyncRequest |
||
486 | * @return $this |
||
487 | */ |
||
488 | public function setAsyncRequest($isAsyncRequest) |
||
489 | { |
||
490 | $this->isAsyncRequest = $isAsyncRequest; |
||
491 | |||
492 | return $this; |
||
493 | } |
||
494 | |||
495 | /** |
||
496 | * Makes the request to GA asynchronous (non-blocking). |
||
497 | * |
||
498 | * @deprecated Use setAsyncRequest(boolean $isAsyncRequest) instead. To be removed in next major version. |
||
499 | * |
||
500 | * @return $this |
||
501 | */ |
||
502 | public function makeNonBlocking() |
||
503 | { |
||
504 | $this->isAsyncRequest = true; |
||
505 | |||
506 | return $this; |
||
507 | } |
||
508 | |||
509 | /** |
||
510 | * Sets the HttpClient. |
||
511 | * |
||
512 | * @internal |
||
513 | * @param HttpClient $httpClient |
||
514 | * @return $this |
||
515 | */ |
||
516 | public function setHttpClient(HttpClient $httpClient) |
||
517 | { |
||
518 | $this->httpClient = $httpClient; |
||
519 | |||
520 | return $this; |
||
521 | } |
||
522 | |||
523 | /** |
||
524 | * Gets the HttpClient. |
||
525 | * |
||
526 | * @return HttpClient |
||
527 | */ |
||
528 | protected function getHttpClient() |
||
529 | { |
||
530 | if ($this->httpClient === null) { |
||
531 | // @codeCoverageIgnoreStart |
||
532 | $this->setHttpClient(new HttpClient()); |
||
533 | } |
||
534 | // @codeCoverageIgnoreEnd |
||
535 | |||
536 | return $this->httpClient; |
||
537 | } |
||
538 | |||
539 | /** |
||
540 | * Gets the full endpoint to GA. |
||
541 | * |
||
542 | * @return string |
||
543 | */ |
||
544 | protected function getEndpoint() |
||
545 | { |
||
546 | return ($this->isDebug) ? $this->uriScheme . $this->debugEndpoint : $this->uriScheme . $this->endpoint; |
||
547 | } |
||
548 | |||
549 | /** |
||
550 | * Sets debug mode to true or false. |
||
551 | * |
||
552 | * @api |
||
553 | * @param bool $value |
||
554 | * @return \TheIconic\Tracking\GoogleAnalytics\Analytics |
||
555 | */ |
||
556 | public function setDebug($value) |
||
557 | { |
||
558 | $this->isDebug = $value; |
||
559 | |||
560 | return $this; |
||
561 | } |
||
562 | |||
563 | /** |
||
564 | * Sends a hit to GA. The hit will contain in the payload all the parameters added before. |
||
565 | * |
||
566 | * @param $methodName |
||
567 | * @return AnalyticsResponse |
||
568 | * @throws Exception\InvalidPayloadDataException |
||
569 | */ |
||
570 | protected function sendHit($methodName) |
||
571 | { |
||
572 | $hitType = strtoupper(substr($methodName, 4)); |
||
573 | |||
574 | $hitConstant = $this->getParameterClassConstant( |
||
575 | 'TheIconic\Tracking\GoogleAnalytics\Parameters\Hit\HitType::HIT_TYPE_' . $hitType, |
||
576 | 'Hit type ' . $hitType . ' is not defined, check spelling' |
||
577 | ); |
||
578 | |||
579 | $this->setHitType($hitConstant); |
||
580 | |||
581 | if (!$this->hasMinimumRequiredParameters()) { |
||
582 | throw new InvalidPayloadDataException(); |
||
583 | } |
||
584 | |||
585 | if ($this->isDisabled) { |
||
586 | return new NullAnalyticsResponse(); |
||
|
|||
587 | } else { |
||
588 | return $this->getHttpClient()->post( |
||
589 | $this->getUrl(), |
||
590 | $this->isAsyncRequest |
||
591 | ); |
||
592 | } |
||
593 | } |
||
594 | |||
595 | /** |
||
596 | * Build and returns URL used to send to Google Analytics. |
||
597 | * |
||
598 | * @api |
||
599 | * @return string |
||
600 | */ |
||
601 | public function getUrl() |
||
611 | |||
612 | /** |
||
613 | * Validates the minimum required parameters for every GA hit are being sent. |
||
614 | * |
||
615 | * @SuppressWarnings(PHPMD.LongVariable) |
||
616 | * |
||
617 | * @return bool |
||
618 | */ |
||
619 | protected function hasMinimumRequiredParameters() |
||
645 | |||
646 | /** |
||
647 | * Sets a parameter action to the value specified by the method call. |
||
648 | * |
||
649 | * @param $parameter |
||
650 | * @param $action |
||
651 | * @return $this |
||
652 | */ |
||
653 | protected function setParameterActionTo($parameter, $action) |
||
667 | |||
668 | /** |
||
669 | * Gets a contant from a class dynamically. |
||
670 | * |
||
671 | * @param $constant |
||
672 | * @param $exceptionMsg |
||
673 | * @return mixed |
||
674 | * @throws \BadMethodCallException |
||
675 | */ |
||
676 | protected function getParameterClassConstant($constant, $exceptionMsg) |
||
684 | |||
685 | /** |
||
686 | * Sets the value for a parameter. |
||
687 | * |
||
688 | * @param $methodName |
||
689 | * @param array $methodArguments |
||
690 | * @return $this |
||
691 | * @throws \InvalidArgumentException |
||
692 | */ |
||
693 | protected function setParameter($methodName, array $methodArguments) |
||
716 | |||
717 | /** |
||
718 | * Adds an item to a compund parameter collection. |
||
719 | * |
||
720 | * @SuppressWarnings(PHPMD.LongVariable) |
||
721 | * |
||
722 | * @param $methodName |
||
723 | * @param array $methodArguments |
||
724 | * @return $this |
||
725 | * @throws \InvalidArgumentException |
||
726 | */ |
||
727 | protected function addItem($methodName, array $methodArguments) |
||
758 | |||
759 | /** |
||
760 | * Gets the value for a parameter. |
||
761 | * |
||
762 | * @SuppressWarnings(PHPMD.LongVariable) |
||
763 | * |
||
764 | * @param $methodName |
||
765 | * @param array $methodArguments |
||
766 | * @return string |
||
767 | * @throws \InvalidArgumentException |
||
768 | */ |
||
769 | protected function getParameter($methodName, array $methodArguments) |
||
807 | |||
808 | /** |
||
809 | * Gets the index value from the arguments. |
||
810 | * |
||
811 | * @param $methodArguments |
||
812 | * @return string |
||
813 | */ |
||
814 | protected function getIndexFromArguments($methodArguments) |
||
823 | |||
824 | /** |
||
825 | * Gets the fully qualified name for a parameter. |
||
826 | * |
||
827 | * @param $parameterClass |
||
828 | * @param $methodName |
||
829 | * @return string |
||
830 | * @throws \BadMethodCallException |
||
831 | */ |
||
832 | protected function getFullParameterClass($parameterClass, $methodName) |
||
840 | |||
841 | /** |
||
842 | * Routes the method call to the adequate protected method. |
||
843 | * |
||
844 | * @param $methodName |
||
845 | * @param array $methodArguments |
||
846 | * @return mixed |
||
847 | * @throws \BadMethodCallException |
||
848 | */ |
||
849 | public function __call($methodName, array $methodArguments) |
||
876 | |||
877 | /** |
||
878 | * Fix typos that went into releases, this way we ensure we don't break scripts in production. |
||
879 | * |
||
880 | * @param string $methodName |
||
881 | * @return string |
||
882 | */ |
||
883 | protected function fixTypos($methodName) |
||
892 | } |
||
893 |
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.