HOW TO WORK WITH
LEGACY CODE
MICHAŁ SZCZUR
PHP Developer since 2010
bass guitarist and trumpeter
michalszczur.pl
Created by /Michał Szczur @partikus
AGENDA
Scope explanation
Dive into legacy mess
How to start and not die
Step by step to heaven
THANKS TO MAREK MATULKA
WHO INSPIRED ME!
WHAT IS LEGACY CODE?
CODE WRITTEN MANY YEARS AGO
BY MYSELF OR OTHER NINJAS
LOW QUALITY CODE
(PRE)HISTORICAL CODE USES NONEXISTING
FRAMEWORKS/LIBRARIES
NO ENVIRONMENT SEPARATION
NO TESTS !!!
WHEN CODE CAN BE CALLED
LEGACY ???
WHEN IT IS
UNMANAGED
WHEN IT IS
TOO BUGGY
WHEN IT IS
TOO HARD TO UNDERSTAND
WHEN IT IS
NONUPGRADABLE
WHEN IT IS
UNDEBUGGABLE
WHEN IT IS
UNREADABLE
WHEN IT IS
TOO COUPLED TO FRAMEWORK
WHEN IT IS
TOO HARD TO ADD NEW FEATURE
WHAT IS NEXT?
FIGHT OR DIE
NO !!!
WE'RE NINJA DEVS
WE LOVE CHALLENGES
LET'S DO IT!
TAKE NEW FRAMEWORK
AND START FROM THE BEGINNING
:(MAYBE SOME DAY IN THE FUTURE
WHAT IS NEXT?
LET'S REFACTOR
BUT WHY ???
EXTENDED LEGACY
CODE
WEB PHP APP
PREASUMPTIONS
YOU DON'T KNOW BUSINESS LOGIC
MOSTLY PROCEDURAL CODE
NO SEPARATION, JUST A FEW LARGE FILES
YOU NEED TO CHANGE STH
ANALYZE
COVER
IT MEANS WRITE TESTS
WHAT SHOULD WE
TEST?
WHY SHOULD WE WRITE TESTS?
LOW LEVEL DOCUMENTATION
FEATURES DESCRIBED BY SCENARIOS (E.G.
GHERKIN)
CLEAN CODE
EASY TO CHANGE (LESS PAIN)
EASY CONTINUOUS DEPLOYMENT
START REFACTORING
# Project structure
/404.php
/database.php
/functions.php
/index.php
/page.php
PROCEDURAL CODE
WRITTEN IN PHP
INLINE PHP FUNCTIONS MIXED WITH HTML,CSS,JS
#/functions.php
function show_all() {
$db = connect_to_db();
$sql = 'SELECT * FROM receivers';
$result = mysql_query($sql) or die(mysql_error());
while ($row = mysql_fetch_array($result)) {
//echo 'ID: ' . $row['id'] . ', mail: ' . $row['mail'];
echo ''.$row['mail'].'';
echo '<form id="'.$row['id'].'" name="n_ID" method="POST" action=
<input type="hidden" name="id" value="'.$row['id'].'" />
<input type="submit" name="delete" value="Delete" />
</form>';
}
if (isset($_POST['delete'])) {
$n_ID = $_POST['id'];
$sql = "DELETE FROM receivers WHERE id = $n_ID";
mysql_query($sql) or die(mysql_error());
WRITE FUNCTIONAL
TEST
// show_all_receivers.js
casper.test.begin('List all receivers', 5, function suite(test) {
casper.start("http://myapp.dev/mailsender.php", function() {
test.assertHttpStatus(200);
test.assertTitle("Homepage | Mail Sender", "Homepage title is expecte
});
casper.thenClick('a#show-all', function() {
test.assertHttpStatus(200);
test.assertTitle(
"Receivers list | Mail Sender",
"Page title is correct"
);
});
casper.run(function() {
test.done();
});
});
PROCEDURAL CODE??
YOU NEED STH MORE
IOC
INVERSION OF CONTROL
DEPENDENCY
INJECTION
IOC IMPLEMENTATION
DON'T REINVENT THE
WHEEL
PACKAGE MANAGER
COMPOSER
RUBYGEMS
MAVEN
COMPONENTS
DEPENDENCY INJECTION
/ /SYMFONY DI PIMPLE AURA DI
WHY SYMFONY
COMPONENTS?
WELL TESTED
DECOUPLED
REUSABLE
WELL DOCUMENTED
AND KNOWN BY COMMUNITY
NEXT STEP IS...
MOVE APP TO /WEB/
# new project structure
/
/web/404.php
/web/database.php
/web/functions.php
/web/index.php
/web/page.php
INSTALL COMPOSER
php -r "readfile('https://getcomposer.org/installer');" > composer-setup.php
php -r "if (hash('SHA384', file_get_contents('composer-setup.php')) ===
php composer-setup.php
php -r "unlink('composer-setup.php');"
INIT COMPOSER CONFIG IN THE ROOT DIR
php composer.phar init
PROJECT STRUCTURE
ls -l
/
/composer.json
/web/
COMPOSER.JSON
{
"name": "michalszczur/legacy-demo",
"authors": [
{
"name": "Michal Szczur",
"email": "job@michalszczur.pl"
}
],
"require": {},
"autoload": {
"psr-4": {
"": "src/"
}
},
"description": "Legacy app demo",
"type": "project",
"license": "proprietary"
ADD DEPENDENCIES
php composer.phar require symfony/dependency-injection
php composer.phar require symfony/config
php composer.phar require symfony/yaml
<?php
# /web/container.php
use SymfonyComponentDependencyInjectionContainerBuilder;
use SymfonyComponentConfigFileLocator;
use SymfonyComponentDependencyInjectionLoaderYamlFileLoader;
require __DIR__.'/../vendor/autoload.php';
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../app/con
$loader->load('services.yml');
$container->compile();
WHICH SERVICES DO WE REALLY
NEED?
function show_all()
{
# Database connection needed
$db = connect_to_db();
$sql = 'SELECT * FROM mail_sender';
$result = mysql_query($sql) or die(mysql_error());
//...
CREATE SEVICES.YMLDEFINITION
SERVICES.YML
parameters:
db_host: localhost
db_port: 3306
db_name: legacyapp
db_user: myuser
db_pass: mypass
services:
db:
class: PDO
arguments: ['mysql:port=%db_port%;host=%db_host%;dbname=%db_name%'
receiver_repository:
class: PDOReceiverRepository
arguments: ['@db']
USE CONTAINER AND REPOSITORY
#index.php
...
require 'container.php';
...
function show_all()
{
# Database connection needed
$receiverRepository = $container->get('receiver_repository');
$result = $receiverRepository->findAll();
...
#index.php
...
$result = $receiverRepository->findAll();
foreach ($result as $row) {
echo ''.$row['mail'].'';
echo '<form id="'.$row['id'].'" name="n_ID" method="POST" action="mai
<input type="hidden" name="id" value="'.$row['id'].'" />
<input type="submit" name="delete" value="Usuń" />
</form>';
}
...
PHP ~ HTML
STILL MIXED TOGETHER
TWIG
TEMPLATE ENGINE FOR PHP
INSTALL TWIG USING COMPOSER
php composer.phar require twig/twig
SERVICES.YML
parameters:
...
twig_paths:
- app/Resources/views
services:
...
twig.loader:
class: Twig_Loader_Filesystem
arguments: ['%twig_paths%']
twig:
class: Twig_Environment
arguments: ['@twig.loader']
#index.php
...
$twig = $container->get('@twig');
$result = $receiverRepository->findAll();
echo $twig->render(
'receiver_list.html.twig',
['receivers' => $result]
);
...
{% for receiver in receivers %}
<form id="{{ receiver.id }}" name="n_ID" method="POST" action="mail_sender.ph
<input type="hidden" name="id" value="{{ receiver.id }}" />
<input type="submit" name="delete" value="Delete" />
</form>
{% endfor %}
BEFORE
function show_all()
{
$db = connect_to_db();
$sql = 'SELECT * FROM mail_sender';
$result = mysql_query($sql) or die(mysql_error());
while ($row = mysql_fetch_array($result)) {
//echo 'ID: ' . $row['id'] . ', mail: ' . $row['mail'];
echo ''.$row['mail'].'';
echo '<form id="'.$row['id'].'" name="n_ID" method="POST" action="mai
<input type="hidden" name="id" value="'.$row['id'].'" />
<input type="submit" name="delete" value="Usuń" />
</form>';
}
...
mysql_free_result($result);
mysql_close($db);
}
AFTER
function show_all() {
$receiverRepository = $container->get('receiver_repository');
$twig = $container->get('@twig');
$result = $receiverRepository->findAll();
echo $twig->render(
'receiver_list.html.twig',
['receivers' => $result]
...
);
LET'S SUM UP
COMMUNICATION
ANALYZE
TESTS
SMALL STEPS
NOT EVERYTHING SIMUNTANOUSLY
PACKAGE MANAGER
DEPENDENCY INJECTION
THIRD PARTY LIBRARIES
DESIGN PATTERNS
SOLID
DECOUPLE
TESTS
QUESTIONS?
SLIDES: SLIDESHARE.NET/MICHASZCZUR
FEEDBACK: JOIND.IN/EVENT/CODETECON-20161
TWITTER: @PARTIKUS
HOMEPAGE: MICHALSZCZUR.PL
THANKS!
LINKS
Marek Matulka - Modernising the Legacy
CasperJS
Symfony Components
Working Effectively with Legacy Code by Michael Feathers
Page Object Pattern (Martin Fowler)
Page Object Pattern (Selenium Docs)

How to work with legacy code