Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

PHP for Adults: Clean Code and Object Calisthenics

1,455 views

Published on

Talk given at GTA PHP meetup on 2016-11-01.

Published in: Technology
  • Be the first to comment

PHP for Adults: Clean Code and Object Calisthenics

  1. 1. PHP 4 ADULTS OBJECT CALISTHENICS AND CLEAN CODE
  2. 2. GUILHERMEBLANCO GUILHERMEBLANCO Guilherme Blanco
  3. 3. MOTIVATION ▸ Readability ▸ Maintainability ▸ Reusability ▸ Testability
  4. 4. SUMMING UP…
  5. 5. CLEAN CODE
  6. 6. S T U P I D
  7. 7. SINGLETON TIGHT COUPLING UNTESTABILITY PREMATURE OPTIMIZATION INDESCRIPTIVE NAMING DUPLICATION
  8. 8. S O L I D
  9. 9. SINGLE RESPONSIBILITY OPEN/CLOSED PRINCIPLE LISKOV SUBSTITUTION PRINCIPLE INTERFACE SEGREGATION DEPENDENCY INVERSION
  10. 10. LISKOV SUBSTITUTION PRINCIPLE
  11. 11. interface Bird { public function setLocation($longitude, $latitude); public function setHeight($height); public function draw(); }
  12. 12. class Penguin implements Bird { public function setHeight($height) { // Do nothing } }
  13. 13. interface Bird { public function setLocation($longitude, $latitude); public function draw(); } interface FlightfulBird extends Bird 
 { 
 public function setHeight($height); }
  14. 14. DEPENDENCY INVERSION PRINCIPLE
  15. 15. namespace DatingUserBundleEntity { class User { /** @var DatingUserBundleEntityImage */ protected $avatar; } } namespace DatingMediaBundleEntity { class Image { /** @var DatingUserBundleEntityUser */ protected $owner; } 
 }
  16. 16. namespace DatingUserBundleEntity { class User { /** @var AvatarInterface */ protected $avatar; } interface AvatarInterface { // ... } } namespace DatingMediaBundleEntity { use DatingUserBundleEntityAvatarInterface; class Image implements AvatarInterface { /** @var DatingUserBundleEntityUser */ protected $owner; } 
 }
  17. 17. OBJECT CALISTHENICS
  18. 18. RULE #1 ONLY ONE INDENTATION LEVEL PER METHOD
  19. 19. public function validateForm($filters='', $validators='', $options='') { $data = $_POST; $input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim()); if ($input->hasInvalid() || $input->hasMissing()) { foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { if (strpos($message, "empty")) { throw new Tss_FormException( "The field {$field} cannot be empty!", 3, "javascript:history.back();" ); 
 } else { throw new Tss_FormException( "{$message}", 3, "javascript:history.back();" ); } } } } return $input; }
  20. 20. 1 2 3 4 public function validateForm($filters='', $validators='', $options='') { $data = $_POST; $input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim()); if ($input->hasInvalid() || $input->hasMissing()) { foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { if (strpos($message, "empty")) { throw new Tss_FormException( "The field {$field} cannot be empty!", 3, "javascript:history.back();" ); 
 } else { throw new Tss_FormException( "{$message}", 3, "javascript:history.back();" ); } } } } return $input; } Class prototype
  21. 21. EARLY RETURNS
  22. 22. public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST; $input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim()); if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; } foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { if (strpos($message, "empty")) { throw new Tss_FormException( "The field {$field} cannot be empty!", 3, "javascript:history.back();" ); 
 } else { throw new Tss_FormException( "{$message}", 3, "javascript:history.back();" ); } } } return $input; }
  23. 23. public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST; $input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim()); if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; } foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { if (strpos($message, "empty")) { throw new Tss_FormException( "The field {$field} cannot be empty!", 3, "javascript:history.back();" ); 
 } else { throw new Tss_FormException( "{$message}", 3, "javascript:history.back();" ); } } } return $input; }
  24. 24. 1 2 3 public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST; $input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim()); if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; } foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { if (strpos($message, "empty")) { throw new Tss_FormException( "The field {$field} cannot be empty!", 3, "javascript:history.back();" ); 
 } else { throw new Tss_FormException( "{$message}", 3, "javascript:history.back();" ); } } } return $input; }
  25. 25. public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST; $input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim()); if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; } foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { $errorMessage = (strpos($message, "empty") === false) ? "The field {$field} cannot be empty!" : $message; throw new Tss_FormException( $errorMessage, 3, "javascript:history.back();" ); } } return $input; }
  26. 26. public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST; $input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim()); if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; } foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { $errorMessage = (strpos($message, "empty") === false) ? "The field {$field} cannot be empty!" : $message; throw new Tss_FormException( $errorMessage, 3, "javascript:history.back();" ); } } return $input; }
  27. 27. 1 2 public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST; $input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim()); if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; } foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { $errorMessage = (strpos($message, "empty") === false) ? "The field {$field} cannot be empty!" : $message; throw new Tss_FormException( $errorMessage, 3, "javascript:history.back();" ); } } return $input; }
  28. 28. public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST; $input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim()); if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; } foreach ($input->getMessages() as $field => $messageList) { $message = array_shift($messageList); $jsAction = "javascript:history.back();"; $errorMessage = (strpos($message, "empty") === false) ? "The field {$field} cannot be empty!" : $message; throw new Tss_FormException($errorMessage, 3, $jsAction); } return $input; }
  29. 29. public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST; $input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim()); if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; } foreach ($input->getMessages() as $field => $messageList) { $message = array_shift($messageList); $jsAction = "javascript:history.back();"; $errorMessage = (strpos($message, "empty") === false) ? "The field {$field} cannot be empty!" : $message; throw new Tss_FormException($errorMessage, 3, $jsAction); } return $input; } Logical groups Variable interpolation
  30. 30. public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST; $input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim()); if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; } foreach ($input->getMessages() as $field => $messageList) { $message = array_shift($messageList); $jsAction = "javascript:history.back();"; $errorMessage = (strpos($message, "empty") === false) ? sprintf("The field %s cannot be empty!", $field) : $message; throw new Tss_FormException($errorMessage, 3, $jsAction); } return $input; }
  31. 31. BENEFITS ▸ Single Responsibility Principle ("S" in SOLID) ▸ Reusability
  32. 32. RULE #2 NO "ELSE" KEYWORD
  33. 33. public function createPost($request) { $entity = new Post(); $form = new MyForm($entity); $form->bind($request); if ($form->isValid()){ $repository = $this->getRepository('MyBundle:Post'); if (!$repository->exists($entity)) { $repository->save($entity); return $this->redirect('create_ok'); } else { $error = "Post Title already exists"; return array('form' => $form, 'error' => $error); } 
 } else { $error = "Invalid fields"; return array('form' => $form, 'error' => $error); } }
  34. 34. public function createPost($request) { $entity = new Post(); $form = new MyForm($entity); $form->bind($request); if ($form->isValid()){ $repository = $this->getRepository('MyBundle:Post'); if (!$repository->exists($entity)) { $repository->save($entity); return $this->redirect('create_ok'); } else { $error = "Post Title already exists"; return array('form' => $form, 'error' => $error); } 
 } else { $error = "Invalid fields"; return array('form' => $form, 'error' => $error); } } Type-casting Coding standards Separate into logical groups. Consider as paragraphs!
  35. 35. public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity); $form->bind($request); if ($form->isValid()) { if (! $repository->exists($entity)) { $repository->save($entity); return $this->redirect('create_ok'); } else { $error = "Post Title already exists"; return array('form' => $form, 'error' => $error); } 
 } else { $error = "Invalid fields"; return array('form' => $form, 'error' => $error); } }
  36. 36. public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity); $form->bind($request); if ($form->isValid()) { if (! $repository->exists($entity)) { $repository->save($entity); return $this->redirect('create_ok'); } else { $error = "Post Title already exists"; return array('form' => $form, 'error' => $error); } 
 } else { $error = "Invalid fields"; return array('form' => $form, 'error' => $error); } }
  37. 37. public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity); $form->bind($request); if ($form->isValid()) { if (! $repository->exists($entity)) { $repository->save($entity); return $this->redirect('create_ok'); } else { $error = "Post Title already exists"; return array('form' => $form, 'error' => $error); } 
 } else { $error = "Invalid fields"; return array('form' => $form, 'error' => $error); } }
  38. 38. UML NORMAL VS. ALTERNATIVE FLOWS
  39. 39. public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity); $form->bind($request); if ($form->isValid()) { if (! $repository->exists($entity)) { $repository->save($entity); return $this->redirect('create_ok'); } else { $error = "Post Title already exists"; return array('form' => $form, 'error' => $error); } 
 } else { $error = "Invalid fields"; return array('form' => $form, 'error' => $error); } }
  40. 40. public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity); $form->bind($request); if ($form->isValid()) { if (! $repository->exists($entity)) { $repository->save($entity); return $this->redirect('create_ok'); } else { $error = "Post Title already exists"; return array('form' => $form, 'error' => $error); } 
 } else { $error = "Invalid fields"; return array('form' => $form, 'error' => $error); } }
  41. 41. public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity); $form->bind($request); if ($form->isValid()) { if (! $repository->exists($entity)) { $repository->save($entity); return $this->redirect('create_ok'); } else { $error = "Post Title already exists"; return array('form' => $form, 'error' => $error); } 
 } else { $error = "Invalid fields"; return array('form' => $form, 'error' => $error); } }
  42. 42. public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity); $form->bind($request); if (! $form->isValid()) { $error = "Invalid fields"; return array('form' => $form, 'error' => $error); } if (! $repository->exists($entity)) { $repository->save($entity); return $this->redirect('create_ok'); } else { $error = "Post Title already exists"; return array('form' => $form, 'error' => $error); } }
  43. 43. public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity); $form->bind($request); if (! $form->isValid()) { $error = "Invalid fields"; return array('form' => $form, 'error' => $error); } if (! $repository->exists($entity)) { $repository->save($entity); return $this->redirect('create_ok'); } else { $error = "Post Title already exists"; return array('form' => $form, 'error' => $error); } }
  44. 44. public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity); $form->bind($request); if (! $form->isValid()) { $error = "Invalid fields"; return array('form' => $form, 'error' => $error); } if (! $repository->exists($entity)) { $repository->save($entity); return $this->redirect('create_ok'); } else { $error = "Post Title already exists"; return array('form' => $form, 'error' => $error); } }
  45. 45. public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity); $form->bind($request); if (! $form->isValid()) { $error = "Invalid fields"; return array('form' => $form, 'error' => $error); } if ($repository->exists($entity)) { $error = "Post Title already exists"; return array('form' => $form, 'error' => $error); } $repository->save($entity); return $this->redirect('create_ok'); }
  46. 46. public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity); $form->bind($request); if (! $form->isValid()) { $error = "Invalid fields"; return array('form' => $form, 'error' => $error); } if ($repository->exists($entity)) { $error = "Post Title already exists"; return array('form' => $form, 'error' => $error); } $repository->save($entity); return $this->redirect('create_ok'); }
  47. 47. BENEFITS ▸ Prevents code duplication ▸ Increases legibility ▸ Reduce cyclomatic complexity
  48. 48. RULE #3 ENCAPSULATE ALL PRIMITIVE TYPES AND STRINGS
  49. 49. RULE #3 ENCAPSULATE ALL PRIMITIVE TYPES AND STRINGS IF THEY HAVE BEHAVIOR
  50. 50. BUT… WHY?
  51. 51. EXCESSIVE USAGE OF OBJECTS IN PHP (IF PHP <7!) DRASTICALLY INCREASES MEMORY FOOTPRINT! Guilherme Blanco
  52. 52. class Item { final public static function find($id) { if (is_string($id) && trim($id) != '') { // do find ... } throw new InvalidArgumentException('$id must be a non-empty string'); } final public static function create($id, array $data) { if ( ! is_string($id)) { throw new InvalidArgumentException('$id must be a string'); } if (empty(trim($id))) { throw new InvalidArgumentException('$id must be a non-empty string'); } // do create ... } }
  53. 53. class Item { final public static function find($id) { if (! is_string($id) || trim($id) === '') { throw new InvalidArgumentException('$id must be a non-empty string'); } // do find ... } final public static function create($id, array $data) { if (! is_string($id) || trim($id) === '') { throw new InvalidArgumentException('$id must be a non-empty string'); } // do create ... } }
  54. 54. class Item { final public static function find($id) { if (! is_string($id) || trim($id) === '') { throw new InvalidArgumentException('$id must be a non-empty string'); } // do find ... } final public static function create($id, array $data) { if (! is_string($id) || trim($id) === '') { throw new InvalidArgumentException('$id must be a non-empty string'); } // do create ... } }
  55. 55. final class Id { /** @var string */ public $value; public function __construct($value) { if (! is_string($id) || trim($id) === '') { $message = sprintf('%s must be a non-empty string', $value); throw new InvalidArgumentException($message); } $this->value = $value; } public function getValue() { return $this->value; } }
  56. 56. class Item { final public static function find(Id $id) { // do find ... } final public static function create(Id $id, array $data) { // do create ... } }
  57. 57. BENEFITS ▸ Type hinting ▸ Encapsulation ▸ Prevents code duplication
  58. 58. RULE #4 ONE OBJECT OPERATOR (->) PER LINE
  59. 59. $this->manager->getConfig()->getSegment()->setName("foo"); Properties are hard to mock What if previous call returned NULL?
  60. 60. JUST USE A NULL OBJECT! Someone watching this talk, one day
  61. 61. final class NullObject { public function __get($property) { return new self; } public function __set($property, $value) { return new self; } public function __call($method, array $arguments) { return new self; } public function __callStatic($method, array $arguments) { return new self; } public function__toString() { return 'null'; } }
  62. 62. WHY IS IT BAD? ▸ Hide encapsulation problem ▸ Hard to debug and handle exceptions ▸ Codebase must be structured to use NullObject ▸ Hard to read and understand
  63. 63. EXCEPTION TO RULE FLUENT INTERFACE UNDER SAME METHOD
  64. 64. $filterChain 
 ->addFilter(new Zend_Filter_Alpha()) ->addFilter(new Zend_Filter_StringToLower()) ;
  65. 65. BENEFITS ▸ Law of Demeter ▸ Readability ▸ Increases testability (easier to mock) ▸ Simplifies debugging
  66. 66. RULE #5 DO NOT ABBREVIATE
  67. 67. THERE ARE 2 HARD PROBLEMS IN COMPUTER SCIENCE: CACHE INVALIDATION, NAMING THINGS AND OFF BY 1 ERRORS. Tim Bray (mentioning Phil Karlton)
  68. 68. WHY DO YOU ABBREVIATE?
  69. 69. CODE DUPLICATION PROBLEM! WRITE SAME NAME REPEATEDLY
  70. 70. MULTIPLE RESPONSIBILITY PROBLEM! LONG NAMES
  71. 71. public function getPage($data) { ... } "Get" from where? public function startProcess() { ... } $trx->process('site.login'); How? WTF is that? renderHomePage forkIntoChildProcess extendedTranslator
  72. 72. BENEFITS ▸ Readability ▸ Better exposing method’s intent ▸ Improved maintainability ▸ Good indicator of code duplication and encapsulation
  73. 73. RULE #6 KEEP YOUR CLASSES SMALL
  74. 74. OBJECTIVE ▸ Maximum 200 lines per class
 (including docblock/documentation) ▸ 10 methods per class ▸ Up to 20 lines per method ▸ 15 classes/interfaces/traits per namespace
  75. 75. BENEFITS ▸ Single Responsibility Principle ▸ Clear and objective methods ▸ Better code segregation ▸ Cleaner namespaces
  76. 76. RULE #7 LIMIT CLASS INSTANCE VARIABLES IN A CLASS (BETWEEN 2 TO 5)
  77. 77. class MyRegistrationService { protected $userService; protected $passwordService; protected $logger; protected $translator; protected $entityManager; protected $imageCropper; // ... } Database interactions should be on UserService Rely on an Event system and move this to a listener Cross-cutting concerns. Should be auto- injected by your DI through an interface hint
  78. 78. class MyRegistrationService implements LoggerAwareInterface, TranslatorAwareInterface { use LoggerAwareTrait; use TranslatorAwareTrait; protected $userService; protected $passwordService; protected $eventDispatcher; // ... }
  79. 79. BENEFITS ▸ Single Responsibility Principle ▸ Loose coupling ▸ Better encapsulation ▸ Testability
  80. 80. RULE #8 USE FIRST CLASS COLLECTIONS
  81. 81. OR IN OTHER TERMS…
  82. 82. ANY CLASS THAT CONTAINS AN ARRAY MUST NOT HAVE ANY OTHER PROPERTY. Guilherme Blanco TEXT
  83. 83. class User { private $name; // ... private $albumList = array(); public function getPublicAlbumList() { $filteredAlbumList = array(); foreach ($this->albumList as $album) { if ($album->getPrivacy() === AlbumPrivacy::PUBLIC) { $filteredAlbumList[] = $album; } } return $filteredAlbumList; } // ... } $publicAlbumList = $user->getPublicAlbumList();
  84. 84. class AlbumList extends Collection { public function getPublic() { $filteredAlbumList = array(); foreach ($this->value as $album) { if ($album->getPrivacy() === AlbumPrivacy::PUBLIC) { $filteredAlbumList[] = $album; } } return $filteredAlbumList; } } class User { private $name; private $albumList = new AlbumList(); // ... } $publicAlbumList = $user->getAlbumList()->getPublic();
  85. 85. class AlbumList extends Collection { public function getPublic() { return new ArrayCollection( array_filter( $this->value, function (Album $album) { return $album->isPublic(); } ) ); } } class User { private $name; private $albumList = new AlbumList(); // ... } $publicAlbumList = $user->getAlbumList()->getPublic();
  86. 86. BENEFITS ▸ Single Responsibility Principle ▸ Collection operations implemented inside of Collection ▸ Usage of SPL classes ▸ Easy to group collections without concerns over their members’ behavior ▸ Filtering, ordering, mapping, combining are good example methods
  87. 87. RULE #9 USE GETTERS AND SETTERS
  88. 88. class BankAccount { public $balance = 0; public function deposit($amount) { $this->balance += $amount; } public function withdraw($amount) { $this->balance -= $amount; } } // Example: $account = new BankAccount(); 
 $account->deposit(100.00); // ... $account->balance = 0; 
 // ... $account->withdraw(10.00); Balance can be modified without class being notified, leading to unexpected errors.
  89. 89. BENEFITS ▸ Operations injection ▸ Transformations encapsulation ▸ Promotes Open/Closed Principle ("O" in SOLID)
  90. 90. QUESTIONS?
  91. 91. THANKS! =) GUILHERMEBLANCO GUILHERMEBLANCO

×