You don't know me, but sometimes I work on fun things:
— Dennie Van Tassel, 1974
Code having the same clean logical structure as a plate of spaghetti.
— Richard Conway, 1978
Spaghetti code is incomprehensible unless one has the discipline to follow each noodle through from one side to the other.
— Michael O. Church, 2012
label_b:
if ($condition_a) {
goto label_a;
}
while ($condition_b) {
if ($condition_c) {
goto label_b;
}
}
label_a:
echo 'Well, that was exciting.';
Languages all mixed together like mac & cheese
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<?
$res = mysql_query("select title from pages where name = 'index'");
if ($res) {
$title = mysql_fetch_assoc($res)[0]['title'];
} else {
$title = 'Default title';
}
?>
<head>
= $title ?>
</head>
<body>
<?
$res = mysql_query("select * from posts order by date desc limit 10");
if (!$res) {
echo "Whoops, no posts!
";
} else {
while ($post = mysql_fetch_assoc($res)) {
?>
<?= $post['title'] ?>
<?= $post['body'] ?>
<?
}
}
?>
</body>
function getUserById($id) {
if ($id >= 0) {
$res = mysql_query("
select * from users
where id = $id
");
if ($res) {
return mysql_fetch_assoc($res)[0];
}
}
return null;
}
function getUserByNameAndPassword($name, $password) {
if ($name) {
$res = mysql_query("
select * from users
where id = $id and password = '$password'
");
if ($res) {
return mysql_fetch_assoc($res)[0];
}
}
return null;
}
function saveNewUser($user) {
if (strlen($user['password']) > 5) {
return false;
}
if (!$user['name'] && !$user['password']) {
return false;
}
// ... more business logic ...
mysql_query("
insert into users (name, password)
values ({$user['name']}, {$user['password']}
");
}
$bullet_app->path('/pets', function() use ($bullet_app) {
$bullet_app->post(function($request) {
$name = $request->post('name');
$type = $request->post('type');
if (!$name || strlen($name) < 3) {
throw new Exception("Pet's name must be 3 or more characters");
}
$res = mysql_query("
select count(name) from pets where name = '$name'
");
if (mysql_fetch($res)[0][0] > 0)
throw new Exception("Pet's name already exists");
}
if ($type != 'dog' && $type != 'cat' && $type != 'fish') {
throw new Exception("Pet must be a dog, a cat or a fish");
}
// save new pet to the database
});
});
Look, it's running. We shipped! How bad could it really be?
$ curl -sS https://getcomposer.org/installer | php
$ ./composer.phar init
$ ./composer.phar install
$ ls
vendor composer.json composer.lock
Failing that, installation with Composer is absurdly straightforward:
$ ./composer.phar require --dev phpunit/phpunit
$ vi tests/MyTest.php
class MyTest extends \PHPUnit_Framework_TestCase {
public function testThatMathStillWorks() {
$this->assertEquals(4, 2 + 2);
}
}
$ ./vendor/bin/phpunit tests/
PHPUnit 4.3.4 by Sebastian Bergmann.
.
Time: 14 ms, Memory: 2.50Mb
OK (1 test, 1 assertion)
or
<body>
<?
$res = mysql_query("select * from posts order by date desc limit 10");
if (!$res) {
echo "Whoops, no posts!
";
} else {
while ($post = mysql_fetch_assoc($res)) {
?>
<?= $post['title'] ?>
<?= $post['body'] ?>
<?
}
}
?>
</body>
function getTemplateFile($templateName) {
$config = new Config(); // <-- this is a dependency!
return $config->templateDirectory.$templateName.'.html.php';
}
When your code has too much responsibility, take some away.
function getTemplateFile($templateName, $c) { // <-- dependency injected!
return $c->templateDirectory.$templateName.'.html.php';
}
class Template {
public function getTemplateFile($templateName, $config) {
return $config->templateDirectory.$templateName.'.html.php';
}
}
class TemplateTest extends PHPUnit_Framework_TestCase {
public function testGetFile() {
$config = $this->getMock('stdClass');
$config->templateDirectory = 'foo/';
$template = new Template();
$result = $template->getTemplateFile('bar', $config);
$this->assertEquals('foo/bar.html.php', $result);
}
}
function getUserById($id) {
if ($id >= 0) {
$res = mysql_query("
select * from users
where id = $id
");
if ($res) {
return mysql_fetch_assoc($res)[0];
}
}
return null;
}
function getUserByNameAndPassword($name, $password) {
if ($name) {
$res = mysql_query("
select * from users
where id = $id and password = '$password'
");
if ($res) {
return mysql_fetch_assoc($res)[0];
}
}
return null;
}
Hey, those should be a in a User object! Refactor ALL THE THINGS!
Yes, they should. Eventually!
getUserById
class User {
public $details;
protected $_db;
public function __construct($db, $details) {
$this->_db = $db;
$this->details = $details;
}
public static function byId($db, $id) { // injecting a database service
$stmt = $db->prepare('select * from users where id = ?');
if ($stmt && $stmt->bind_param('i', $id) && $stmt->execute()
&& ($result = $stmt->get_result()) !== false
&& ($user = $result->fetch_assoc()) !== null
) {
$stmt->close();
} else return null;
return new self($db, $user);
}
}
We passed in an external service to talk to the database, which means we can also pass in a fake to test the logic.
public function testUserByIdPrepareFail() {
$db = $this->getMock('stdClass', ['prepare']);
$db->expects($this->once())->method('prepare')->willReturn(false);
$this->assertNull(User::byId($db, 123));
}
function getUserById($id) {
// return the associative array of user details if the user is found
// return null otherwise
}
function getUserById($id) {
return ($user = User::byId(new mysqli(), $id)) ? $user->details : null;
}
getUserById
interface!
function getUserByNameAndPassword($name, $password) {
if ($name) {
$res = mysql_query("
select * from users
where id = $id and password = '$password'
");
if ($res) {
return mysql_fetch_assoc($res)[0];
}
}
return null;
}
function getUserByNameAndPassword($name, $password) {
if ($name) {
$res = mysql_query("
select * from users
where id = $id and password = '$password' and active = 1
");
if ($res) {
return mysql_fetch_assoc($res)[0];
}
}
return null;
}
User
class...
class User {
public static function byName($db, $name) {
// just like byId, but with the name as the identifier
}
public login($pass) {
if ($this->details['password'] !== $pass) {
throw new Exception("Passwords don't match");
}
if ($this->details['active'] !== 1) {
throw new Exception("User is not active");
}
}
}
The tests cover the business logic
/*
* @expectedException Exception
* @expectedExceptionMessage Passwords don't match
*/
public function testUserLoginFailsWithWrongPassword() {
$user = new User(null, ['password' => 'password']);
$user->login('wrong password');
}
/*
* @expectedException Exception
* @expectedExceptionMessage User is not active
*/
public function testUserLoginFailsWithInactiveUser() {
$user = new User(null, ['active' => 0, 'password' => 'password']);
$user->login('password');
}
...and the final integration should look familiar
function getUserByNameAndPassword($name, $password) {
// return null if the user is not found
// return null if the password does not match
// return null if the user is not active
// return the associative array of user details otherwise
}
becomes
function getUserByNameAndPassword($name, $password) {
$user = User::byName(new mysqli(), $name);
if (!$user) {
return null;
}
try {
$user->login($password);
} catch (Exception $e) {
return null;
}
return $user->details;
}
Refactor old code while simultaneously adding a new feature
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<?
$res = mysql_query("select title from pages where name = 'index'");
if ($res) {
$title = mysql_fetch_assoc($res)[0]['title'];
} else {
$title = 'Default title';
}
?>
<head>
= $title ?>
</head>
This needs a model, a view and a controller! RIGHT NOW!
class Page {
protected $db;
protected $details;
public function __construct($db, $details = []) {
$this->db = $db;
$this->details = $details;
}
public static function load($db, $name = '') {
// load from db by page name
}
public function title() {
return (isset($this->details['title']) && $this->details['title'])
? $this->details['title']
: 'Default title';
}
}
public function testPageTitleIsDefaultIfMissing() {
$page = new Page(null);
$this->assertEquals('Default title', $page->title());
}
public function testPageTitleIsDefaultIfEmpty() {
$page = new Page(null, ['title' => '']);
$this->assertEquals('Default title', $page->title());
}
public function testPageTitleIsSet() {
$page = new Page(null, ['title' => 'my title']);
$this->assertEquals('my title', $page->title());
}
<?php $page = Page::load(new mysqli(), 'index'); ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
= $page->title() ?>
</head>
Questions?
https://joind.in/13727