Photo of me squinting into the camera
Portfolio

Code Modernization

Summary

I helped refactor and modernize critical parts of a PHP/jQuery application. On the frontend, I replaced a bespoke, jQuery-based UI framework with Vue.js. For the PHP backend, I introduced type-hinted facades, PSR-2 code formatting, and a flexible data model.

Screenshot of the commit that introduced PSR-2 formatting
The point of no return. When I introduced PSR-2 code formatting, more than 243,000 lines of code were changed across four repositories. 😅 View commit

PKP's open source software for scholarly publishing is more than 20 years old. With a small dev team and a large codebase, it was difficult to prioritize major refactors. But we knew that modernizing the codebase would be worth it in the long run, by reducing maintenance costs and speeding up future development.

The biggest challenge we faced was finding ways to isolate parts of the codebase, so that we didn't have to refactor the entire application all at once. In the sections below, I describe the major refactors I conducted across the frontend and backend over a period of 4-5 years.

jQuery → Vue.js

The first major refactor that I worked on was the introduction of Vue. At the time, the applications' interactive UI components were implemented as a custom, undocumented framework based on jQuery. When the UI needed to be updated, the component made a request to its handler and received an HTML blob with the updated template.

This made it challenging to introduce Vue in small pieces. Whenever the old framework deleted a branch of the DOM, any Vue component that was mounted within that branch would be removed, losing the state data and causing memory leaks. In an ideal world, I would have mounted Vue to the root element and isolated the destructive components of the old framework in branches of the DOM — a kind of "dead branch" of the element tree.

Example code showing how the old UI framework took over a branch of the DOM.
<div id="app">
    <h1>{{ title }}</h1>

    <!--
        An empty element into which a jQuery component
        will render a template, destroying the element
        and recreating it.
    -->
    <div id="jquery-component">
        <script type="text/javascript">
            $('#jquery-component').pkpHandler(
                $.pkp.controllers.ExampleHandler
            );
        </script>
    </div>
</div>

<script type="text/javascript">
    /**
     * Mounting the Vue app
     */
    var app = new Vue({
        el: '#app',
        data: {
            title: '👋 Hi there!'
        }
    });
</script>

Vue warns you not to do this. When two sources of authority control the DOM, they don't know what the other is up to and the DOM can fall out of sync with Vue's state data.

Screenshot of the console warning: Templates should only be responsible for mapping the state to the UI
Vue warns you of the dangers of messing with its templates, but every refactor requires breaking a convention or two.

But it's just a warning. It would be safe to do as long as none of the Vue components try to interact with the dead branch. However, I hadn't yet proven the business case for the migration to Vue and we didn't have the resources to commit to a rewrite of every page of the app. I needed to figure out a way to reverse the structure — instantiate a Vue app inside of a component within the old framework — without causing memory leaks, so that we could introduce Vue one component at a time.

I solved this by mounting each new Vue-based component as a separate Vue app and maintaining a central registry of these apps. Whenever a component in the old framework destroys the DOM, it uses the registry to safely destroy any Vue apps mounted within a "dead branch" of the DOM. Instead of having a single-page app, we had something like a "many-app page".

This allowed us to move forward with rewrites of critical components, weaving them into the old framework without having to rewrite that framework significantly. In this way, we were able to progressively refactor the application without significantly slowing down the pace of feature development.

I used a number of other techniques to glue components from both frameworks togethers. I added toast-style notifications and used a global event bus so that they could be triggered by either UI framework. I devised a way to open a modal controlled by the old jQuery-based framework from within a Vue component.

It got pretty ugly at times, but I made steady progress deprecating and removing the old jQuery UI. When I had enough UI components built in Vue, I swapped out the root component of the jQuery framework. Now, instead of having a few Vue apps mounted inside of a jQuery-based UI, every page in the application is controlled by a single Vue component, opening the way towards modern single-page app concepts like routing.

Modern PHP and Laravel

PHP has come a long way in the 20 years since I began writing it. Modern PHP has everything important for developer experience: type safety, namespaces and imports, support for inheritance and composition, dependency injection, and shared tools for code formatting. But any application written before PHP 7 wouldn't have access to all of that.

At PKP, we had more than a quarter of a million lines of code written without these conventions. Modernizing this codebase was daunting, but it was hard to justify maintaining our outdated application architecture when the PHP community had evolved with reliable frameworks like Symfony and Laravel, and the adoption of PSR standards.

I introduced the PSR-2 code styles and wrote commit hooks to apply the formatting automatically using PHP-CS-Fixer (see the big commit). I also integrated Laravel's Service Providers so that we could take advantage of automatic dependency injection (source).

I am most proud of the refactoring I did to introduce stricter type hinting. We had dozens of DAO classes in our application. Each DAO is responsible for reading and writing data for one model. Almost all of this code was loosely typed and, the way it was structured, there was a lot of code duplication, with SQL query fragments written over and over again. Below is what a typical DAO looked like.

Example of one of the old DAOs.
class ReviewAssignmentDAO extends DAO
{
    public function getByRound($round)
    {
        $result = $this->retrieve(
            'SELECT * FROM ... WHERE round=?',
            [(int) $round]
        );
        return $this->fromRow($result);
    }

    public function getCompletedByRound($round)
    {
        $result = $this->retrieve(
            'SELECT * FROM ... WHERE round=? and completed=1',
            [(int) $round]
        );
        return $this->fromRow($result);
    }
}

Even the way we used a DAO required us to manually type hint the response.

/** @var $dao ReviewAssignmentDAO */
$dao = DAORegistry::getDAO('ReviewAssignmentDAO');

Using the DAOs this way was prone to errors. And whenever we needed a DAO to retrieve slightly different data, we had to write a new method with a new SQL query. As a result, our codebase was littered with large DAO classes with a lot of repetitive code.

We didn't have the resources to refactor all of our DAOs at once, so I introduced a Repository pattern that was a fully type-hinted wrapper class.

An example of a type-hinted wrapper for the DAOs.
<?php
namespace PKP\submission;

use DAO;

class SubmissionRepository
{
    public DAO $dao;

    public function __construct(DAO $dao)
    {
        $this->dao = $dao;
    }

    public function get(int $id): Submission
    {
        return $this->dao->getById($id);
    }
}

This provided type hinting without completely deprecating the use of the underlying DAOs. We could start using this new pattern without refactoring everywhere the old pattern was used. But it resulted in more code for us to maintain, and we didn't yet have a clear path to refactor the DAOs themselves. I needed a way to eliminate all of those duplicate query methods that had accumulated in the DAOs.

To solve this problem, I integrated Laravel's Query Builder and created a fluent interface for building modular database queries.

Example of a fluent interface for getting a collection of objects.
<?php
namespace PKP\submissions;

use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;

class Collector
{
    public ?array $assignedTo = null;
    public ?array $status = null;

    public function filterByAssignedTo(?array $assignedTo): self
    {
        $this->assignedTo = $assignedTo;
        return $this;
    }

    public function filterByStatus(?array $status): self
    {
        $this->status = $status;
        return $this;
    }

    public function getQueryBuilder(): Builder
    {
        return DB::table(...)
            ->select(...)
            ->when(
                $this->status !== null,
                function (Builder $q) {
                    $q->whereIn('status', $this->status);
                }
            )
            ->when(
                $this->assignedTo !== null,
                function (Builder $q) {
                    $q->whereIn('assignedTo', $this->assignedTo);
                }
            );
    }
}

This allowed me to refactor dozens of DAO methods into one Collector, resulting in a fluent, type-hinted interface that supports autocomplete in any code editor.

The fluent, type-hinted collectors helped our developers build queries without learning the database structure of every model. See this Collector

After merging these changes, I updated the documentation on DAOs and Repositories so that my teammates could learn and use the new patterns. Although it wasn't a formal part of my role, I often worked on documentation and developer relations for our open source software.

Photo of my dog Peanut