PHP 102
Out with the Bad,
In with the Good
php[tek] 2013
Who is this guy?
Jeremy Kendall
I love to code
I love to take pictures
I'm terribly forgetful
I work at OpenSky
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
Why Did You
Start Programming?
I wanted to solve problems, but . . .
. . . I frequently caused
as many problems
as I solved.
“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
“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
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
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.
What's the Problem?
• Crappy application
• CRUD bookshelf
• What does it do?:
– Display books
– Add books
– Edit books
– Delete books
What's There?
• Database (MySQL)
• View (index.php)
• Form (book-form.php)
• Form processor (process-form.php)
• Delete (delete-book.php)
index.php
book-form.php
index.php – db connection
$db = mysql_connect('localhost', 'testuser', 'testpass');
mysql_select_db('bookshelf', $db);
$query = "SELECT * FROM bookshelf ORDER BY title";
$result = mysql_query($query);
index.php – db connection
$db = mysql_connect('localhost', 'testuser', 'testpass');
mysql_select_db('bookshelf', $db);
$query = "SELECT * FROM bookshelf ORDER BY title";
$result = mysql_query($query);
index.php – db connection
$db = mysql_connect('localhost', 'testuser', 'testpass');
mysql_select_db('bookshelf', $db);
$query = "SELECT * FROM bookshelf ORDER BY title";
$result = mysql_query($query);
index.php – db connection
$db = mysql_connect('localhost', 'testuser', 'testpass');
mysql_select_db('bookshelf', $db);
$query = "SELECT * FROM bookshelf ORDER BY title";
$result = mysql_query($query);
index.php – books table
<?php while ($book = mysql_fetch_assoc($result)): ?>
<tr>
<td>
<a href="book-form.php?id=<?php echo $book['id']; ?>">
<?php echo $book['title']; ?>
</a>
</td>
<td>
<?php echo $book['author']; ?>
</td>
</tr>
<?php endwhile; ?>
index.php – books table
<?php while ($book = mysql_fetch_assoc($result)): ?>
<tr>
<td>
<a href="book-form.php?id=<?php echo $book['id']; ?>">
<?php echo $book['title']; ?>
</a>
</td>
<td>
<?php echo $book['author']; ?>
</td>
</tr>
<?php endwhile; ?>
book-form.php
// Database connection code
$id = empty($_GET['id']) ? null : $_GET['id'];
if ($id) {
// Database connection code
$query = "SELECT title, author "
. "FROM bookshelf WHERE id = $id";
$result = mysql_query($query);
$book = mysql_fetch_assoc($result);
$title = $book['title'];
$author = $book['author'];
}
book-form.php
$id = empty($_GET['id']) ? null : $_GET['id'];
if ($id) {
// Database connection code
$query = "SELECT title, author "
. "FROM bookshelf WHERE id = $id";
$result = mysql_query($query);
$book = mysql_fetch_assoc($result);
$title = $book['title'];
$author = $book['author'];
}
book-form.php
$id = empty($_GET['id']) ? null : $_GET['id'];
if ($id) {
// Database connection code
$query = "SELECT title, author "
. "FROM bookshelf WHERE id = $id";
$result = mysql_query($query);
$book = mysql_fetch_assoc($result);
$title = $book['title'];
$author = $book['author'];
}
book-form.php
<input type="hidden" name="id" value="<?php echo $id; ?>" />
<input type="text" name="title" value="<?php echo $title; ?>" />
<input type="text" name="author" value="<?php echo $author; ?>" />
book-form.php
<input type="hidden" name="id" value="<?php echo $id; ?>" />
<input type="text" name="title" value="<?php echo $title; ?>" />
<input type="text" name="author" value="<?php echo $author; ?>" />
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);
}
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);
}
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);
}
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);
}
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);
}
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?
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
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
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)
base.php
// . . . snip . . .
$db = mysql_connect('localhost', 'testuser', 'testpass');
mysql_select_db('bookshelf', $db);
Replace db info with base.php
Remove the db connection code from each
file with:
require_once dirname(__FILE__) . '/library/base.php';
STOP!
PDO
• PHP Data Objects
• Lightweight, consistent interface
• Data access abstraction
• Not database abstraction
http://www.php.net/manual/en/intro.pdo.php
base.php
// . . . snip . . .
$options = array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
);
$dsn =
'mysql:host=localhost;dbname=bookshelf;charset=utf8';
$username = 'testuser';
$password = 'testpass';
try {
$dbh = new PDO($dsn, $username, $password, $options);
} catch (PDOException $e) {
error_log($e->getMessage(), 3, '../logs/errors.log');
echo 'An error occurred while trying to connect to the
database.';
}
base.php
// . . . snip . . .
$options = array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
);
$dsn =
'mysql:host=localhost;dbname=bookshelf;charset=utf8';
$username = 'testuser';
$password = 'testpass';
try {
$dbh = new PDO($dsn, $username, $password, $options);
} catch (PDOException $e) {
error_log($e->getMessage(), 3, '../logs/errors.log');
echo 'An error occurred while trying to connect to the
database.';
}
base.php
// . . . snip . . .
$options = array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
);
$dsn =
'mysql:host=localhost;dbname=bookshelf;charset=utf8';
$username = 'testuser';
$password = 'testpass';
try {
$dbh = new PDO($dsn, $username, $password, $options);
} catch (PDOException $e) {
error_log($e->getMessage(), 3, '../logs/errors.log');
echo 'An error occurred while trying to connect to the
database.';
}
base.php
// . . . snip . . .
$options = array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
);
$dsn =
'mysql:host=localhost;dbname=bookshelf;charset=utf8';
$username = 'testuser';
$password = 'testpass';
try {
$dbh = new PDO($dsn, $username, $password, $options);
} catch (PDOException $e) {
error_log($e->getMessage(), 3, '../logs/errors.log');
echo 'An error occurred while trying to connect to the
database.';
}
Now start replacing mysql_*
// index.php
$books = $dbh->query($sql)->fetchAll();
// book-form.php
$book = $dbh->query($sql)->fetch();
// process-book.php and delete-book.php
$dbh->exec($sql);
Now on to the “handy items” I promised
base.php
$env = getenv('APPLICATION_ENVIRONMENT');
if (!$env) {
$env = "production";
}
date_default_timezone_set('America/Chicago');
if ($env === "develop") {
error_reporting(-1);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
}
// . . . snip . . .
base.php
$env = getenv('APPLICATION_ENVIRONMENT');
if (!$env) {
$env = "production";
}
date_default_timezone_set('America/Chicago');
if ($env === "develop") {
error_reporting(-1);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
}
// . . . snip . . .
base.php
$env = getenv('APPLICATION_ENVIRONMENT');
if (!$env) {
$env = "production";
}
date_default_timezone_set('America/Chicago');
if ($env === "develop") {
error_reporting(-1);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
}
// . . . snip . . .
base.php
$env = getenv('APPLICATION_ENVIRONMENT');
if (!$env) {
$env = "production";
}
date_default_timezone_set('America/Chicago');
if ($env === "develop") {
error_reporting(-1);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
}
// . . . snip . . .
Duplication removed, but . . .
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
We echo $title and $author
<input type="hidden" name="id" value="<?php echo $id; ?>" />
<input type="text" name="title" value="<?php echo $title; ?>" />
<input type="text" name="author" value="<?php echo $author; ?>" />
Without defining $title and $author
require_once dirname(__FILE__) . '/library/base.php';
$id = empty($_GET['id']) ? null : $_GET['id'];
if ($id) {
$book = $dbh->query( . . . )->fetch();
$title = $book['title'];
$author = $book['author'];
}
Super easy to fix
$id = empty($_GET['id']) ? null : $_GET['id'];
$title = null;
$author = null;
if ($id) {
$book = $dbh->query( . . . )->fetch();
$title = $book['title'];
$author = $book['author'];
}
FIEO
• Filter input
– Your users want to destroy your app
– Prevent SQL injection
• Escape output
– Or just destroy your app yourself
– Defend against XSS
http://xkcd.com/327/
book-form.php
Use filter_input() to guarantee $id is
either false or an int.
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
process-book.php
More filter_input()
$id = filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT);
$title = filter_input(INPUT_POST, 'title',
FILTER_SANITIZE_STRING);
$author = filter_input(INPUT_POST, 'author',
FILTER_SANITIZE_STRING);
index.php
Use htmlspecialchars() to escape
output
htmlspecialchars($book['title'], ENT_COMPAT, 'UTF-8'); ?>
htmlspecialchars($book['author'], ENT_COMPAT, 'UTF-8'); ?>
Prepared Statements
• Uses fewer resources
• Runs faster
• Protects against SQL injection
• Easier to read and maintain
book-form.php: Before
$book = $dbh->query("SELECT title, author FROM
bookshelf WHERE id = $id")->fetch();
book-form.php: After
$statement = $dbh->prepare('SELECT title, author FROM
bookshelf WHERE id = :id');
$statement->bindParam(':id', $id);
$statement->execute();
$book = $statement->fetch();
book-form.php: After
$statement = $dbh->prepare('SELECT title, author FROM
bookshelf WHERE id = :id');
$statement->bindParam(':id', $id);
$statement->execute();
$book = $statement->fetch();
book-form.php: After
$statement = $dbh->prepare('SELECT title, author FROM
bookshelf WHERE id = :id');
$statement->bindParam(':id', $id);
$statement->execute();
$book = $statement->fetch();
book-form.php: After
$statement = $dbh->prepare('SELECT title, author FROM
bookshelf WHERE id = :id');
$statement->bindParam(':id', $id);
$statement->execute();
$book = $statement->fetch();
book-form.php: After
$statement = $dbh->prepare('SELECT title, author FROM
bookshelf WHERE id = :id');
$statement->bindParam(':id', $id);
$statement->execute();
$book = $statement->fetch();
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);
}
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();
}
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();
}
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();
}
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();
}
Service Layer
Let's replace all that baked in DB stuff
with a Service Layer
Why?
• Introduce some OOP principles
• High ROI
– Maintainability
– Testability
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)
BookshelfService.php
namespace BookshelfService;
class BookshelfService
{
private $dbh;
public function __construct(PDO $dbh) {}
public function find($id) {}
public function findAll() {}
public function save(array $options) {}
public function delete($id) {}
}
BookshelfService.php
private $dbh;
public function __construct(PDO $dbh)
{
$this->dbh = $dbh;
}
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();
}
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);
}
}
A New Class Means . . .
. . . we need to add a few things to base.php.
base.php
set_include_path(
implode(PATH_SEPARATOR, array(
get_include_path(),
dirname(__FILE__)
)
)
);
base.php
function autoload($className)
{
// PSR-0 autoloader
}
spl_autoload_register('autoload');
https://gist.github.com/jwage/221634
base.php
// database connection code
$bookshelf = new BookshelfServiceBookshelfService($dbh);
And now for some before and after
shots . . .
index.php: Before
require_once dirname(__FILE__) . '/library/base.php';
$books = $dbh->query("SELECT * FROM bookshelf ORDER BY
title")->fetchAll();
index.php: After
require_once dirname(__FILE__) . '/library/base.php';
$books = $bookshelf->findAll();
book-form.php: Before
if ($id) {
$statement = $dbh->prepare('SELECT title, author FROM
bookshelf WHERE id = :id');
$statement->bindParam(':id', $id);
$statement->execute();
$book = $statement->fetch();
$title = $book['title'];
$author = $book['author'];
}
book-form.php: After
if ($id) {
$book = $bookshelf->find($id);
$title = $book['title'];
$author = $book['author'];
}
process-book.php: Before
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();
}
process-book.php: After
$book = array(
'id' => $id,
'title' => $title,
'author' => $author
);
$bookshelf->save($book);
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?
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
Want More?
• Presentation source code: https://github.com/jeremykendall/bookshelf
• PHP Manual: http://www.php.net/manual/en/index.php
• PHP The Right Way: http://www.phptherightway.com/
• PHP 101 Suggestions:
http://csiphp.com/blog/2011/07/19/stop-doing-it-wrong-and-learn-to-code-good-too/
• PHP 101: PHP for the Absolute Beginner:
http://devzone.zend.com/6/php-101-php-for-the-absolute-beginner/
• PHPDeveloper.org http://phpdeveloper.org/
• php|architect: http://www.phparch.com/
• Composer http://getcomposer.org
Questions?
Thanks!
@JeremyKendall
jeremy@jeremykendall.net
http://joind.in/8188

Php 102: Out with the Bad, In with the Good

  • 1.
    PHP 102 Out withthe Bad, In with the Good php[tek] 2013
  • 2.
    Who is thisguy? Jeremy Kendall I love to code I love to take pictures I'm terribly forgetful I work at OpenSky
  • 3.
    Following Along You canfollow along with the presentation code at github.com. https://github.com/jeremykendall/bookshelf Before = oh-the-horror After = much-better
  • 4.
    Why Did You StartProgramming?
  • 5.
    I wanted tosolve problems, but . . .
  • 6.
    . . .I frequently caused as many problems as I solved.
  • 7.
    “Always code asif 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 codeand 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 aProblem 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'veseen 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)
  • 13.
  • 14.
  • 15.
    index.php – dbconnection $db = mysql_connect('localhost', 'testuser', 'testpass'); mysql_select_db('bookshelf', $db); $query = "SELECT * FROM bookshelf ORDER BY title"; $result = mysql_query($query);
  • 16.
    index.php – dbconnection $db = mysql_connect('localhost', 'testuser', 'testpass'); mysql_select_db('bookshelf', $db); $query = "SELECT * FROM bookshelf ORDER BY title"; $result = mysql_query($query);
  • 17.
    index.php – dbconnection $db = mysql_connect('localhost', 'testuser', 'testpass'); mysql_select_db('bookshelf', $db); $query = "SELECT * FROM bookshelf ORDER BY title"; $result = mysql_query($query);
  • 18.
    index.php – dbconnection $db = mysql_connect('localhost', 'testuser', 'testpass'); mysql_select_db('bookshelf', $db); $query = "SELECT * FROM bookshelf ORDER BY title"; $result = mysql_query($query);
  • 19.
    index.php – bookstable <?php while ($book = mysql_fetch_assoc($result)): ?> <tr> <td> <a href="book-form.php?id=<?php echo $book['id']; ?>"> <?php echo $book['title']; ?> </a> </td> <td> <?php echo $book['author']; ?> </td> </tr> <?php endwhile; ?>
  • 20.
    index.php – bookstable <?php while ($book = mysql_fetch_assoc($result)): ?> <tr> <td> <a href="book-form.php?id=<?php echo $book['id']; ?>"> <?php echo $book['title']; ?> </a> </td> <td> <?php echo $book['author']; ?> </td> </tr> <?php endwhile; ?>
  • 21.
    book-form.php // Database connectioncode $id = empty($_GET['id']) ? null : $_GET['id']; if ($id) { // Database connection code $query = "SELECT title, author " . "FROM bookshelf WHERE id = $id"; $result = mysql_query($query); $book = mysql_fetch_assoc($result); $title = $book['title']; $author = $book['author']; }
  • 22.
    book-form.php $id = empty($_GET['id'])? null : $_GET['id']; if ($id) { // Database connection code $query = "SELECT title, author " . "FROM bookshelf WHERE id = $id"; $result = mysql_query($query); $book = mysql_fetch_assoc($result); $title = $book['title']; $author = $book['author']; }
  • 23.
    book-form.php $id = empty($_GET['id'])? null : $_GET['id']; if ($id) { // Database connection code $query = "SELECT title, author " . "FROM bookshelf WHERE id = $id"; $result = mysql_query($query); $book = mysql_fetch_assoc($result); $title = $book['title']; $author = $book['author']; }
  • 24.
    book-form.php <input type="hidden" name="id"value="<?php echo $id; ?>" /> <input type="text" name="title" value="<?php echo $title; ?>" /> <input type="text" name="author" value="<?php echo $author; ?>" />
  • 25.
    book-form.php <input type="hidden" name="id"value="<?php echo $id; ?>" /> <input type="text" name="title" value="<?php echo $title; ?>" /> <input type="text" name="author" value="<?php echo $author; ?>" />
  • 26.
    process-book.php // Database connectioncode $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 connectioncode $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 connectioncode $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 connectioncode $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 connectioncode $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); }
  • 32.
    Glaring Problems? • Codeduplication • 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 extensiondeprecated 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 allthat duplicated code into an include file, say library/base.php. (We'll add a few other handy items while we're in there)
  • 36.
    base.php // . .. snip . . . $db = mysql_connect('localhost', 'testuser', 'testpass'); mysql_select_db('bookshelf', $db);
  • 37.
    Replace db infowith base.php Remove the db connection code from each file with: require_once dirname(__FILE__) . '/library/base.php';
  • 38.
  • 39.
    PDO • PHP DataObjects • Lightweight, consistent interface • Data access abstraction • Not database abstraction http://www.php.net/manual/en/intro.pdo.php
  • 40.
    base.php // . .. snip . . . $options = array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC ); $dsn = 'mysql:host=localhost;dbname=bookshelf;charset=utf8'; $username = 'testuser'; $password = 'testpass'; try { $dbh = new PDO($dsn, $username, $password, $options); } catch (PDOException $e) { error_log($e->getMessage(), 3, '../logs/errors.log'); echo 'An error occurred while trying to connect to the database.'; }
  • 41.
    base.php // . .. snip . . . $options = array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC ); $dsn = 'mysql:host=localhost;dbname=bookshelf;charset=utf8'; $username = 'testuser'; $password = 'testpass'; try { $dbh = new PDO($dsn, $username, $password, $options); } catch (PDOException $e) { error_log($e->getMessage(), 3, '../logs/errors.log'); echo 'An error occurred while trying to connect to the database.'; }
  • 42.
    base.php // . .. snip . . . $options = array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC ); $dsn = 'mysql:host=localhost;dbname=bookshelf;charset=utf8'; $username = 'testuser'; $password = 'testpass'; try { $dbh = new PDO($dsn, $username, $password, $options); } catch (PDOException $e) { error_log($e->getMessage(), 3, '../logs/errors.log'); echo 'An error occurred while trying to connect to the database.'; }
  • 43.
    base.php // . .. snip . . . $options = array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC ); $dsn = 'mysql:host=localhost;dbname=bookshelf;charset=utf8'; $username = 'testuser'; $password = 'testpass'; try { $dbh = new PDO($dsn, $username, $password, $options); } catch (PDOException $e) { error_log($e->getMessage(), 3, '../logs/errors.log'); echo 'An error occurred while trying to connect to the database.'; }
  • 44.
    Now start replacingmysql_* // index.php $books = $dbh->query($sql)->fetchAll(); // book-form.php $book = $dbh->query($sql)->fetch(); // process-book.php and delete-book.php $dbh->exec($sql);
  • 45.
    Now on tothe “handy items” I promised
  • 46.
    base.php $env = getenv('APPLICATION_ENVIRONMENT'); if(!$env) { $env = "production"; } date_default_timezone_set('America/Chicago'); if ($env === "develop") { error_reporting(-1); ini_set('display_errors', 1); ini_set('display_startup_errors', 1); } // . . . snip . . .
  • 47.
    base.php $env = getenv('APPLICATION_ENVIRONMENT'); if(!$env) { $env = "production"; } date_default_timezone_set('America/Chicago'); if ($env === "develop") { error_reporting(-1); ini_set('display_errors', 1); ini_set('display_startup_errors', 1); } // . . . snip . . .
  • 48.
    base.php $env = getenv('APPLICATION_ENVIRONMENT'); if(!$env) { $env = "production"; } date_default_timezone_set('America/Chicago'); if ($env === "develop") { error_reporting(-1); ini_set('display_errors', 1); ini_set('display_startup_errors', 1); } // . . . snip . . .
  • 49.
    base.php $env = getenv('APPLICATION_ENVIRONMENT'); if(!$env) { $env = "production"; } date_default_timezone_set('America/Chicago'); if ($env === "develop") { error_reporting(-1); ini_set('display_errors', 1); ini_set('display_startup_errors', 1); } // . . . snip . . .
  • 50.
  • 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
  • 53.
    We echo $titleand $author <input type="hidden" name="id" value="<?php echo $id; ?>" /> <input type="text" name="title" value="<?php echo $title; ?>" /> <input type="text" name="author" value="<?php echo $author; ?>" />
  • 54.
    Without defining $titleand $author require_once dirname(__FILE__) . '/library/base.php'; $id = empty($_GET['id']) ? null : $_GET['id']; if ($id) { $book = $dbh->query( . . . )->fetch(); $title = $book['title']; $author = $book['author']; }
  • 55.
    Super easy tofix $id = empty($_GET['id']) ? null : $_GET['id']; $title = null; $author = null; if ($id) { $book = $dbh->query( . . . )->fetch(); $title = $book['title']; $author = $book['author']; }
  • 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
  • 57.
  • 58.
    book-form.php Use filter_input() toguarantee $id is either false or an int. $id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
  • 59.
    process-book.php More filter_input() $id =filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT); $title = filter_input(INPUT_POST, 'title', FILTER_SANITIZE_STRING); $author = filter_input(INPUT_POST, 'author', FILTER_SANITIZE_STRING);
  • 60.
    index.php Use htmlspecialchars() toescape output htmlspecialchars($book['title'], ENT_COMPAT, 'UTF-8'); ?> htmlspecialchars($book['author'], ENT_COMPAT, 'UTF-8'); ?>
  • 61.
    Prepared Statements • Usesfewer resources • Runs faster • Protects against SQL injection • Easier to read and maintain
  • 62.
    book-form.php: Before $book =$dbh->query("SELECT title, author FROM bookshelf WHERE id = $id")->fetch();
  • 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(); }
  • 73.
    Service Layer Let's replaceall that baked in DB stuff with a Service Layer
  • 74.
    Why? • Introduce someOOP principles • High ROI – Maintainability – Testability
  • 75.
    What? Where? • Classname: BookshelfService • Namespace: BookshelfService • Location: library/Bookshelf/Service • Filename: BookshelfService.php • PSR-0 compliant (The class name should mirror its location in the file system)
  • 76.
    BookshelfService.php namespace BookshelfService; class BookshelfService { private$dbh; public function __construct(PDO $dbh) {} public function find($id) {} public function findAll() {} public function save(array $options) {} public function delete($id) {} }
  • 77.
    BookshelfService.php private $dbh; public function__construct(PDO $dbh) { $this->dbh = $dbh; }
  • 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 ClassMeans . . . . . . we need to add a few things to base.php.
  • 81.
  • 82.
    base.php function autoload($className) { // PSR-0autoloader } spl_autoload_register('autoload'); https://gist.github.com/jwage/221634
  • 83.
    base.php // database connectioncode $bookshelf = new BookshelfServiceBookshelfService($dbh);
  • 84.
    And now forsome before and after shots . . .
  • 85.
    index.php: Before require_once dirname(__FILE__). '/library/base.php'; $books = $dbh->query("SELECT * FROM bookshelf ORDER BY title")->fetchAll();
  • 86.
    index.php: After require_once dirname(__FILE__). '/library/base.php'; $books = $bookshelf->findAll();
  • 87.
    book-form.php: Before if ($id){ $statement = $dbh->prepare('SELECT title, author FROM bookshelf WHERE id = :id'); $statement->bindParam(':id', $id); $statement->execute(); $book = $statement->fetch(); $title = $book['title']; $author = $book['author']; }
  • 88.
    book-form.php: After if ($id){ $book = $bookshelf->find($id); $title = $book['title']; $author = $book['author']; }
  • 89.
    process-book.php: Before 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(); }
  • 90.
    process-book.php: After $book =array( 'id' => $id, 'title' => $title, 'author' => $author ); $bookshelf->save($book);
  • 91.
    What Have WeDone? • 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 couldwe do? • Global config file • Environment config file • Unit and functional tests • Don't roll your own! • Add Composer, add libraries as needed
  • 93.
    Want More? • Presentationsource code: https://github.com/jeremykendall/bookshelf • PHP Manual: http://www.php.net/manual/en/index.php • PHP The Right Way: http://www.phptherightway.com/ • PHP 101 Suggestions: http://csiphp.com/blog/2011/07/19/stop-doing-it-wrong-and-learn-to-code-good-too/ • PHP 101: PHP for the Absolute Beginner: http://devzone.zend.com/6/php-101-php-for-the-absolute-beginner/ • PHPDeveloper.org http://phpdeveloper.org/ • php|architect: http://www.phparch.com/ • Composer http://getcomposer.org
  • 94.
  • 95.