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

Embed Size (px)

DESCRIPTION

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.

Citation preview

  • 1. PHP 102Out with the Bad,In with the Goodphp[tek] 2013

2. Who is this guy?Jeremy KendallI love to codeI love to take picturesIm terribly forgetfulI work at OpenSky 3. Following AlongYou can follow along with the presentationcode at github.com.https://github.com/jeremykendall/bookshelfBefore = oh-the-horrorAfter = much-better 4. Why Did YouStart Programming? 5. I wanted to solve problems, but . . . 6. . . . I frequently causedas many problemsas I solved. 7. Always code as if the person who ends upmaintaining your code is a violent psychopathwho knows where you live.http://c2.com/cgi/wiki?CodeForTheMaintainer 8. Alternatively, always code and comment in sucha way that if someone a few notches junior picksup the code, they will take pleasure in readingand learning from it.http://c2.com/cgi/wiki?CodeForTheMaintainer 9. Lets Solve a Problem Together Well review a typical PHP application Find horrible mistakes Correct those mistakes Make a cool improvement Learn and apply better practices 10. Typical application? Ive seen code similar to the samples Ill showin almost every app Ive ever touched. Ive made the same mistakes in almost everyapp I wrote in my early days. Im guessing youve seen and made similarmistakes too. 11. Whats the Problem? Crappy application CRUD bookshelf What does it do?: Display books Add books Edit books Delete books 12. Whats There? Database (MySQL) View (index.php) Form (book-form.php) Form processor (process-form.php) Delete (delete-book.php) 13. index.php 14. book-form.php 15. 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); 16. 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); 17. 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); 18. 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); 19. index.php books table 20. index.php books table 21. 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];} 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 25. book-form.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. Glaring Problems? Code duplication Input isnt filtered Output isnt escaped Vendor specific functions User-provided data used in SQL Did you see any others? 32. Deprecated! mysql extension deprecated as of 5.5.0 Use mysqli or PDO Read the MySQL Choosing an API onphp.net: http://bit.ly/13zur2e 33. 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 34. ConsolidateLets throw all that duplicated code into aninclude file, say library/base.php.(Well add a few other handy items while were in there) 35. base.php// . . . snip . . .$db = mysql_connect(localhost, testuser, testpass);mysql_select_db(bookshelf, $db); 36. Replace db info with base.phpRemove the db connection code from eachfile with:require_once dirname(__FILE__) . /library/base.php; 37. STOP! 38. PDO PHP Data Objects Lightweight, consistent interface Data access abstraction Not database abstractionhttp://www.php.net/manual/en/intro.pdo.php 39. 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 thedatabase.;} 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 thedatabase.;} 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 thedatabase.;} 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 thedatabase.;} 43. 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); 44. Now on to the handy items I promised 45. 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 . . . 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. Duplication removed, but . . . 50. Besides getting shot . . .Whenever PHP generates an error messageinternally, its processed and formatted all theway up to the fully formatted message that canbe outputted straight to the browser. Only justbefore it is displayed the error_reporting settingis checked.http://derickrethans.nl/five-reasons-why-the-shutop-operator-should-be-avoided.html 51. We echo $title and $author 52. Without defining $title and $authorrequire_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];} 53. 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];} 54. FIEO Filter input Your users want to destroy your app Prevent SQL injection Escape output Or just destroy your app yourself Defend against XSS 55. http://xkcd.com/327/ 56. book-form.phpUse filter_input() to guarantee $id iseither false or an int.$id = filter_input(INPUT_GET, id, FILTER_VALIDATE_INT); 57. process-book.phpMore 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); 58. index.phpUse htmlspecialchars() to escapeoutputhtmlspecialchars($book[title], ENT_COMPAT, UTF-8); ?>htmlspecialchars($book[author], ENT_COMPAT, UTF-8); ?> 59. Prepared Statements Uses fewer resources Runs faster Protects against SQL injection Easier to read and maintain 60. book-form.php: Before$book = $dbh->query("SELECT title, author FROMbookshelf WHERE id = $id")->fetch(); 61. book-form.php: After$statement = $dbh->prepare(SELECT title, author FROMbookshelf WHERE id = :id);$statement->bindParam(:id, $id);$statement->execute();$book = $statement->fetch(); 62. book-form.php: After$statement = $dbh->prepare(SELECT title, author FROMbookshelf WHERE id = :id);$statement->bindParam(:id, $id);$statement->execute();$book = $statement->fetch(); 63. book-form.php: After$statement = $dbh->prepare(SELECT title, author FROMbookshelf WHERE id = :id);$statement->bindParam(:id, $id);$statement->execute();$book = $statement->fetch(); 64. book-form.php: After$statement = $dbh->prepare(SELECT title, author FROMbookshelf WHERE id = :id);$statement->bindParam(:id, $id);$statement->execute();$book = $statement->fetch(); 65. book-form.php: After$statement = $dbh->prepare(SELECT title, author FROMbookshelf WHERE id = :id);$statement->bindParam(:id, $id);$statement->execute();$book = $statement->fetch(); 66. process-book.php: Beforeif (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);} 67. process-book.php: Afterif ($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();} 68. process-book.php: Afterif ($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();} 69. process-book.php: Afterif ($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: Afterif ($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. Service LayerLets replace all that baked in DB stuffwith a Service Layer 72. Why? Introduce some OOP principles High ROI Maintainability Testability 73. 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 filesystem) 74. BookshelfService.phpnamespace 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) {}} 75. BookshelfService.phpprivate $dbh;public function __construct(PDO $dbh){$this->dbh = $dbh;} 76. BookshelfService.phppublic 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();} 77. BookshelfService.phppublic function save(array $options){if ($options[id]) {$statement = $this->dbh->prepare("UPDATE bookshelfSET title = :title, author = :author WHERE id = :id");$statement->execute($options);} else {unset($options[id]);$statement = $this->dbh->prepare("INSERT INTObookshelf (title, author) VALUES (:title, :author)");$statement->execute($options);}} 78. A New Class Means . . .. . . we need to add a few things to base.php. 79. base.phpset_include_path(implode(PATH_SEPARATOR, array(get_include_path(),dirname(__FILE__)))); 80. base.phpfunction autoload($className){// PSR-0 autoloader}spl_autoload_register(autoload);https://gist.github.com/jwage/221634 81. base.php// database connection code$bookshelf = new BookshelfServiceBookshelfService($dbh); 82. And now for some before and aftershots . . . 83. index.php: Beforerequire_once dirname(__FILE__) . /library/base.php;$books = $dbh->query("SELECT * FROM bookshelf ORDER BYtitle")->fetchAll(); 84. index.php: Afterrequire_once dirname(__FILE__) . /library/base.php;$books = $bookshelf->findAll(); 85. book-form.php: Beforeif ($id) {$statement = $dbh->prepare(SELECT title, author FROMbookshelf WHERE id = :id);$statement->bindParam(:id, $id);$statement->execute();$book = $statement->fetch();$title = $book[title];$author = $book[author];} 86. book-form.php: Afterif ($id) {$book = $bookshelf->find($id);$title = $book[title];$author = $book[author];} 87. process-book.php: Beforeif ($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();} 88. process-book.php: After$book = array(id => $id,title => $title,author => $author);$bookshelf->save($book); 89. What Have We Done? Discovered weve been handed a disaster Iteratively improved on the nightmare Seen some nice features of PHP PDO Filtering and escaping OOP Learned something? 90. What more could we do? Global config file Environment config file Unit and functional tests Dont roll your own! Add Composer, add libraries as needed 91. 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 92. Questions? 93. Thanks!@[email protected]://joind.in/8188