A long time ago a php file would contain all kind of stuff to make up a web page. It performed queries, handled user input and displayed or included HTML. Today these files contain mostly classes that work together to generate a similar page, but in a different way. This Object Oriented Programming is the standard today, but how do you get from the old scripts to this way of programming without breaking the current behavior.
Write a dispatcher
Before refactor code can start, you most find out how the server decides which script to execute when you visit a page. Sometimes it is very obvious, the url path matches the file location. Sometimes it is more obscure. You need to recreate this selection of the right file when it runs over a single dispatch script.
If you are convinced you have found the right way to determine what script must be executed, it is time to test that. If you haven’t any devops skills, please consult someone who does, because you have to write a set of rewrite rules that routes all to the new dispatch script. This script will direct the request to the right file to execute. If all is done right the site will behave exactly as before.
# Rewrite example
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ./index.php [L]
Select a framework
The next thing to do is setting up a framework that you will use to handle the user requests and return a response. I suggest to look around on packagist for a suitable framework, but feel free to create your own. The framework of your choosing should at least have a routing mechanism that will be used to execute the right code.
{
"name": "my/website",
"require": {
"slim/slim": "^4.4",
"slim/psr7": "^1.0"
}
}
Create a fallback class
As soon as the framework of your choosing is in place, you can start implementing it. Start with a route that catches every url and routes it to a specific class method. The code you created to dispatch the right script can now move to a protected method within your new fallback class. Using output buffering you can populate the response object with the output data you received from your new dispatch method. Everything should still be working after this change.
include '../vendor/autoload.php';
$app = \Slim\Factory\AppFactory::create();
// routes
$app->get('/{url: .*}', function ($request, $response) {
$fallback = new LegacyHandler(); // catch all
return $fallback->handle($request, $response);
});
$app->run();
Add a controller
Next up is implementing a design pattern like model-view-controller to improve the existing code. Despite of the name, the MVC pattern starts with a controller. Most systems have some kind of user, news or product management, so choose one of those resources for your first controller.
namespace App\Controllers;
class UserController {
public function listUsers($request, $response) {}
public function edit($request, $response) {}
public function save($request, $response) {}
}
Gather all the actions in the file(s) for the resource you selected and copy them into the created controller class. For instance if you find a code snippet that handles the post data, it probably will go inside a save or store method, a HTML form would most likely go in a create or edit method.
When all parts of the selected resource are placed in the controller, it is time make sure them can be accessed by adding them to the routing. You can reuse existing urls or create new and remove the old ones later.
// routes
$app->group('/users', function ($group) {
$controller = new UserController();
$group->get('/edit/{id:[0-9]+}', function ($request, $response) use ($controller) {
return $controller->edit($request, $response);
});
$group->post('{id:[0-9]+}', function ($request, $response) use ($controller) {
return $controller->save($request, $response);
});
});
$app->get('/{url: .*}', function ($request, $response) {
// fallback
});
Make a Model
To improve your code further you should implement some kind of data access layer. Object-relational mapping or ORM is a technique where you use objects to retrieve or store data in a database. These objects we call Models and fit perfectly in the MVC pattern. A well documented and easy to learn ORM system is the one from the Laravel framework: Eloquent ORM.
When an ORM is added to the project, it is time to look into the database to look for the table that is used in the newly created Controller and create a Model object for it. Depending on the chosen system, the Model may look like this:
class User extends Illuminate\Database\Eloquent\Model {
/** @var string $table The database table */
protected $table = 'users'; // is default
/** @var string $primaryKey The primary key [default: id] */
protected $primaryKey = 'user_id';
/** @var array $casts */
protected $casts = [
'active' => 'boolean',
'visits' => 'integer',
];
}
Now it is time to cleanup the Controller and replace the queries with the new Model until no SQL is left inside the Controller.
class UserController {
public function save($request, $response) {
$post = $request->getParsedBody();
$user = User::firstOrNew(['user_id' => $post['id'] ?? null);
$user->username = $post['username'];
$user->active = true;
// ... more lines to store
$user->save();
return $reponse->withHeader('location', '/users');
}
}
Next up the View
Inside the Controller there is still some messy HTML or include stuff going on to render a page for the end-user. This can be cleanup by a View and/or a template engine. In general, if all the data that render a page can be provided by one Controller method it would be find to just use a template engine as a View, if not; Create a View that uses the template engine.
class UserView {
/** @var \Twig\Environment $template;
protected $template;
/**
* @param User[] $users
* @return string
*/
public function listUsers($users) {
return $this->template->render('user-list.twig', $users);
}
}
When all templates are in place and implemented in your View, all methods inside the Controller that present data will look the same:
- Fetch user input from the request
- Send the user input to the Model
- Use the View to render output
- Add output to response
class UserController {
/** @var UserView $userView */
protected $userView;
public function show($request, $response) {
$id = $request->getAttribute('id');
$output = $userView->showUser(
User::findOrFail($id)
);
$response->getBody()->write($output);
return $response;
}
}
Conclusive
First prepare your code and server configuration in such a way that you can start adding or improving code without breaking the current implementation. Second take one part of your system and implement a model-view-controller for it. Than take the next part, and the next part, until all of your code is refactored. This technique is called the Strangler Pattern.