The strategy I used to rewrite an entire legacy PHP application in Laravel by myself
I was brought on to perform a full rewrite of Septic Sitter's "Legacy PHP" codebase to a new application built on the Laravel framework. This consisted of a complete rewrite of the "Cloud" application, the "on site" management application, and the client facing web frontend.
My primary task at Septic Sitter was rewriting the various "Legacy PHP" based web applications used by septic sitter into a modularized codebase, with all functionality built as reusable modules shared between the various applications required to operate this IOT platform. I chose the Laravel framework to accomplish this goal because of it's compatibility with the original project and it's "Rails like" paradigm.
The following are some noteworthy points for anyone looking to do something similar in the future.
The general development strategy for this rewrite was to do it "one page at a time". This means that rewriting would focus on a single application route and rewrite its framework through several passes - a pass to separate the html markup and convert to Blade templates.
A second pass to rewrite application logic into reusable services applcation logic into discrete services and classes that could be reused over and over as needed in the app. Progress was very slow at first, but since many pages in the app shared functionality with other pages, each subsequent page was completed exponentially faster than the last.
The following sections layout the strategy I used to accomplish porting this application, these steps didn't always happen in the order written, but were the same for each page
In the event this project could not be completed by launch day, a contingency plan was put in place. In the event that certain sections of the rewrite couldn't be finished in time for launch, the system would simply fall back to the legacy php version of the code. This meant that the new application needed to be 100% backwards compatible with the original application, but do it in such a way that it doesn't impede the development of a new and improved architecture.
Sessions in Laravel are not handled using the default PHP session system, meaning the first issue encountered was making sessions compatible between the Legacy PHP app and the Laravel rewrite. This was accomplished using a "phpsession" middleware, which if added to a route, would copy the contents of the php session into the laravel session data structure. The approach had some drawbacks, namely, it wasn't really possible to use anything except "legacy" session data until the rewrite was completed.
A method was required to decide which version of the application to serve for each particular route of the system. In the event that a website route was not available yet in the rewrite of the application, we needed a method to fallback to the original app. This way, if my estimations were incorrect and I wasn't actually going to be able to complete the rewrite before launch, a smooth transition could still happen over time, gracefully falling back to the orignal code. Because the original version of the app used Apache .htaccess files to handle application routing, it made sense to re use this approach to handle fallbacks. The development process of the new site was exactly the same as a standalone Laravel site, you would define your routes within the routes.php file of your Laravel application, the magic happened by hard coding that route in the htaccess file. Instead of pointing to the entry point of the original application, we could point to the index.php file in the root of the laravel application, afterwhich, Laravel would use its internal routes file to determine what it should be serving.
A common pattern you see in many "legacy" PHP applications is business logic written inline with the layout template that defines the look and design of your site in a web browser.
You end up with code that looks something like this
Hello <?php
$connection = connect_to_database('user_database', 'my_password');
$current_user = find_current_user_in_database($connection);
echo $current_user->first_name;
?>
Welcome to your online bank account!
Your current balance is $<?php
$account_balanace = get_account_balance_for_user($current_user);
echo $account_balance;
?>
By mixing logic and layout in this way, our code becomes very difficult to read, reuse, and maintain.
The solution is to separate these two pieces of our application into discrete reusable entities. The layout templates were rewritten as Blade templates and the logic was rewritten to a modularized service layer.
Using Laravel & Blade, we can rewrite the code above into something that looks more like the this
Hello {{ $name }}
Welcome to your online bank account!
Your current balance is ${{ $balance }}
These rewritten templates were matched with controller functions appropriate to the action they performed in the original application.
Values were hard coded to allow for front end development to happen simultaneously with a rewrite of the application logic.
<?php
// AccountController.php
public function show()
{
/**
An example controller function. Hard coded values were passed to allow the service layer to be developed
without interfering with the front end rewrite. Hard coded values were replaced overtime with real code
as the service layer was developed
*/
return view("account.show", [
'name' => "Bob",
'balance' => 3212.76,
]);
}
The logic that was in-lined with the layout templates in the original application was rewritten as a reusable service layer.
The general strategy for converting the code was "convert as necessary" meaning the original templates were manually parsed line by line and any logic that was encountered would be re organized, rewritten, and tested before continuing to parse the original code further.
This process is pretty slow and tedious for the first few pages, but tends to speed up significantly as application development progresses. This is because much of the logic that makes up a web page tends to be shared on many other pages. By re organizing the codebase into reusable modules, any time subsequent pages needed to implement functionality that was previously encountered elsewhere in the application, no further development was necessary, we could simply reuse the original code.
By rewriting to a service layer the functionality previously limited to use on a single page could now be used and reused anywhere
<?php
public function show(User $user, AccountService $accountService)
{
/**
The previously hard coded controller function from the previous example,
now rewritten with actual functionality. AccountService is a reusable
module used to organize any methods and actions related to account
management into a single reusable source
*/
return view("account.show", [
'name' => $user->name,
'balance' => $accountService->getBalanceForUser($user),
]);
}
Because of the requirement to gracefully fall back to the original application, this meant that Eloquent needed to be massaged into the original schema and not the other way around. This is a problem because Eloquent, by default, is very opinionated in how it expects you to name your database tables and relationships.
We accomodated this problem by manually setting table names and primary keys on Eloquent models to those of the original application. The models themselves, however, retained the "Rails like" opinionated structure Eloquent expected. This allowed the dev team to continue to use their original schema while still writing code in the style expected of experienced Laravel developers. It also leaves open the door to painlessly refactor the original schema in the future.
Model relationships, much like services, were written on an 'as necessary" basis. If logic being rewritten to a service layer required database access, the code was written and tested at that time. This method allowed for completing the rewrite one feature at a time, but as an added bonus, also worked as a method to remove unused features and from the original codebase.
One of the main goals I wanted to achieve with the rewrite of the septic sitter application was to unify the codebase of the on site hub that would be in stalled in the clients home and the codebase of the cloud based management app. These two applications perform exactly the same tasks with only a few differences between them. This meant that if I could find a way to re use the codebase of the cloud app and find a way to enable/disable site specific code, that I could essentially rewrite the management hub app for free.
This was accomplished through the use of feature flags and abstraction drivers.
Feature flags are a boolean (yes/no) value attached to a customers account that let the application know whether they should have access to certain features. A common use case for this is for staging your code to production environments, where newly written features and code are loaded and enabled on your production environment, but these features are only enabled for internal team members and specially flagged clients who have been tasked with helping you test these additions to your app. By only enabling features for a small number of users, you can find and fix issues that may have slipped based your quality assurance team that may break functionaity or have unforseen useability issues, etc before these issues are shipped out to the majority of your customers
In septic sitter, this idea was used to flag features on/off that should only be used on the hub management app or cloud system, while allowing the majority of the functionlity to be shared between these two systems. For example, on the hub, we run code that communicates with the IOT device installed in your septic system awhich obviously doesn't need to happen in the cloud and on the cloud side there is code that handles billing and account administration
An abstraction driver is a software development pattern where the implementation details are abstracted away and your app only communicates with them at a high level. This pattern is often seen when saving data to persistant storage. Your application doesn't care whether you are saving this information to a hard drive, a database, or a team of monkeys furiously typing away on a bunch of typewriters. As long as it can store the data and retrieve it at a later date, your app doesn't care about the implementation details
We used this pattern in a clever way with septic sitter. Although feature flags will prevent you from accessing functionality in environments where you shouldn't be able to do so, in the event the user finds a way around this, "null drivers", essentially drivers that do absolutely nothing, would handle that request, and return the expected result even if it didn't do anything.
Consider the situation where the cloud based system queries its "IOT device installed in a septic system" Obvkously this isn't going to exist in the cloud environment so instead, a fake result will be returned saying that there was no results.
This is useful because since PHP code is interpreted and van be viewed by anyone with the knowledge to access it, any trade secret code that we don't want users to see can be omitted from the on site management hub while still allowing us to share a codebase.
One of the key features of septic sitter is that if there is a problem such as a tank overflow or freezing, the septic provider and the home owner need to be notified In the original legacy application, this functionality did exist, but the code to handle it was very complicated, inflexible, and only supported notifications via email.
In the rewrite, this functionality was replaced by Laravels awesome Notification system (https://laravel.com/docs/5.6/notifications)
By replacing the notification system, notifications were now much more flexible and could communicate with end users not just via email, but SMS, slack messages, and many other ways via custom notification channels.
When viewing graphs on the original application, when viewing graphs, One of the biggest bottlenecks that I found that upon page load, a separate ajax call was made for every single sensor connected to the Septic IOT device. I solved this issue by using an HTTP Stream that send all data needed to build graphs in a single request. Because this data streamed in, we could load and render graphs one by one instead of having to wait for all data to be loaded to render which would greatly reduce the perceived load times to the end users would have been made much more managable using this method. Many other performance improvements were also added and load times went from an average of 10-30 seconds to 0-3 seconds depending on the customer. Thats a huge improvement!
The Septic Sitter rewrite was a very ambitious project that took a lot of late nights and long hours, but I took it as a personal challenge to myself to see it completed. This rewrite was a real highlight of my career and I hope that the work I did really helps Septic Sitter become very successful in the future.
A booking system for the RCMP barracks and housing programs
An Online Marketplace For Buying And Selling Music Plugins
An Dealer to Manufacturer financing application used in 300+ powersport dealerships across Canada