Complex classes like Tree 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 Tree, and based on these observations, apply Extract Interface, too.
| 1 | <?php | ||
| 34 | class Tree extends Object implements \ArrayAccess, \Countable, \Iterator | ||
| 35 | { | ||
| 36 | /** | ||
| 37 | * @var string | ||
| 38 | */ | ||
| 39 | private $ref; | ||
| 40 | |||
| 41 | /** | ||
| 42 | * the cursor position | ||
| 43 | * | ||
| 44 | * @var int | ||
| 45 | */ | ||
| 46 | private $position; | ||
| 47 | |||
| 48 | /** | ||
| 49 | * the tree subject | ||
| 50 | * | ||
| 51 | * @var Object | ||
| 52 | */ | ||
| 53 | private $subject; | ||
| 54 | |||
| 55 | /** | ||
| 56 | * tree children | ||
| 57 | * | ||
| 58 | * @var array | ||
| 59 | */ | ||
| 60 | private $children = array(); | ||
| 61 | |||
| 62 | /** | ||
| 63 | * tree path children | ||
| 64 | * | ||
| 65 | * @var array | ||
| 66 | */ | ||
| 67 | private $pathChildren = array(); | ||
| 68 | |||
| 69 | /** | ||
| 70 | * the blob of the actual tree | ||
| 71 | * | ||
| 72 | * @var \GitElephant\Objects\Object | ||
| 73 | */ | ||
| 74 | private $blob; | ||
| 75 | |||
| 76 | /** | ||
| 77 | * static method to generate standalone log | ||
| 78 | * | ||
| 79 | * @param \GitElephant\Repository $repository repo | ||
| 80 | * @param array $outputLines output lines from command.log | ||
| 81 | * | ||
| 82 | * @return \GitElephant\Objects\Log | ||
| 83 | */ | ||
| 84 | public static function createFromOutputLines(Repository $repository, $outputLines) | ||
| 85 |     { | ||
| 86 | $tree = new self($repository); | ||
| 87 | $tree->parseOutputLines($outputLines); | ||
| 88 | |||
| 89 | return $tree; | ||
| 90 | } | ||
| 91 | |||
| 92 | /** | ||
| 93 | * get the commit properties from command | ||
| 94 | * | ||
| 95 | * @see LsTreeCommand::tree | ||
| 96 | */ | ||
| 97 | 15 | private function createFromCommand() | |
| 98 |     { | ||
| 99 | 15 | $command = LsTreeCommand::getInstance($this->getRepository())->tree($this->ref, $this->subject); | |
| 100 | 15 | $outputLines = $this->getCaller()->execute($command)->getOutputLines(true); | |
| 101 | 15 | $this->parseOutputLines($outputLines); | |
| 102 | 15 | } | |
| 103 | |||
| 104 | /** | ||
| 105 | * Some path examples: | ||
| 106 | * empty string for root | ||
| 107 | * folder1/folder2 | ||
| 108 | * folder1/folder2/filename | ||
| 109 | * | ||
| 110 | * @param \GitElephant\Repository $repository the repository | ||
| 111 | * @param string $ref a treeish reference | ||
| 112 | * @param Object $subject the subject | ||
| 113 | * | ||
| 114 | * @throws \RuntimeException | ||
| 115 | * @throws \Symfony\Component\Process\Exception\RuntimeException | ||
| 116 | * @internal param \GitElephant\Objects\Object|string $treeObject Object instance | ||
| 117 | */ | ||
| 118 | 15 | public function __construct(Repository $repository, $ref = 'HEAD', $subject = null) | |
| 119 |     { | ||
| 120 | 15 | $this->position = 0; | |
| 121 | 15 | $this->repository = $repository; | |
| 122 | 15 | $this->ref = $ref; | |
| 123 | 15 | $this->subject = $subject; | |
| 124 | 15 | $this->createFromCommand(); | |
| 125 | 15 | } | |
| 126 | |||
| 127 | /** | ||
| 128 | * parse the output of a git command showing a ls-tree | ||
| 129 | * | ||
| 130 | * @param array $outputLines output lines | ||
| 131 | */ | ||
| 132 | 15 | private function parseOutputLines($outputLines) | |
| 133 |     { | ||
| 134 | 15 |         foreach ($outputLines as $line) { | |
| 135 | 15 | $this->parseLine($line); | |
| 136 | 15 | } | |
| 137 | 15 | usort($this->children, array($this, 'sortChildren')); | |
| 138 | 15 | $this->scanPathsForBlob($outputLines); | |
| 139 | 15 | } | |
| 140 | |||
| 141 | /** | ||
| 142 | * @return \GitElephant\Command\Caller\Caller | ||
| 143 | */ | ||
| 144 | 15 | private function getCaller() | |
| 148 | |||
| 149 | /** | ||
| 150 | * get the current tree parent, null if root | ||
| 151 | * | ||
| 152 | * @return null|string | ||
| 153 | */ | ||
| 154 | public function getParent() | ||
| 155 |     { | ||
| 156 |         if ($this->isRoot()) { | ||
| 157 | return null; | ||
| 158 | } | ||
| 159 | |||
| 160 | return substr($this->subject->getFullPath(), 0, strrpos($this->subject->getFullPath(), '/')); | ||
| 161 | } | ||
| 162 | |||
| 163 | /** | ||
| 164 | * tell if the tree created is the root of the repository | ||
| 165 | * | ||
| 166 | * @return bool | ||
| 167 | */ | ||
| 168 | 15 | public function isRoot() | |
| 172 | |||
| 173 | /** | ||
| 174 | * tell if the path given is a blob path | ||
| 175 | * | ||
| 176 | * @return bool | ||
| 177 | */ | ||
| 178 | 15 | public function isBlob() | |
| 182 | |||
| 183 | /** | ||
| 184 | * the current tree path is a binary file | ||
| 185 | * | ||
| 186 | * @return bool | ||
| 187 | */ | ||
| 188 | public function isBinary() | ||
| 192 | |||
| 193 | /** | ||
| 194 | * get binary data | ||
| 195 | * | ||
| 196 | * @throws \RuntimeException | ||
| 197 | * @throws \Symfony\Component\Process\Exception\LogicException | ||
| 198 | * @throws \Symfony\Component\Process\Exception\InvalidArgumentException | ||
| 199 | * @throws \Symfony\Component\Process\Exception\RuntimeException | ||
| 200 | * @return string | ||
| 201 | */ | ||
| 202 | public function getBinaryData() | ||
| 203 |     { | ||
| 204 | $cmd = CatFileCommand::getInstance($this->getRepository())->content($this->getSubject(), $this->ref); | ||
| 205 | |||
| 206 | return $this->getCaller()->execute($cmd)->getRawOutput(); | ||
| 207 | } | ||
| 208 | |||
| 209 | /** | ||
| 210 | * Return an array like this | ||
| 211 | * 0 => array( | ||
| 212 | * 'path' => the path to the current element | ||
| 213 | * 'label' => the name of the current element | ||
| 214 | * ), | ||
| 215 | * 1 => array(), | ||
| 216 | * ... | ||
| 217 | * | ||
| 218 | * @return array | ||
| 219 | */ | ||
| 220 | public function getBreadcrumb() | ||
| 221 |     { | ||
| 222 | $bc = array(); | ||
| 223 |         if (!$this->isRoot()) { | ||
| 224 |             $arrayNames = explode('/', $this->subject->getFullPath()); | ||
| 225 | $pathString = ''; | ||
| 226 |             foreach ($arrayNames as $i => $name) { | ||
| 227 |                 if ($this->isBlob() && $name == $this->blob->getName()) { | ||
| 228 | $bc[$i]['path'] = $pathString . $name; | ||
| 229 | $bc[$i]['label'] = $this->blob; | ||
| 230 | $pathString .= $name . '/'; | ||
| 231 |                 } else { | ||
| 232 | $bc[$i]['path'] = $pathString . $name; | ||
| 233 | $bc[$i]['label'] = $name; | ||
| 234 | $pathString .= $name . '/'; | ||
| 235 | } | ||
| 236 | } | ||
| 237 | } | ||
| 238 | |||
| 239 | return $bc; | ||
| 240 | } | ||
| 241 | |||
| 242 | /** | ||
| 243 | * check if the path is equals to a fullPath | ||
| 244 | * to tell if it's a blob | ||
| 245 | * | ||
| 246 | * @param array $outputLines output lines | ||
| 247 | * | ||
| 248 | * @return mixed | ||
| 249 | */ | ||
| 250 | 15 | private function scanPathsForBlob($outputLines) | |
| 251 |     { | ||
| 252 | // no children, empty folder or blob! | ||
| 253 | 15 |         if (count($this->children) > 0) { | |
| 254 | 10 | return; | |
| 255 | } | ||
| 256 | // root, no blob | ||
| 257 | 7 |         if ($this->isRoot()) { | |
| 258 | return; | ||
| 259 | } | ||
| 260 | 7 |         if (1 === count($outputLines)) { | |
| 261 | 7 | $treeObject = Object::createFromOutputLine($this->repository, $outputLines[0]); | |
| 262 | 7 |             if ($treeObject->getSha() === $this->subject->getSha()) { | |
| 263 | 7 | $this->blob = $treeObject; | |
| 264 | 7 | } | |
| 265 | 7 | } | |
| 266 | 7 | } | |
| 267 | |||
| 268 | /** | ||
| 269 | * Reorder children of the tree | ||
| 270 | * Tree first (alphabetically) and then blobs (alphabetically) | ||
| 271 | * | ||
| 272 | * @param \GitElephant\Objects\Object $a the first object | ||
| 273 | * @param \GitElephant\Objects\Object $b the second object | ||
| 274 | * | ||
| 275 | * @return int | ||
| 276 | */ | ||
| 277 | 7 | private function sortChildren(Object $a, Object $b) | |
| 278 |     { | ||
| 279 | 7 |         if ($a->getType() == $b->getType()) { | |
| 280 | 5 | $names = array($a->getName(), $b->getName()); | |
| 281 | 5 | sort($names); | |
| 282 | |||
| 283 | 5 | return ($a->getName() == $names[0]) ? -1 : 1; | |
| 284 | } | ||
| 285 | |||
| 286 | 5 | return $a->getType() == Object::TYPE_TREE || $b->getType() == Object::TYPE_BLOB ? -1 : 1; | |
| 287 | } | ||
| 288 | |||
| 289 | /** | ||
| 290 | * Parse a single line into pieces | ||
| 291 | * | ||
| 292 | * @param string $line a single line output from the git binary | ||
| 293 | * | ||
| 294 | * @return mixed | ||
| 295 | */ | ||
| 296 | 15 | private function parseLine($line) | |
| 297 |     { | ||
| 298 | 15 |         if ($line == '') { | |
| 299 | return; | ||
| 300 | } | ||
| 301 | 15 | $slices = Object::getLineSlices($line); | |
| 302 | 15 |         if ($this->isBlob()) { | |
| 303 | $this->pathChildren[] = $this->blob->getName(); | ||
| 304 |         } else { | ||
| 305 | 15 |             if ($this->isRoot()) { | |
| 306 | // if is root check for first children | ||
| 307 | 9 | $pattern = '/(\w+)\/(.*)/'; | |
| 308 | 9 | $replacement = '$1'; | |
| 309 | 9 |             } else { | |
| 310 | // filter by the children of the path | ||
| 311 | 10 | $actualPath = $this->subject->getFullPath(); | |
| 312 | 10 |                 if (!preg_match(sprintf('/^%s\/(\w*)/', preg_quote($actualPath, '/')), $slices['fullPath'])) { | |
| 313 | 7 | return; | |
| 314 | } | ||
| 315 | 5 |                 $pattern     = sprintf('/^%s\/(\w*)/', preg_quote($actualPath, '/')); | |
| 316 | 5 | $replacement = '$1'; | |
| 317 | } | ||
| 318 | 10 | $name = preg_replace($pattern, $replacement, $slices['fullPath']); | |
| 319 | 10 |             if (strpos($name, '/') !== false) { | |
| 320 | return; | ||
| 321 | } | ||
| 322 | 10 |             if (!in_array($name, $this->pathChildren)) { | |
| 323 | 10 | $path = rtrim(rtrim($slices['fullPath'], $name), '/'); | |
| 324 | 10 | $treeObject = new TreeObject( | |
| 325 | 10 | $this->repository, | |
| 326 | 10 | $slices['permissions'], | |
| 327 | 10 | $slices['type'], | |
| 328 | 10 | $slices['sha'], | |
| 329 | 10 | $slices['size'], | |
| 330 | 10 | $name, | |
| 331 | $path | ||
| 332 | 10 | ); | |
| 333 | 10 | $this->children[] = $treeObject; | |
| 334 | 10 | $this->pathChildren[] = $name; | |
| 335 | 10 | } | |
| 336 | } | ||
| 337 | 10 | } | |
| 338 | |||
| 339 | /** | ||
| 340 | * get the last commit message for this tree | ||
| 341 | * | ||
| 342 | * @param string $ref | ||
| 343 | * | ||
| 344 | * @throws \RuntimeException | ||
| 345 | * @return Commit\Message | ||
| 346 | */ | ||
| 347 | public function getLastCommitMessage($ref = 'master') | ||
| 351 | |||
| 352 | /** | ||
| 353 | * get author of the last commit | ||
| 354 | * | ||
| 355 | * @param string $ref | ||
| 356 | * | ||
| 357 | * @throws \RuntimeException | ||
| 358 | * @return Author | ||
| 359 | */ | ||
| 360 | public function getLastCommitAuthor($ref = 'master') | ||
| 364 | |||
| 365 | /** | ||
| 366 | * get the last commit for a given treeish, for the actual tree | ||
| 367 | * | ||
| 368 | * @param string $ref | ||
| 369 | * | ||
| 370 | * @throws \RuntimeException | ||
| 371 | * @throws \Symfony\Component\Process\Exception\RuntimeException | ||
| 372 | * @return Commit | ||
| 373 | */ | ||
| 374 | public function getLastCommit($ref = 'master') | ||
| 375 |     { | ||
| 376 |         if ($this->isRoot()) { | ||
| 377 | return $this->getRepository()->getCommit($ref); | ||
| 378 | } | ||
| 379 | $log = $this->repository->getObjectLog($this->getObject(), $ref); | ||
|  | |||
| 380 | |||
| 381 | return $log[0]; | ||
| 382 | } | ||
| 383 | |||
| 384 | /** | ||
| 385 | * get the tree object for this tree | ||
| 386 | * | ||
| 387 | * @return \GitElephant\Objects\Object | ||
| 388 | */ | ||
| 389 | 1 | public function getObject() | |
| 390 |     { | ||
| 391 | 1 |         if ($this->isRoot()) { | |
| 392 | 1 | return null; | |
| 393 |         } else { | ||
| 394 | 1 | return $this->getSubject(); | |
| 395 | } | ||
| 396 | } | ||
| 397 | |||
| 398 | /** | ||
| 399 | * Blob getter | ||
| 400 | * | ||
| 401 | * @return \GitElephant\Objects\Object | ||
| 402 | */ | ||
| 403 | 5 | public function getBlob() | |
| 407 | |||
| 408 | /** | ||
| 409 | * Get Subject | ||
| 410 | * | ||
| 411 | * @return \GitElephant\Objects\Object | ||
| 412 | */ | ||
| 413 | 1 | public function getSubject() | |
| 417 | |||
| 418 | /** | ||
| 419 | * Get Ref | ||
| 420 | * | ||
| 421 | * @return string | ||
| 422 | */ | ||
| 423 | public function getRef() | ||
| 427 | |||
| 428 | /** | ||
| 429 | * ArrayAccess interface | ||
| 430 | * | ||
| 431 | * @param int $offset offset | ||
| 432 | * | ||
| 433 | * @return bool | ||
| 434 | */ | ||
| 435 | public function offsetExists($offset) | ||
| 439 | |||
| 440 | |||
| 441 | /** | ||
| 442 | * ArrayAccess interface | ||
| 443 | * | ||
| 444 | * @param int $offset offset | ||
| 445 | * | ||
| 446 | * @return null | ||
| 447 | */ | ||
| 448 | 8 | public function offsetGet($offset) | |
| 452 | |||
| 453 | /** | ||
| 454 | * ArrayAccess interface | ||
| 455 | * | ||
| 456 | * @param int $offset offset | ||
| 457 | * @param mixed $value value | ||
| 458 | */ | ||
| 459 | public function offsetSet($offset, $value) | ||
| 460 |     { | ||
| 461 |         if (is_null($offset)) { | ||
| 462 | $this->children[] = $value; | ||
| 463 |         } else { | ||
| 464 | $this->children[$offset] = $value; | ||
| 465 | } | ||
| 466 | } | ||
| 467 | |||
| 468 | /** | ||
| 469 | * ArrayAccess interface | ||
| 470 | * | ||
| 471 | * @param int $offset offset | ||
| 472 | */ | ||
| 473 | public function offsetUnset($offset) | ||
| 477 | |||
| 478 | /** | ||
| 479 | * Countable interface | ||
| 480 | * | ||
| 481 | * @return int|void | ||
| 482 | */ | ||
| 483 | 4 | public function count() | |
| 487 | |||
| 488 | /** | ||
| 489 | * Iterator interface | ||
| 490 | * | ||
| 491 | * @return mixed | ||
| 492 | */ | ||
| 493 | 1 | public function current() | |
| 497 | |||
| 498 | /** | ||
| 499 | * Iterator interface | ||
| 500 | */ | ||
| 501 | 1 | public function next() | |
| 505 | |||
| 506 | /** | ||
| 507 | * Iterator interface | ||
| 508 | * | ||
| 509 | * @return int | ||
| 510 | */ | ||
| 511 | public function key() | ||
| 515 | |||
| 516 | /** | ||
| 517 | * Iterator interface | ||
| 518 | * | ||
| 519 | * @return bool | ||
| 520 | */ | ||
| 521 | 1 | public function valid() | |
| 525 | |||
| 526 | /** | ||
| 527 | * Iterator interface | ||
| 528 | */ | ||
| 529 | 1 | public function rewind() | |
| 533 | } | ||
| 534 | 
Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code: