In this session, we'll look at a typical PHP application, review a few of the horrible mistakes the fictional developer made, and then refactor the app according to some best practices. Along the way you might even learn a thing or two about PHP you don't already know.
2. Who is this guy?
Jeremy Kendall
I love to code
I love to take pictures
I'm terribly forgetful
I work at OpenSky
3. Following Along
You can follow along with the presentation
code at github.com.
https://github.com/jeremykendall/bookshelf
Before = oh-the-horror
After = much-better
6. . . . I frequently caused
as many problems
as I solved.
7. “Always code as if the person who ends up
maintaining your code is a violent psychopath
who knows where you live.”
http://c2.com/cgi/wiki?CodeForTheMaintainer
8. “Alternatively, always code and comment in such
a way that if someone a few notches junior picks
up the code, they will take pleasure in reading
and learning from it.”
http://c2.com/cgi/wiki?CodeForTheMaintainer
9. Let's Solve a Problem Together
• We'll review a “typical” PHP application
• Find horrible mistakes
• Correct those mistakes
• Make a cool improvement
• Learn and apply better practices
10. Typical application?
• I've seen code similar to the samples I'll show
in almost every app I've ever touched.
• I've made the same mistakes in almost every
app I wrote in my early days.
• I'm guessing you've seen and made similar
mistakes too.
11. What's the Problem?
• Crappy application
• CRUD bookshelf
• What does it do?:
– Display books
– Add books
– Edit books
– Delete books
12. What's There?
• Database (MySQL)
• View (index.php)
• Form (book-form.php)
• Form processor (process-form.php)
• Delete (delete-book.php)
26. process-book.php
// Database connection code
$ins = "INSERT INTO bookshelf (title, author) "
."VALUES ('{$_POST['title']}', '{$_POST['author']}')";
$up = "UPDATE bookshelf SET title = '{$_POST['title']}', "
. "author = '{$_POST['author']}' WHERE id = {$_POST['id']}";
if (empty($_POST['id'])) {
mysql_query($ins);
} else {
mysql_query($up);
}
27. process-book.php
// Database connection code
$ins = "INSERT INTO bookshelf (title, author) "
."VALUES ('{$_POST['title']}', '{$_POST['author']}')";
$up = "UPDATE bookshelf SET title = '{$_POST['title']}', "
. "author = '{$_POST['author']}' WHERE id = {$_POST['id']}";
if (empty($_POST['id'])) {
mysql_query($ins);
} else {
mysql_query($up);
}
28. process-book.php
// Database connection code
$ins = "INSERT INTO bookshelf (title, author) "
."VALUES ('{$_POST['title']}', '{$_POST['author']}')";
$up = "UPDATE bookshelf SET title = '{$_POST['title']}', "
. "author = '{$_POST['author']}' WHERE id = {$_POST['id']}";
if (empty($_POST['id'])) {
mysql_query($ins);
} else {
mysql_query($up);
}
29. process-book.php
// Database connection code
$ins = "INSERT INTO bookshelf (title, author) "
."VALUES ('{$_POST['title']}', '{$_POST['author']}')";
$up = "UPDATE bookshelf SET title = '{$_POST['title']}', "
. "author = '{$_POST['author']}' WHERE id = {$_POST['id']}";
if (empty($_POST['id'])) {
mysql_query($ins);
} else {
mysql_query($up);
}
30. process-book.php
// Database connection code
$ins = "INSERT INTO bookshelf (title, author) "
."VALUES ('{$_POST['title']}', '{$_POST['author']}')";
$up = "UPDATE bookshelf SET title = '{$_POST['title']}', "
. "author = '{$_POST['author']}' WHERE id = {$_POST['id']}";
if (empty($_POST['id'])) {
mysql_query($ins);
} else {
mysql_query($up);
}
31.
32. Glaring Problems?
• Code duplication
• Input isn't filtered
• Output isn't escaped
• Vendor specific functions
• User-provided data used in SQL
• Did you see any others?
33. Deprecated!
• mysql extension deprecated as of 5.5.0
• Use mysqli or PDO
• Read the MySQL “Choosing an API” on
php.net: http://bit.ly/13zur2e
34. Code Duplication
$db = mysql_connect('localhost', 'testuser', 'testpass');
mysql_select_db('bookshelf', $db);
• Violates DRY principle
• Maintenance nightmare
• The next developer will want to kill you
• And your family
• And your pets
35. Consolidate
Let's throw all that duplicated code into an
include file, say library/base.php.
(We'll add a few other handy items while we're in there)
52. Besides getting shot . . .
“Whenever PHP generates an error message
internally, it's processed and formatted all the
way up to the fully formatted message that can
be outputted straight to the browser. Only just
before it is displayed the error_reporting setting
is checked.”
http://derickrethans.nl/five-reasons-why-the-shutop-operator-should-be-avoided.html
56. FIEO
• Filter input
– Your users want to destroy your app
– Prevent SQL injection
• Escape output
– Or just destroy your app yourself
– Defend against XSS
63. book-form.php: After
$statement = $dbh->prepare('SELECT title, author FROM
bookshelf WHERE id = :id');
$statement->bindParam(':id', $id);
$statement->execute();
$book = $statement->fetch();
64. book-form.php: After
$statement = $dbh->prepare('SELECT title, author FROM
bookshelf WHERE id = :id');
$statement->bindParam(':id', $id);
$statement->execute();
$book = $statement->fetch();
65. book-form.php: After
$statement = $dbh->prepare('SELECT title, author FROM
bookshelf WHERE id = :id');
$statement->bindParam(':id', $id);
$statement->execute();
$book = $statement->fetch();
66. book-form.php: After
$statement = $dbh->prepare('SELECT title, author FROM
bookshelf WHERE id = :id');
$statement->bindParam(':id', $id);
$statement->execute();
$book = $statement->fetch();
67. book-form.php: After
$statement = $dbh->prepare('SELECT title, author FROM
bookshelf WHERE id = :id');
$statement->bindParam(':id', $id);
$statement->execute();
$book = $statement->fetch();
68. process-book.php: Before
if (empty($_POST['id'])) {
$sql = "INSERT INTO bookshelf (title, author) "
."VALUES ('{$_POST['title']}',
'{$_POST['author']}')";
$dbh->exec($sql);
} else {
$sql = "UPDATE bookshelf SET title =
'{$_POST['title']}', "
. "author = '{$_POST['author']}' WHERE id =
{$_POST['id']}";
$dbh->exec($sql);
}
69. process-book.php: After
if ($id) {
$statement = $dbh->prepare("UPDATE bookshelf SET title
= :title, author = :author WHERE id = :id");
$statement->bindParam(':title', $title);
$statement->bindParam(':author', $author);
$statement->bindParam(':id', $id);
$statement->execute();
} else {
$statement = $dbh->prepare("INSERT INTO bookshelf
(title, author) VALUES (:title, :author)");
$statement->bindParam(':title', $title);
$statement->bindParam(':author', $author);
$statement->execute();
}
70. process-book.php: After
if ($id) {
$statement = $dbh->prepare("UPDATE bookshelf SET title
= :title, author = :author WHERE id = :id");
$statement->bindParam(':title', $title);
$statement->bindParam(':author', $author);
$statement->bindParam(':id', $id);
$statement->execute();
} else {
$statement = $dbh->prepare("INSERT INTO bookshelf
(title, author) VALUES (:title, :author)");
$statement->bindParam(':title', $title);
$statement->bindParam(':author', $author);
$statement->execute();
}
71. process-book.php: After
if ($id) {
$statement = $dbh->prepare("UPDATE bookshelf SET title
= :title, author = :author WHERE id = :id");
$statement->bindParam(':title', $title);
$statement->bindParam(':author', $author);
$statement->bindParam(':id', $id);
$statement->execute();
} else {
$statement = $dbh->prepare("INSERT INTO bookshelf
(title, author) VALUES (:title, :author)");
$statement->bindParam(':title', $title);
$statement->bindParam(':author', $author);
$statement->execute();
}
72. process-book.php: After
if ($id) {
$statement = $dbh->prepare("UPDATE bookshelf SET title
= :title, author = :author WHERE id = :id");
$statement->bindParam(':title', $title);
$statement->bindParam(':author', $author);
$statement->bindParam(':id', $id);
$statement->execute();
} else {
$statement = $dbh->prepare("INSERT INTO bookshelf
(title, author) VALUES (:title, :author)");
$statement->bindParam(':title', $title);
$statement->bindParam(':author', $author);
$statement->execute();
}
75. What? Where?
• Class name: BookshelfService
• Namespace: BookshelfService
• Location: library/Bookshelf/Service
• Filename: BookshelfService.php
• PSR-0 compliant
(The class name should mirror its location in the file
system)
78. BookshelfService.php
public function find($id)
{
$sql = 'SELECT * FROM bookshelf WHERE id = :id';
$statement = $this->dbh->prepare($sql);
$statement->bindParam(':id', $id);
$statement->execute();
return $statement->fetch();
}
public function findAll()
{
$sql = 'SELECT * FROM bookshelf ORDER BY title';
return $this->dbh->query($sql)->fetchAll();
}
79. BookshelfService.php
public function save(array $options)
{
if ($options['id']) {
$statement = $this->dbh->prepare("UPDATE bookshelf
SET title = :title, author = :author WHERE id = :id");
$statement->execute($options);
} else {
unset($options['id']);
$statement = $this->dbh->prepare("INSERT INTO
bookshelf (title, author) VALUES (:title, :author)");
$statement->execute($options);
}
}
80. A New Class Means . . .
. . . we need to add a few things to base.php.
91. What Have We Done?
• Discovered we've been handed a disaster
• Iteratively improved on the nightmare
• Seen some nice features of PHP
– PDO
– Filtering and escaping
– OOP
• Learned something?
92. What more could we do?
• Global config file
• Environment config file
• Unit and functional tests
• Don't roll your own!
• Add Composer, add libraries as needed