Refactoring Multi-Dimensional Array PHP to Object Oriented PHP

One of my favourite things to do is refactor legacy code. I get to see the thinking process that went into solving the problem. It’s like standing on the shoulders of giants.

private function convertActivityToArray(array $activity)
    {
        $activityArray = array();

        $previousGroupNumber = null;
        /** @var Activity $activityItem */
        foreach ($activity as $activityItem) {
            $type = $activityItem->getType();
            $groupNumber = $activityItem->getGroupNumber();
            $user = $activityItem->getUser();
            $name = $user->getName();
            $avatar = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/avatar/' . $user->getAvatar();
            $data = $activityItem->getData();
            $created = $activityItem->getCreatedAt();

            $activityArray[$groupNumber]['type'] = $type;
            $activityArray[$groupNumber]['created'] = $created;

            if ($type == ActivityType::Like) {
                if ($groupNumber != $previousGroupNumber) {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Comment) {
                $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                $activityArray[$groupNumber]['parent']['data'] = $data;
            } else if ($type == ActivityType::Joined) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;#
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Added) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            }

            $previousGroupNumber = $groupNumber;
        }

        return $activityArray;
    }

First we refactor $data.

 $data = $activityItem->getData();

$data is an array stored in the database like this:

Joined Data:

a:2:{s:6:"userId";s:1:"1";s:8:"username";s:8:"mbwagner";}

Watchlisted Data

a:5:{s:13:"documentaryId";i:728;s:16:"documentaryTitle";s:24:"The U.S. vs. John Lennon";s:18:"documentaryExcerpt";s:160:"In retrospect, it seems absurd that the United States government felt so threatened by the presence of John Lennon that they tried to have him deported. But tha";s:20:"documentaryThumbnail";s:24:"cover/usvsjohnlennon.jpg";s:15:"documentarySlug";s:22:"the-u-s-vs-john-lennon";}

The problem with this way is the data is hardcoded into the database, what happens if the data changes? So I decide to populate data on the fly.

Replace:

 $data = $activityItem->getData();

with:

$dataStrategyContext = new DataStrategyContext(
                $type,
                $this->request,
                $this->documentaryService,
                $this->commentService);
$data = $dataStrategyContext->createData($activityItem);

Here we use the Strategy Pattern to populate data.

class DataStrategyContext
{
    /**
     * @var StrategyInterface|null
     */
    private $strategy = null;

    public function __construct(
        string $type,
        Request $request,
        DocumentaryService $documentaryService,
        CommentService $commentService)
    {
        switch ($type) {
            case ActivityType::WATCHLIST:
                $this->strategy = new StrategyWatchlist(
                    $request,
                    $documentaryService
                );
            break;
            case ActivityType::COMMENT:
                $this->strategy = new StrategyComment(
                    $request,
                    $commentService
                );
            break;
            case ActivityType::JOINED:
                $this->strategy = new StrategyJoined();
            break;
            case ActivityType::ADDED:
                $this->strategy = new StrategyAdded(
                    $documentaryService
                );
            break;
        }
    }

    /**
     * @param Activity $activityEntity
     * @return mixed
     */
    public function createData(Activity $activityEntity)
    {
        return $this->strategy->createData($activityEntity);
    }
}

StrategyWatchlist would look like the following:

class StrategyWatchlist implements StrategyInterface
{
    /**
     * @var Request
     */
    private $request;

    /**
     * @var DocumentaryService
     */
    private $documentaryService;

    /**
     * @param Request $request
     * @param DocumentaryService $documentaryService
     */
    public function __construct(
        Request $request,
        DocumentaryService $documentaryService)
    {
        $this->request = $request;
        $this->documentaryService = $documentaryService;
    }

    /**
     * @param Activity $activityEntity
     * @return Data
     */
    public function createData(Activity $activityEntity)
    {
        $documentaryId = $activityEntity->getObjectId();
        $documentary = $this->documentaryService->getDocumentaryById($documentaryId);

        $watchlistData = new WatchlistData();
        $watchlistData->setDocumentaryId($documentary->getId());
        $watchlistData->setDocumentaryTitle($documentary->getTitle());
        $watchlistData->setDocumentarySlug($documentary->getSlug());
        $watchlistData->setDocumentarySummary($documentary->getSummary());
        $poster = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $documentary->getPoster();
        $watchlistData->setDocumentaryPoster($poster);

        return $watchlistData;
    }
}

The next thing is to refactor parent and children from this:

if ($type == ActivityType::Like) {
                if ($groupNumber != $previousGroupNumber) {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Comment) {
                $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                $activityArray[$groupNumber]['parent']['data'] = $data;
            } else if ($type == ActivityType::Joined) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;#
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Added) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            }

            $previousGroupNumber = $groupNumber;
        }

After refactoring and creating ActivityParent and ActivityChild it should look like this:

if ($type == ActivityType::Like) {
	if ($groupNumber != $previousGroupNumber) {
		$activityParent = new ActivityParent();
		$activityParent->setName($name);
		$activityParent->setUsername($username);
		$activityParent->setAvatar($avatar);
		$activityParent->setData($data);

		$activityArray[$groupNumber]['parent'] = $activityParent->toArray();
	} else {
		$activityChild = new ActivityChild();
		$activityChild->setData($data);
		$activityChild->setUsername($username);
		$activityChild->setName($name);
		$activityChild->setAvatar($avatar);

		$activityArray[$groupNumber]['child'][] = $activityChild->toArray();
	}
} else if ($type == ActivityType::Comment) {
	$activityParent = new ActivityParent();
		$activityParent->setName($name);
		$activityParent->setUsername($username);
		$activityParent->setAvatar($avatar);
		$activityParent->setData($data);
} else if ($type == ActivityType::Joined) {
	if ($groupNumber != $previousGroupNumber) {
		$activityParent = new ActivityParent();
		$activityParent->setName($name);
		$activityParent->setUsername($username);
		$activityParent->setAvatar($avatar);
		$activityParent->setData($data);
	} else {
		$activityChild = new ActivityChild();
		$activityChild->setData($data);
		$activityChild->setUsername($username);
		$activityChild->setName($name);
		$activityChild->setAvatar($avatar);

		$activityArray[$groupNumber]['child'][] = $activityChild->toArray();
	}
} else if ($type == ActivityType::Added) {
	if ($groupNumber != $previousGroupNumber) {
		$activityParent = new ActivityParent();
		$activityParent->setName($name);
		$activityParent->setUsername($username);
		$activityParent->setAvatar($avatar);
		$activityParent->setData($data);
	} else {
		$activityChild = new ActivityChild();
		$activityChild->setData($data);
		$activityChild->setUsername($username);
		$activityChild->setName($name);
		$activityChild->setAvatar($avatar);

		$activityArray[$groupNumber]['child'][] = $activityChild->toArray();
	}
}

$previousGroupNumber = $groupNumber;
}

Notice there are duplicate times when we create ActivityParent and ActivityChild. Only certain types have children:

class ActivityType
{
    const LIKE = "like";
    const COMMENT = "comment";
    const FOLLOW = "follow";
    const JOINED = "joined";
    const ADDED = "added";

    /**
     * @return array
     */
    public static function getAllTypes()
    {
        return [
            self::LIKE,
            self::ADDED,
            self::JOINED,
            self::COMMENT,
            self::FOLLOW
        ];
    }

    /**
     * @return array
     */
    public static function getTypesWithChildren()
    {
        return [
            self::LIKE,
            self::JOINED,
            self::ADDED
        ];
    }

    /**
     * @param string $type
     * @return bool
     */
    public static function hasChildren(string $type)
    {
        return in_array($type, self::getTypesWithChildren());
    }
}

Then we update the convertActivityToArray() function to this:


            $hasChildren = ActivityType::hasChildren($type);
            if ($hasChildren) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityParent = new ActivityParent();
                    $activityParent->setName($name);
                    $activityParent->setUsername($username);
                    $activityParent->setAvatar($avatar);
                    $activityParent->setData($data);

                    $activityArray[$groupNumber]['parent'] = $activityParent->toArray();

                } else {
                    $activityChild = new ActivityChild();
                    $activityChild->setData($data);
                    $activityChild->setUsername($username);
                    $activityChild->setName($name);
                    $activityChild->setAvatar($avatar);

                    $activityArray[$groupNumber]['child'][] = $activityChild->toArray();
                }
            } else {
                $activityParent = new ActivityParent();
                $activityParent->setName($name);
                $activityParent->setUsername($username);
                $activityParent->setAvatar($avatar);
                $activityParent->setData($data);

                $activityArray[$groupNumber]['parent'] = $activityParent->toArray();
            }

Notice ActivityParent and ActivityChild are doing the same thing so I refactored it to just ActivityObject. Plus I moved it above the IF statements.

$activityObject = new ActivityObject();
$activityObject->setName($name);
$activityObject->setUsername($username);
$activityObject->setAvatar($avatar);
$activityObject->setData($data);

$hasChildren = ActivityType::hasChildren($type);
if ($hasChildren) {
	if ($groupNumber != $previousGroupNumber) {
		$activityArray[$groupNumber]['parent'] = $activityObject->toArray();
	} else {
		$activityArray[$groupNumber]['child'][] = $activityObject->toArray();
	}
} else {
	$activityArray[$groupNumber]['parent'] = $activityObject->toArray();
}

Next we add ActivityItemOject to hold the array keys and values.

class ActivityItemObject
{
    /**
     * @var string
     */
    private $type;

    /**
     * @var \DateTime
     */
    private $created;

    /**
     * @var ActivityObject
     */
    private $parent;

    /**
     * @var ActivityObject[]
     */
    private $children;

    public function __construct()
    {
        $this->children = [];
    }
.....

    /**
     * @return array
     */
    public function toArray()
    {
        $children = [];
        foreach ($this->children as $child) {
            $children[] = $child->toArray();
        }

        $array = [
            'type' => $this->type,
            'created' => $this->created,
            'parent' => $this->parent->toArray(),
            'child' => $children
        ];

        return $array;
    }

And finally the completed code:


    /**
     * @param ArrayCollection|Activity[] $activity
     * @return array
     */
    private function convertActivityToArray(array $activity)
    {
        $activityMap = [];

        $previousGroupNumber = 0;
        /** @var Activity $activityEntity */
        foreach ($activity as $activityItem) {
            $groupNumber = $activityItem->getGroupNumber();

            if (array_key_exists($groupNumber, $activityMap) != null) {
                $activityItemObject = $activityMap[$groupNumber];
            } else {
                $activityItemObject = new ActivityItemObject();
            }

            $type = $activityItem->getType();
            $created = $activityItem->getCreatedAt();

            $activityItemObject->setType($type);
            $activityItemObject->setCreated($created);

            $dataStrategyContext = new DataStrategyContext(
                $type,
                $this->request,
                $this->documentaryService,
                $this->commentService);
            $data = $dataStrategyContext->createData($activityItem);

            $user = $activityItem->getUser();
            $name = $user->getName();
            $avatar = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/avatar/' . $user->getAvatar();
            $username = $user->getUsername();

            $activityObject = new ActivityObject();
            $activityObject->setName($name);
            $activityObject->setUsername($username);
            $activityObject->setAvatar($avatar);
            $activityObject->setData($data);

            $hasChildren = ActivityType::hasChildren($type);
            if ($hasChildren) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityItemObject->setParent($activityObject);
                } else {
                    $activityItemObject->addChild($activityObject);
                }
            } else {
                $activityItemObject->setParent($activityObject);
            }

            $activityMap[$groupNumber] = $activityItemObject;

            $previousGroupNumber = $groupNumber;
        }

        $display = [];
        /** @var ActivityItemObject $value */
        foreach ($activityMap as $key => $value) {
            $display[$key] = $value->toArray();
        }

        return $display;
    }