├── .gitignore ├── README.md └── pages └── original.html /.gitignore: -------------------------------------------------------------------------------- 1 | ; Ignore IDE files 2 | nbproject 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tutorial text for "I ♥ PHP" 2 | === 3 | 4 | This repo contains the text for the [I ♥ PHP](http://ilovephp.jondh.me.uk) tutorial. 5 | The tutorial has been designed for beginner and improving-beginner programmers wishing to learn 6 | some good practices in PHP web development. The code example uses PDO/SQLite, and demonstrates 7 | parameterisation, HTML escaping, logic/content separation, authentication, form handling, sessions 8 | and proper password hashing. The repo is public so that experienced developers may propose 9 | improvements if they wish. 10 | 11 | Code changes in the tutorial are shown as diffs in the text, and each modified file can be 12 | downloaded in its entirety at that point of development. To facilitate this, files and diffs are 13 | extracted from this repo by a script, rather than being copied in manually. This makes 14 | code improvements much easier to transpose to the tutorial than the traditional method of 15 | making adjustments by hand. 16 | 17 | Here is [a blog post about the tutorial](http://blog.jondh.me.uk/2014/08/online-php-beginners-tutorial/) 18 | that was written prior to, and at the time of, going live. Since then, some readers have made 19 | suggestions, tickets have been raised, and improvements have been committed and published. 20 | 21 | See also the [code repo here](https://github.com/halfer/php-tutorial-project). 22 | 23 | Page build process 24 | --- 25 | 26 | The pages are held in a single file, using `__NEWFILE__` as a chapter separator. Menu titles are 27 | derived from the first `

` in each section, and small amounts of PHP are permitted. There are two 28 | main uses for PHP in this repo: to render diffs and conditionally render non-live notes. Here's 29 | how to render a tabbed (multi-file) diff block: 30 | 31 | 32 | 33 | Comments are used here, as hashes will break if changes are rebased in. In the unlikely event the 34 | repo contains clashing comment texts, a simple date filter can be added. 35 | 36 | Here's how to render a development mode note (the permitted note classes are `critical`, `todo`, 37 | `in-progress` and `comment`): 38 | 39 | 40 |
41 | Here's a note 42 |
43 | 44 | 45 | Once merged to this repo, pages are rebuilt manually using a script. The file `project-branch` 46 | specifies the branch of the code repo to use for the diffs; this permits different versions of the 47 | chapter text to share a code repo, or to have their own, as the author decides. 48 | 49 | The ability to maintain different versions of the text and code will be reflected in the tutorial, 50 | so users who are currently working through the tutorial may carry on with an old version, rather 51 | than being forced to start again. 52 | 53 | Notes: 54 | 55 | - The single file format is fine for the moment, but if someone wants to see this split into 56 | separate files, that can be arranged 57 | - A good feedback loop for the text probably won't be initiated simply by making these repos public. 58 | I suspect good commenting tools on the site itself will also be necessary 59 | -------------------------------------------------------------------------------- /pages/original.html: -------------------------------------------------------------------------------- 1 |

Introduction

2 | 3 | 4 |
5 | Comments, to-do items and critical notes appear in this sidebar, in test environments 6 | only. These help collate and organise feedback. 7 |
8 | 9 | 10 |

11 | In this tutorial, I'll present how to write a simple blog system, using the PHP language and 12 | the Apache web server. For the database, we'll use SQLite, to keep things simple. Don't worry 13 | if these terms don't mean anything yet, that's okay. I'm assuming readers are familiar 14 | with how to use their computer, but perhaps have not programmed before. That said, even a 15 | beginner's tutorial can get non-trivial quickly, so if you are brand new to programming, you 16 | may wish to research unfamiliar keywords (usually on the PHP website). 17 |

18 | 19 |

20 | The application we will build together was built prior to writing the tutorial. The text is 21 | therefore written around the way I think development happens in real life; some steps may 22 | introduce bugs, or be written in ways the purist might consider non-ideal. However, for the most 23 | part, these problems will be ironed out as we go. At the end of this project, we'll have a 24 | functional, maintainable and secure application, but there'll be much 25 | that can still be improved on (which will likely make for a subsequent tutorial). 26 |

27 | 28 | 29 |
30 | The site has a working title of 'I ♥ PHP' (or "ilovephp" in URLs). 31 | I did wonder if 'I love code' would be better, in case other languages might be added in 32 | the future, but I think that might be too ambitious! 33 |
34 | 35 | 36 |

37 | If you are new to programming, it is possible that you might feel somewhat lost as you work 38 | through the material. Whilst any prior practice will always help, the before-and-after code 39 | samples should see you right through to the end, even if you don't initially understand every 40 | step. You should find that some of this will sink in, and will contribute to your future "aha" 41 | moment, even if it is daunting at the start. The elation of getting it working will, I hope, 42 | encourage you to dive even deeper into programming. Enjoy the ride! 43 |

44 | 45 | 69 | 70 |

71 | I'll assume that your development environment is set up. Here's what you will need: 72 |

73 | 74 | 79 | 80 | 81 |
82 | I've swapped out references to MAMP and WAMP, as MAMP is quite restricted in the free 83 | version (only one vhost) and it's nice to have one product for all systems. 84 |
85 | 86 | 87 |

88 | For Windows and Mac users, you can get all of this with 89 | XAMPP. If you use Linux, you may have all of this 90 | already — although XAMPP works on Linux too, and adds a user-friendly control panel. 91 | If you get stuck, turn to your favourite search engine, and 92 | ask "How do I install [whatever] on [Windows]". In 99% of cases, your question has already 93 | been answered, so be persistent. 94 |

95 | 96 | 97 |
98 | It might be nice to recommend something lightweight too, in addition to NB. 99 | Brackets and 100 | Light Table are both free, in frequent development 101 | and x-platform - add one of these? 102 |
103 | 104 | 105 |

106 | You'll also need an editor to modify files. Most computers have a free text editor already 107 | installed, but these are rarely good enough. Use whatever you like, but if you have no 108 | preference, download and install NetBeans. 109 |

110 | 111 |
~
112 | 113 |

114 | In designing this tutorial, I wrote out a list of things I thought would be great to have, and 115 | then developed until I had a basic complete product. In retrospect, it would have been better to 116 | decide this prior to development; regardless of whether you are setting up a business or programming 117 | for fun, having a simple and achievable plan is miles better than an all-singing, 118 | all-dancing one that ends up abandoned half-way through (either through boredom, bankruptcy, or 119 | both). 120 |

121 | 122 |

Here's the initial list of features/technologies I put on paper:

123 | 124 |
125 |

126 | Login, Logout, Add comment, Create user, Amend user, Delete user, Email validation for new user 127 | accounts, Display post, Accept markdown format, Create post, Edit post, Unpublish post, Delete post, 128 | Unit tests, List posts, AJAX posting/commenting, Database profiler, Internationalisation, 129 | Pretty URLs, Use template engine, Per-environment configuration files, Post pagination, 130 | Comment pagination, Unpublish comment, Delete comment, Foreign key constraints, Unique constraints, 131 | User access levels. 132 |

133 |
134 | 135 |

136 | To illustrate how much an "initial working product" should trim the initial wishlist, here's 137 | what made it into the first version: 138 |

139 | 140 |
141 |

142 | Login, Logout, Add comment, Display post, Create post, Edit post, Delete post, List posts, 143 | Delete comment, Foreign key constraints. 144 |

145 |
146 | 147 |

148 | It's worth pointing out that, with any software project, rejecting a feature now does 149 | not mean you can never add that feature. It just means that you are prioritising getting 150 | important things done, so that the product is in a releasable state quickly. Once it is out the 151 | door, you can then find a few more items on your list to add into the next iteration. In a 152 | commercial situation, this approach is helpful because if your users don't like a change you've 153 | made, you've wasted two months rather than two years — a saving that could be the difference 154 | between steering things back on track, and going bust. 155 |

156 | 157 |

158 | One of the other features of this tutorial is that it uses an absolute minimum of additional 159 | software to get it working. Software projects nearly always make use of libraries, 160 | each of which offer some particular pre-written functionality. These are well worth using in 161 | proper development, since they are better-tested by more people than an individual can usually 162 | achieve on their own. However, for a tutorial I think they can add integration complexity, and 163 | explaining the advanced-level code within is usually quite a distraction. Thus, I have eschewed 164 | libraries more than I would normally, and suspect this will be corrected in a second (more 165 | advanced) tutorial. 166 |

167 |

168 | I have made one exception for this rule, however, for reasons of password security. The login 169 | system requires some cryptographic code, and this sort of thing is quite easy to get wrong. Whilst 170 | this is just a tutorial, if it teaches readers to leave security to the experts (the 171 | library-writers), it has served a valuable purpose. 172 |

173 | 174 |
~
175 | 176 |

177 | So, let us get started. I'll assume you know how to create, edit, save and delete files in your 178 | editor, and that you can create folders as required. This might be in the root of your "htdocs" 179 | or "www" folder, depending on what you used to install Apache. You can use a subfolder of "blog" 180 | if you wish, or for more advanced users, feel free to set up a new vhost. So, when I use 181 | http://localhost/, you can assume I am referring to the root of your project, even 182 | if your URL is somewhat different. 183 |

184 | 185 |

186 | Let us first check that your server installation is working okay. Create a file called 187 | info.php in the root of your project folder: 188 |

189 | 190 | 191 | 192 |

193 | Then, run this in your browser, by accessing the URL http://localhost/info.php. 194 | You should see a PHP configuration page. If you do, then check you are running a version later 195 | than 5.4; if it is earlier than this then 196 | you will need to upgrade. If you don't see this screen at all, then check that you have created 197 | the file in the right folder. 198 |

199 | 200 |

201 | Once you have this working, and showing an acceptable version number, you can delete this file. 202 |

203 | 204 |

Here's the first bit of code to add:

205 | 206 |
207 | 208 |
209 | 210 |

211 | This is the initial file, so all we have to do is copy and paste it in. To do so, create 212 | index.php in the root of your project folder, open it in your 213 | editor, and ensure it is empty before pasting in the code. Then visit 214 | http://localhost/index.php in your browser, to ensure that you can see the 215 | initial mock-up of our blog's home page. 216 |

217 | 218 |

219 | If you can, then your first piece of coding is done! The language is HTML, which is understood 220 | by your web browser as a way to describe documents on the web. HTML is not actually a 221 | programming language: it is properly referred to as a markup language. The markup itself 222 | is made up of tags such as <body> and 223 | <h1>, each of which has an opening tag (without the slash) 224 | and a closing tag (with the slash). 225 | 226 | These 227 | are nested to form a hierarchy in the same way as an ordinary letter is hierarchical: instead of 228 | an envelope containing a document containing headings and paragraphs, we have an 229 | <html> containing a <body> containing 230 | <h1> and <p>. Easy peasy! 231 |

232 | 233 | 253 | 254 |

255 | Let us start improving our skeleton home page. One of the observations we can make is that there 256 | is a fair bit of repetition: there are two articles that both have a heading, date, summary 257 | and hyperlink. Since this is just test data, let us add a loop around the first article block 258 | and delete the second one. 259 |

260 | 261 |

262 | Here is how I am presenting the changes in this tutorial. This will be good practice for you, 263 | since all changes will be shown in this way. The approach of showing code changes like this is 264 | known as a diff, for obvious reasons (though the diffs I use are more colourful than 265 | the usual monochrome versions). 266 |

267 | 268 | 269 | 270 |

271 | The rules for making these changes are simple: the code on the left shows the old file, and the 272 | code on the right shows the new one. Also, red lines are deleted, green lines are added. So, 273 | the two red blocks on the left are in the existing file, and can be removed. The green block 274 | on the right should be added. Where you see a solid horizontal bar across the whole diff, 275 | that's just a sign that some identical lines have been missed out for brevity. 276 |

277 |

278 | Thus, if you wish, you can delete both article blocks and replace them with the new loop block 279 | (containing the for() through to the endfor). However, the way this was 280 | actually written was as follows: 281 |

282 | 283 | 293 | 294 |

295 | In summary, it doesn't matter too much how you apply these changes (or, indeed, any changes in 296 | this tutorial). That said, understanding the way in which changes are written in real 297 | life may help towards a greater understanding of the code. Don't worry though if you think you've 298 | made a mistake; the full file for each change is available above each one. 299 |

300 | 301 | 313 | 314 |

315 | So, finally, refresh your browser screen, and make sure your changes work. You should now see 316 | three articles, each with a different heading and paragraph. 317 |

318 | 319 |
~
320 | 321 |

322 | The next step we will take is to set up the database. We'll use a database system called 323 | SQLite (pronounced "see-kwel light") as it is really easy to connect to, and does not 324 | require a database server. Create the following two files in your project, paste in the contents, 325 | and then we'll discuss what they do. 326 |

327 | 328 |

329 | You'll notice that some changes in this tutorial — including one of these files, 330 | data/init.sql — has a name that contains a forward slash. 331 | This means that the file is stored in a sub-directory, and that you'll need to create that 332 | manually if it does not already exist. 333 |

334 | 335 | 336 | 337 |

338 | This change comprises of two new files, a SQL file and a PHP file. I'll discuss the SQL file 339 | first. 340 |

341 | 342 | 353 | 354 |

355 | Broadly, databases are stored in terms of tables, columns and rows. Each 356 | different kind of data we wish to store needs its own table, but to start with, we only have one: 357 | a post. The CREATE TABLE statement specifies which properties we want a post to have, 358 | namely: 359 |

360 | 361 | 369 | 370 |

371 | The column id has two particular features: 372 |

373 | 374 | 381 | 382 |

383 | A blog post is therefore just a row added to the posts table, since each row has space to store 384 | all of these values. Once the table is created, we also create some dummy article data, using 385 | INSERT. The format of this command, as used in the SQL script, is simply thus: 386 |

387 | 388 |

389 | 390 | INSERT INTO table_name (column_1, column_2, ...) VALUES (value_1, value_2, ...); 391 | 392 |

393 | 394 |

395 | Now, the SQL file won't do anything on its own: we need a program to send the commands to 396 | the database. This is where the install.php file comes in; we'll 397 | run that whenever we want to set up the blog (or to wipe it and start again). I'll describe 398 | its broad features here, but do also read the comments in the code. 399 |

400 | 401 | 402 |
403 | Re-running the installer on a fresh vhost just now resulted in permission errors when 404 | trying to create the file. Can we add a check in the script? 405 |
406 | 407 | 408 |

409 | The first half of the code is written in PHP. Here, we use a series of if() 410 | statements to ensure everything goes smoothly. The variable $error is set to an 411 | error message if something goes wrong. Here are the steps we take: 412 |

413 | 414 | 437 | 438 |

439 | The second half of the file (from <!DOCTYPE html>) presents the results of 440 | the script, in HTML. 441 |

442 |

So, if you have not already tried it, visit http://localhost/install.php in your 443 | browser, and make sure some posts are created. If you want to re-run it, delete the data.sqlite 444 | file manually and refresh your browser page. 445 |

446 | 447 | __NEWFILE__ 448 | 449 |

Using real data

450 | 451 |

452 | Since we now have some data in our database, we can change our home page to render that data, rather 453 | than the mock data it has now. It's worth noting though that the process of designing the page 454 | with dummy data hasn't been a waste of time; using a mock-up is often a valuable part of deciding what 455 | a page should contain, and where those elements should go. So, make these changes next: 456 |

457 | 458 | 459 | 460 |

461 | The first few lines determine the file path to the database, and then we create a new PDO object 462 | with new PDO(), which we can use to access the data. We then use the 463 | query() method to run a SQL statement that reads articles from the post 464 | table. 465 |

466 | 467 | 492 | 493 |

494 | So, refresh your browser's view of http://localhost/index.php, and ensure that your 495 | new changes render the posts from the database without errors. I'll then explain what is happening. 496 |

497 | 498 |

499 | After connecting to the database, the query is used to retrieve column values for each table row, 500 | returning ordering the rows in created_at DESC (i.e. in reverse creation order, or most 501 | recent first). It then enters a loop (in this case a while()) to render all the posts it 502 | finds. 503 |

504 | 505 |

506 | We use $stmt->fetch() to read the next available row, until the point when there are no 507 | rows left. When that happens, this method will return false and the loop will exit. 508 |

509 | 510 |

511 | For every row, there are two observations worth making. Firstly, rows are returned in an array, so we 512 | access them using the array syntax such as $row['title']. Secondly, you'll see that 513 | where text strings are output, they are wrapped in the htmlspecialchars() function. The 514 | reason for this is that, if user input (a blog title or blog post in this case) contains angle brackets, 515 | it could break the HTML used in the page layout, and worse, might let a user inject unauthorised 516 | JavaScript that would be run on other people's computers. 517 |

518 | 519 |

520 | You'll notice that most of the PHP code to deal with the database side of things is in the top half 521 | of the file, and the second half is predominantly HTML. This isn't an accident: this arrangement creates 522 | a healthy separation between the two, which makes each easier to work with. Of course, there are a few 523 | cases where dynamic output is required inside the HTML, but these are kept deliberately short. 524 |

525 | 526 |
~
527 | 528 |

529 | The next step is to create a page to show individual posts. Add the following file: 530 |

531 | 532 | 533 | 534 |

535 | Now, it is our intention to link this page from individual posts on the home page. However, we've 536 | not quite done that bit yet, so let's visit the new page manually, in order to test it. Open a 537 | new tab, paste the link http://localhost/view-post.php into the address 538 | bar, and check that a simple post appears. 539 |

540 | 541 |

542 | 543 | The connection to the database is the same as before, but we now have a WHERE clause 544 | in our SELECT statement, and we are now using prepare() and 545 | execute() to send the statement to the database driver. 546 |

547 | 548 |

549 | Let's take the WHERE statement first. In the home page, we ran a query without 550 | this, because we wanted everything in the table. However it is more often the case that we 551 | want to limit returned rows to those matching one or more conditions. This is where the 552 | WHERE comes in. For our new page, we only want rows that have a specific id (and 553 | since "id" is unique, we know we won't get more than one). 554 |

555 | 556 |

557 | You'll notice that "id" is fixed to 1 (i.e. the first post). We'll fix that later, since of 558 | course this should depend on the post the user clicks on. 559 |

560 | 561 |

562 | The other change is swapping out the call to query() with two new methods. 563 | prepare() is used to set up the statement, and indicates with a colon where values 564 | should go (i.e. ":id"). The execute() statement then runs the query, swapping 565 | place-holders with real values (in this case, the number 1). This technique is known as 566 | parameterisation, and is a good way to inject user-supplied input in a secure manner 567 | (the post ID is not yet user-supplied, but it soon will be). 568 |

569 | 570 |

571 | In fact, let's wire in the post page now. Make the following changes, and test that clicking 572 | on each 'Read more' link shows the appropriate post. 573 |

574 | 575 | 576 | 577 |

578 | Alright, so let's make some nice easy changes now. A look at our 579 | index.php and view-post.php 580 | files shows that our blog title and synopsis is repeated in both files. That's not good, since 581 | if these things need to be amended, we need to change them more than once. Happily, the 582 | solution is easy: we create a PHP file for the duplicated snippet, and then use 583 | require to pull it in. 584 |

585 | 586 | 587 | 588 |

589 | Once that is done, we can see there is still some duplication, this time with the database 590 | PHP code. So, let's fix that too! You'll notice here that we're using a subfolder for 591 | lib: 592 | this is a very common name for library code folders. 593 |

594 | 595 | 596 | 597 |

598 | The process of spotting code improvements (e.g. to reduce repetition) is known as 599 | refactoring, and it can be thought of as "cleaning as you go". Projects that are 600 | subject to these improvements are, at least in theory, more maintainable than ones that only 601 | get feature changes. 602 |

603 | 604 | 625 | 626 | __NEWFILE__ 627 | 628 |

Adding more features

629 | 630 |

631 | It is normal practice to link the main heading of a website to the home page, as this makes 632 | for a consistent navigation experience. So, let's do that now: 633 |

634 | 635 | 636 | 637 |

638 | Next, let us add in some logic that interprets two carriage-returns in a post as a paragraph 639 | break. Here we go: 640 |

641 | 642 | 643 | 644 |

645 | Now, the default date style used around the application isn't very readable, so let's 646 | improve that now: 647 |

648 | 649 | 650 | 651 |

652 | Now we've done a bit of tidying, let's tackle a bigger item of functionality. We need 653 | to allow users to comment on articles, so let's make the necessary database changes to 654 | prepare for that: 655 |

656 | 657 | 658 | 659 |

660 | Since this involves database changes, we'll need to delete our database file, and re-run 661 | the installer. So, delete the file in data/data.sqlite and then 662 | reinstall by visiting http://localhost/install.php again. 663 |

664 | 665 |

666 | You might ask why we are putting so much effort into an installer when we're nowhere 667 | near having a piece of finished software. The answer is that this installer is for us, 668 | the developers, and not for end users. Thus, it is something we want to set up reasonably 669 | early in our development lifecycle; whenever a database change is made, we want to be able 670 | to recreate our useful test data quickly and easily. 671 |

672 | 673 |

674 | Now we're ready to start adding some comment-related changes. To start with, let's add 675 | a count of comments on the front page. This uses the SQL command COUNT() to count 676 | the number of rows that would be returned by a query: 677 |

678 | 679 | 680 | 681 |

682 | Also, we'll add a comment listing to individual post pages: 683 |

684 | 685 | 686 | 687 |

688 | So, give that a whirl. You will want to check the new count on the homepage, and the comments 689 | feature on individual posts. How's that looking? 690 |

691 | 692 | __NEWFILE__ 693 | 694 |

Improving the installer

695 | 696 | 697 |
698 | I think the first person voice here is okay - the tutorial starts off with it, so it's 699 | consistent. 700 |
701 | 702 | 703 |

704 | At this juncture, I decided to tidy up the installer a bit more. Firstly, it would be nice to 705 | see some data about what it has created; in the course of development you can expect 706 | to wipe and recreate your test data hundreds of times, so it's worth making the output 707 | useful. 708 |

709 | 710 |

711 | Secondly, as it stands it demonstrates non-optimal techniques, and fixing that gives me an 712 | opportunity to explain how to improve upon it. Broadly, the problem is that visiting the URL 713 | changes the database, but it does not take into account that web addresses can receive visits 714 | from automated software (e.g. search engines looking for new websites). To be sure that it 715 | is a human who has requested an install, I've used a form with a "post" method (this is 716 | explained in more detail later). 717 |

718 | 719 |

720 | The changes here are quite substantial, so you may find it easier to download the file, and 721 | copy the whole thing over the top of the old version. So, here's the diff: 722 |

723 | 724 | 725 | 726 |

727 | Finally, let's add some links so we can easily move to our next task after re-installing: 728 |

729 | 730 | 731 | 732 |

733 | As is our custom, delete your database file and re-run the installer. This time you should 734 | have an "Install" button. All being well, it will do the same installation when you click it, 735 | and then you can opt to go straight to the blog's home page. 736 |

737 | 738 |
~
739 | 740 |

741 | Since our changes to the installer have been brief, let's do some refactoring and a couple of 742 | minor functionality tweaks. Now, it could be said that this process of tidying seems rather 743 | erratic, and that similar items of work should be neatly collected together. However, real-life 744 | development rarely works that way: instead, a rough but usable solution is developed, and then 745 | improvements and refactorings are added depending on user feedback, and often also on time 746 | available. 747 |

748 | 749 |

750 | So, here's our first small improvement task. One of the things you may have noticed is that 751 | posts and comments are being marked as written on a particular day, but with no time 752 | information. It is usual to record and show this sort of data, so let's do that now. Don't 753 | forget to re-run the installer to test it! 754 |

755 | 756 | 757 | 758 |

759 | Following on from our last bit of refactoring, here's another opportunity to tidy code. Arguably, 760 | the view post page has too much business logic in it, and it would be more maintainable 761 | to move this to a separate file. So, create lib/view-post.php 762 | and paste in the new content. 763 |

764 | 765 | 766 | 767 |

768 | One of the situations we have not yet catered for is the user requesting a blog article 769 | that does not exist. We need to handle that based on the maxim that "if something can go 770 | wrong, it will". There are a good few small changes here, but in essence our approach is 771 | that if we cannot find a database row, we issue a browser redirect and show an error. 772 |

773 | 774 | 775 |
776 | I've tested the new redirect code in common.php on a 777 | non-standard port, it works fine. 778 |
779 | 780 | 781 | 782 | 783 |

784 | Since the browser redirect is useful, it has been written as a function, and re-used by 785 | the installer. Although there is no new data to install, it is a good idea to delete your 786 | database file and re-install at this point, just to check it still works. 787 |

788 | 789 | __NEWFILE__ 790 | 791 |

Commenting form

792 | 793 |

794 | Righto, let's get onto a new solid block of functionality: allowing users to add comments. 795 | This adds a new block of HTML in comment-form.php, which 796 | reports any errors when the comment is made, a form to capture the usual comment information, 797 | and some business logic in lib/view-post.php and 798 | view-post.php to glue it all together. 799 |

800 | 801 | 802 | 803 |

804 | Let us have a look in detail at view-post.php, which is the page 805 | called by the web server when a single post is rendered. In the newly inserted code, we take 806 | these actions: 807 |

808 | 809 |
    810 |
  1. Reset the errors variable to null i.e. we do not yet know if they are any errors.
  2. 811 |
  3. Detect if we are in a POST operation. If we are not (i.e. the page is just being rendered 812 | normally, rather than submitting a form) then don't do any more of this new stuff.
  4. 813 |
  5. Get the author name, website URL and comment message, and pass them to new 814 | function addCommentToPost() for validation and saving.
  6. 815 |
  7. This will return any resulting errors into the $errors array.
  8. 816 |
  9. If the comment-adding function successfully saves a comment to the database, 817 | redirect to self and exit. This will request the page again in GET mode.
  10. 818 |
  11. If the comment save failed (perhaps because a mandatory field was not supplied) then 819 | the errors array will contain the error message(s). Since this skips the 820 | redirection phase, the form is rendered within the same call, which allows the errors 821 | to be marked on the form.
  12. 822 |
823 | 824 | 825 |
826 | If we get stuck for space, we could move this sidebar to the earlier 827 | place where we refactored the installer from GET to POST. 828 |
829 | 830 | 831 | 860 | 861 | 862 |
863 | I've added this hash deliberately in the wrong order - I felt it was best to do this before the 864 | validation failure thing. In the long term maybe it would be simpler just to merge this 865 | with the code that introduces the feature? 866 |
867 | 868 | 869 |

870 | Adding a comment doesn't work yet, as the INSERT command is missing a value for 871 | the date of comment creation. Let's fix that now: 872 |

873 | 874 | 875 | 876 |

877 | So, that gets the success case working. However, if you test a failure condition (empty name 878 | field or an empty comment, the fields that were filled in now disappear. This is because 879 | a form does not by default contain values, so we have to add them manually. 880 |

881 | 882 |

883 | Thus, we now set empty values for the GET case (no form submission) in 884 | view-post.php as well as 885 | the already existing POST case. Where we output the user-supplied data, we pass it through 886 | the PHP function htmlspecialchars(), which prevents any rendering problems if the 887 | user has used any HTML characters such as angle brackets. 888 |

889 | 890 | 891 |
892 | We could split the comment in comment-form.php 893 | as one of those vars is already in use, but it's not important. 894 |
895 | 896 | 897 | 898 | 899 | 900 |
901 | These last five items are refactoring/tidying. If this page gets too long, we could move 902 | a couple of them to the next page? 903 |
904 | 905 | 906 |

907 | And now for some more tidying. The first tweaking opportunity I noticed was that the code to 908 | make a comment safe to render to the screen, and to swap newlines for paragraph tags, might be 909 | useful elsewhere in the future. So I've generalised that snippet of code in a function, and 910 | then made use of it: 911 |

912 | 913 | 914 | 915 |

916 | A nice simple one is up next. The code for install.php was 917 | rather bloated by the presence of the 918 | large function at the start, and there's no reason why this couldn't be stored separately, 919 | making the installer page easier to maintain. We refactored this one a while ago, but it's 920 | perfectly fine to refactor again — the process can be regarded as fairly iterative 921 | anyway. So here's the diff, resulting in new file 922 | lib/install.php: 923 |

924 | 925 | 926 | 927 |

928 | When a comment is created, we use a snippet of code to create a timestamp in a format suitable 929 | for the database server. Since that'll be useful for other things, let's convert that to a 930 | reusable function: 931 |

932 | 933 | 934 | 935 |

936 | I noticed that the installer creates its own database connection. Although there was no 937 | pressing need to do so, I modified it so it uses a connection passed to it. This would make it 938 | easier to run automated tests against it, for example — a custom test connection would be 939 | passed to it, rather than the "hard-wired" one we have now removed. 940 |

941 | 942 | 943 | 944 |

945 | The last tweak is very easy: a re-reading of some of the code showed that I'd not updated 946 | a comment in line with a code change. Now, I could magic this away for the benefit of the 947 | tutorial, but I rather like the opportunity to show that code is never perfect, and that 948 | sometimes comments come out of sync with what they're meant to describe! So, just apply the 949 | following diff, and we're done for this chapter. 950 |

951 | 952 | 953 | 954 | __NEWFILE__ 955 | 956 |

Adding a login system

957 | 958 |

959 | The next thing I decided to implement was a log-on system. This will allow a blog administrator 960 | to write, edit and delete posts, and to delete comments. In this set of changes, I add a 961 | users table, and allow the installer to create an admin user with a new password each time it 962 | is run. So, make the following changes, and then re-create the database: 963 |

964 | 965 | 966 | 967 |

968 | There are two fields in the new user table that I added based on my experience rather than an 969 | immediate need. These are created_at, which holds the date and time when the user 970 | was first set up, and is_enabled, which allows us to turn users on and off. Most 971 | user systems will find a practical use for these simple features during their lifetime. 972 |

973 | 974 |

975 | You'll have noticed a comment in lib/install.php noting that 976 | the password is stored in plaintext. 977 | This means that, as it stands, passwords would be stored literally, which is considered to be 978 | very bad practice indeed. What we should do is to store passwords in an encoded, non-reversible 979 | format, so that even if they are stolen they will be nearly impossible to read. This acts as a 980 | form of protection should a cracker get through our security and steal our database. 981 |

982 | 983 | 984 |
985 | I was originally of the view that the library should be loaded only if the PHP version 986 | is <5.5, but this feels a bit fussy. I think the note here to say that users 987 | on 5.5+ can omit these changes if they wish is a cleaner. 988 |
989 | 990 | 991 |

992 | So, let's make that improvement straight away. To do so, we'll need the library I talked about 993 | at the start of the tutorial, stored here in vendor/password_compat. 994 | The features it provides are so useful they have been built into PHP 5.5, so if you are running 995 | this version (or later) you can skip adding the new file and the associated 996 | require_once. However if you are running an earlier version, or if you are not 997 | sure which version you have, add all of these changes. 998 |

999 | 1000 | 1001 | 1002 |

1003 | So, what does the new code do? It makes use of a new function called 1004 | password_hash(), which takes a password as input and produces what is known as a 1005 | hash. A hash is a mathematical calculation that is strictly one way, which means that 1006 | if sometimes steals our database of password hashes, they will find it very difficult indeed 1007 | to recreate the passwords they were generated from. 1008 |

1009 | 1010 |

1011 | Whether or not you are using it, by all means do have a read of the source code in 1012 | password.php. However, an in-depth exploration of it would be rather 1013 | advanced at this stage, so for our purposes we will assume it just works. 1014 |

1015 | 1016 |

1017 | The next step is to add a login form, and a link from which to access it: 1018 |

1019 | 1020 | 1021 | 1022 |

1023 | In the next change, we add our familiar block of business logic before the main HTML. In this 1024 | case, it checks to see if the form has been submitted; if it has, then it turns on the 1025 | session system (more about that in a minute), creates a hash of the submitted password, 1026 | and compares it with the hash stored in the database. 1027 |

1028 | 1029 |

1030 | If the user gets their username wrong (e.g. it is not found) or the password wrong (the 1031 | password hash does not match the one for the username supplied) then we regard this as a login 1032 | failure. It is important for us not to be too helpful here (such as explaining that a username 1033 | does not exist), as this information might be useful to a system cracker. 1034 |

1035 | 1036 |

1037 | If the password matches however, then we call login() to sign in the user, and 1038 | then we redirect to the home page using redirectAndExit(). 1039 |

1040 | 1041 | 1065 | 1066 |

1067 | So, with that explained, let's add the changes: 1068 |

1069 | 1070 | 1071 | 1072 |

1073 | Now we have a way to determine whether users are logged in or not, let's switch that feature 1074 | on for all pages. Here we also modify the HTML snippet that contains the page header, so 1075 | it can show the appropriate login/logout link. 1076 |

1077 | 1078 | 1079 | 1080 |

1081 | You may have noticed, if you tried the logout link, that this page does not yet exist. So let's 1082 | add that now in the following set of changes. Whilst we are at it, let's greet the user by 1083 | their username while they are logged in, as this makes the experience of the site a bit 1084 | more friendly. 1085 |

1086 | 1087 | 1088 | 1089 |

1090 | That's all for this chapter, so give your application a good test, especially the new 1091 | login and logout features. When you're ready, we'll proceed in the next chapter with some 1092 | small tweaks to improve what we have. 1093 |

1094 | 1095 | __NEWFILE__ 1096 | 1097 |

Tidy up

1098 | 1099 | 1100 |
1101 | Have I over-iterated the point about refactoring? Read through earlier material. 1102 |
1103 | 1104 | 1105 |

1106 | We've got a nice set of features working, but there are a good number of improvements and 1107 | refactoring changes that can be made. Doing these periodically can make for easier and 1108 | happier development, which in turn can improve your development efficiency. 1109 |

1110 | 1111 |

1112 | The first change is two-fold: pages were replicating some information in the header, and 1113 | various items (the top menu bar and system messages) were using inline style rules rather than 1114 | using class rules that need only be written once. 1115 |

1116 | 1117 | 1118 | 1119 |

1120 | Let's take a better look at the second change. Some of the original code was written like 1121 | this: 1122 |

1123 | 1124 |
1125 | <div style="border: 1px solid #ff6666; padding: 6px;"> … </div> 1126 |
1127 | 1128 |

1129 | The purpose of the style attribute to specify CSS rules (otherwise known as 1130 | style rules) to the HTML within. Firstly, we have the border rule, which says 1131 | the content (an error message) should have a red border rendered in an unbroken line, and 1132 | the padding means that there should be six pixels of gap between the border and 1133 | an invisible box around the content. 1134 |

1135 | 1136 |

1137 | However, having to write that for every error message takes a bit of effort, and it's a 1138 | pain if we decide that error messages should have a magenta border and not a red one. Thus, 1139 | it makes life easier if we write this instead: 1140 |

1141 | 1142 |
1143 | <div class="error box"> … </div> 1144 |
1145 | 1146 |

1147 | That applies two rules to the block: one called error and another called 1148 | box. That's much easier to remember, easier to read, and — since we 1149 | centralise the definitions in assets/main.css — easier 1150 | to change if we have to. 1151 |

1152 | 1153 |

1154 | While we are dealing with CSS changes, let's add a few more. Here we add some rules for 1155 | article synopses, titles and dates on the home page, and comments on individual blog posts. 1156 |

1157 | 1158 | 1159 | 1160 |

1161 | You may have noticed that some function calls that need to access the database create their 1162 | own connection rather than use one that we've already created. For low-volume systems this 1163 | might not matter a great deal, but programmers hate this sort of inefficiency, and where it is 1164 | trivial to fix, we should. 1165 |

1166 | 1167 | 1168 | 1169 |

1170 | Now we improve the appearance of the comment form. It's worth opening up an article page 1171 | prior to making the improvements, so you can refresh it after the CSS is in place. This will 1172 | allow you to see the change quickly. 1173 |

1174 | 1175 | 1176 | 1177 |

1178 | We now turn our attention to an improvement to the database. This consists of the tables 1179 | post and user, for blog posts and authors respectively. When 1180 | creating a post record, we insert our automatically generated user.id in 1181 | post.user_id to store which user has authored it (of course, we only have one 1182 | user in our test data, but in practice we might have several). 1183 |

1184 | 1185 |

1186 | However, it is possible to insert any number in post.user_id, so if we have 1187 | an undiscovered bug in our code, it might cause a non-existent user primary key to be stored 1188 | here. Since we rely on values here always pointing to a user row, if a bad value were to get 1189 | in, it might crash our application. 1190 |

1191 | 1192 |

1193 | To protect against that scenario, many database systems will allow the use of foreign key 1194 | constraints. These allow us to specify that values inserted into a particular column must 1195 | exist in another column belonging to another table, and that an error must occur if this 1196 | condition is not met. SQLite offers this feature, although it is unusual in that it needs to 1197 | be turned on explicitly, using the PRAGMA command. 1198 |

1199 | 1200 |

1201 | So, let's make these changes: 1202 |

1203 | 1204 | 1205 |
1206 | Deleting the user table does not result 1207 | in a constraint violation, since we refuse to run the SQL unless the database file is 1208 | manually deleted. 1209 |
1210 | 1211 | 1212 | 1213 | 1214 |

1215 | Here's what's new: 1216 |

1217 | 1218 | 1230 | 1231 |

1232 | As usual: delete your database file, re-run the installer, and check all the changes appear 1233 | to work. 1234 |

1235 | 1236 |

1237 | The last change is related to security. Since all of our files are in the web server's 1238 | public directory, a user who knows our directory structure would be able to download files 1239 | that we didn't intend to make accessible. Since SQLite uses a single file to store its data, 1240 | and since this file is often stored in a web-accessible location, it is of particular 1241 | importance to lock this down. 1242 |

1243 | 1244 | 1245 | 1246 | __NEWFILE__ 1247 | 1248 |

New post creation

1249 | 1250 |

1251 | Let's turn our attention to another major feature: writing a new article. We start off with 1252 | setting up a new page, laying out the necessary form input controls, and adding a new logged-in 1253 | menu item. There's also a change here to add a generic class user-form, so our 1254 | forms across the whole application can easily acquire a common look-and-feel; see how I've 1255 | gone back to existing form comment-form.php to update that too. 1256 |

1257 | 1258 | 1259 | 1260 |

1261 | Now, we only want to show this page for users who are logged in. Thus, if a user who is 1262 | not logged in tries to access it (by typing the URL in directly) we must redirect them 1263 | elsewhere; in this case, the home page will do fine. 1264 |

1265 | 1266 | 1267 | 1268 |

1269 | While we are here, let's take a look at how redirectAndExit() works; this is in 1270 | common.php. It starts by reading the domain we are running 1271 | on (such as localhost), since it is good not to hardwire this into our code. 1272 | It then sends a Location HTTP header to the browser, which will cause it to 1273 | request the specified address. Since the PHP script would happily carry on running at this 1274 | point (until it has detected that the browser has disconnected) we also need to forcibly 1275 | exit and wait for the redirect. 1276 |

1277 | 1278 | 1317 | 1318 |

1319 | Although we now have the structure of the new post feature, it does not work yet. So let us 1320 | fix that now: 1321 |

1322 | 1323 | 1324 | 1325 |

1326 | In a similar way to saving comments, we first test if we are in a post operation, using 1327 | if ($_POST). This contains an array of input values that will be only be present 1328 | if a user has submitted the form. 1329 |

1330 | 1331 |

1332 | We then make some simple checks to ensure the form data is acceptable prior to our attempting 1333 | to insert it into the database. This is a process known as form validation and is 1334 | a frequent task within web application development. If any checks fail, we allow the page to be 1335 | rendered in the POST request itself, plus error messages as appropriate, and it is only if the 1336 | checks succeed that we try to save the data and redirect back to the newly committed article. 1337 |

1338 | 1339 |

1340 | However, as it stands this redirect will just display a new, empty post, since we have not 1341 | added the logic to render the edit facility for a specific row. Let's add that now: 1342 |

1343 | 1344 | 1345 | 1346 |

1347 | That was nice and easy: if we find we are not in a post operation and we have a 1348 | post primary key and that row exists, then show it in the edit form. You can see that 1349 | now we need the database connection (in $pdo) for two things, I've moved 1350 | that line so that it is always executed. 1351 |

1352 | 1353 | 1354 |
1355 | Whether to trust the user with raw HTML is a complex area - maybe add a sidebar on this? 1356 | Or is this too much detail? 1357 |
1358 | 1359 | 1360 |

1361 | As we have done before, this code uses htmlspecialchars() to prevent users 1362 | from entering HTML, which could break our page layout or introduce security problems. It is 1363 | perhaps less of a worry here, since at least these users are authenticated, and hence 1364 | they might be considered more trustworthy than anonymous commenters. 1365 |

1366 | 1367 |

1368 | If you tried editing a post, you'll have found that this created a new post, rather than 1369 | updating the old one. So let's fix that also: 1370 |

1371 | 1372 | 1373 | 1374 |

1375 | I've moved the code to read the current post towards the start of the page, since this is 1376 | now useful in two situations. The first is when displaying an article for 1377 | editing, and the second is when submitting the edit form to save any changes. 1378 | In both cases, $_GET['post_id'] will be available, and we can look up that row 1379 | from the post table, and obtain title/body data if it is read successfully. 1380 |

1381 | 1382 |

1383 | Within a POST operation, we can then check $postId, and if it has a value we 1384 | know we are editing an existing article rather than creating a new one. Thus, if we are editing, 1385 | we call the new function editPost(), which will run the necessary 1386 | UPDATE command against the database, rather than addPost(), which 1387 | would run an INSERT. 1388 |

1389 | 1390 |

1391 | You might have noticed that there has been no attempt to check whether the user editing a 1392 | post is the same as the user who wrote it. Whether one person may edit other person's posts is 1393 | a feature decision, but for the time being it is one that I have deliberately omitted, to 1394 | keep things simple. 1395 |

1396 | 1397 | __NEWFILE__ 1398 | 1399 |

Post editing

1400 | 1401 |

1402 | In this chapter, we will build on our new editing functionality, but first, let's simplify 1403 | the title HTML block by moving the menu into a separate file: 1404 |

1405 | 1406 | 1407 | 1408 |

1409 | That change allows us to wire in the menu fragment separately, without the 1410 | title, so we can use a custom title in the edit page. You'll see that we set the title to 1411 | "New post" or "Edit post" depending on whether we can find a primary key in the query string. 1412 |

1413 | 1414 | 1415 | 1416 |

1417 | Let's also add in a cancel link in the edit page, to make it easier to abandon an edit: 1418 |

1419 | 1420 | 1421 | 1422 |

1423 | On the home page, and for logged-in users only, I next added an edit link against each post, 1424 | like so: 1425 |

1426 | 1427 | 1428 | 1429 |

1430 | You may have noticed that the class name (read-more) of the wrapping div was no 1431 | longer accurate, which is why I changed this to something more generic. As it happens, this 1432 | section doesn't have any CSS rules specifically attached to it, but it's good to have something 1433 | for future styles to hook onto). 1434 |

1435 | 1436 |

1437 | Lastly, let's make a another nice easy change - there's no point in rendering the login 1438 | page if we are already logged in. So, after the session is started, if we find we are logged 1439 | in already, redirect back to the home page. 1440 |

1441 | 1442 | 1443 | 1444 | __NEWFILE__ 1445 | 1446 |

All posts page

1447 | 1448 |

1449 | The next step is to add an All Posts screen for administrative users. We'll start with 1450 | a static mock-up with hard-wired post values: 1451 |

1452 | 1453 | 1454 | 1455 |

1456 | Of course, since this is a restricted screen, we must only allow authorised users to see it. 1457 | To do so, we redirect back to the home page, and exit as usual: 1458 |

1459 | 1460 | 1461 | 1462 | 1463 |

1464 | Great stuff. Don't forget to test it by logging out, and then trying to visit the page 1465 | manually! 1466 |

1467 | 1468 | 1469 |
1470 | This sidebar section might be slightly better melded into the text, which is 1471 | pretty light at the moment anyway. 1472 |
1473 | 1474 | 1475 | 1536 | 1537 |

1538 | Now, the logic we use to read the posts for the front page will be useful also for this 1539 | new screen. Since we don't want to write this twice, let's move it to a function so we can 1540 | reuse it later on. 1541 |

1542 | 1543 | 1544 | 1545 |

1546 | While working on the home page, I noticed I'd used the variable name $row. This 1547 | is rather generic — most things read from a database are rows — so I swapped to 1548 | a better name for it. This is more readable, and fits in with the common naming convention 1549 | of using a plural name for an array and the corresponding singular name for each item. 1550 |

1551 | 1552 | 1553 | 1554 |

1555 | Next, we'll modify the mock-up by adding in a creation time for posts: 1556 |

1557 | 1558 | 1559 | 1560 |

1561 | As with our mock-up approach before, once a layout contains everything required, it is time to 1562 | convert to a working version. So, let's do that now: we'll add a loop and render table data in 1563 | the HTML. 1564 |

1565 | 1566 | 1567 | 1568 |

1569 | As it stands, the user may click on a delete button to remove a post, but this is not 1570 | presently handled. So, let's do that now: 1571 |

1572 | 1573 | 1574 | 1575 |

1576 | Finally let's add in a post count on this page: 1577 |

1578 | 1579 | 1580 | 1581 | __NEWFILE__ 1582 | 1583 |

Comment admin

1584 | 1585 |

1586 | In this section, we'll allow authorised users to delete comments from posts. To kick us 1587 | off, here's a nice item of refactoring — the comments list within article pages is quite 1588 | large, and it would be more modular to store it in a separate file. 1589 |

1590 | 1591 | 1592 | 1593 |

1594 | Let us now add a delete button next to comments on blog posts, for authorised users only. I've 1595 | added a bit of extra CSS to get it to float to the right-hand side. 1596 |

1597 | 1598 | 1599 | 1600 |

1601 | Since we want to do more than one thing with comments (add and delete), we need to 1602 | make a change to explain what we are doing within the form submission. To do this, I've used 1603 | the query string, which sends these items of information: 1604 |

1605 | 1606 | 1612 | 1613 | 1614 | 1615 | 1616 |
1617 | We could add in a comment against the new line in view-post.php 1618 | to explain that this new function will redirect upon success. 1619 |
1620 | 1621 | 1622 |

1623 | Let us now make some room in our view post page, by moving some logic to a new function. 1624 |

1625 | 1626 | 1627 | 1628 |

1629 | We now need to read the action key we set up, so we can decide what feature to call. 1630 | If the user is deleting a comment, the new function deleteComment in the next set 1631 | of changes is called; although we could just delete by comment.id, to be sure of 1632 | deleting the right thing we filter additionally by comment.post_id. 1633 |

1634 | 1635 |

1636 | Note that we also add an if() clause to ensure that the delete operation is skipped if 1637 | the user is not logged in. Although we do not render delete buttons for anonymous users, it is 1638 | still possible for security crackers to fake a form submission that contains a valid deletion 1639 | request — this clause prevents that. 1640 |

1641 | 1642 | 1643 | 1644 |

1645 | Finally, I noticed that the logic to add a comment is contained within a function, but the 1646 | logic to delete one is still in the switch() block in 1647 | view-post.php. The following change just tidies that up: 1648 |

1649 | 1650 | 1651 | 1652 |

1653 | As ever, test everything to check it works, before we proceed to the final chapter! 1654 |

1655 | 1656 | __NEWFILE__ 1657 | 1658 |

Adding some polish

1659 | 1660 |

1661 | Excellent, we're now on the home straight! Our functionality is finished, so we'll just 1662 | do a last round of sprucing up. To start, let's add in a home link for authorised users. This 1663 | is useful on the restricted pages, since they don't have a home link themselves: 1664 |

1665 | 1666 | 1667 | 1668 |

1669 | Next, I thought it would be nice to have a comment count for each article on the All Posts page. 1670 | Add the following changes: 1671 |

1672 | 1673 | 1674 | 1675 |

1676 | Let's take a closer look at how the new query works. The query now looks like this: 1677 |

1678 | 1679 |
1680 |
SELECT
1681 | 	id, title, created_at, body,
1682 | 	(SELECT COUNT(*) FROM comment WHERE comment.post_id = post.id) comment_count
1683 | FROM
1684 | 	post
1685 | ORDER BY
1686 | 	created_at DESC
1687 |
1688 | 1689 |

1690 | The new part of this is the bracketed expression on the third line. This 1691 | sort of SQL is known as a sub-query since it is a query contained inside another 1692 | one. The result of it can be read like a real column, which we explicitly name by adding 1693 | comment_count at the end of it. 1694 |

1695 | 1696 |

1697 | This sub-query will count rows in the comment table, based on which post they 1698 | belong to. Since the job of the outer query is to list posts, we can filter the comment count 1699 | for each row by making a comparison to the outer table, post. 1700 |

1701 | 1702 |

1703 | The next change is another easy one: we'll add a header row to the All Posts table: 1704 |

1705 | 1706 | 1707 | 1708 |

1709 | While we are on the All Posts page, it would make sense to add in a link to the view page 1710 | for each article. So let's do that now: 1711 |

1712 | 1713 | 1714 | 1715 |

1716 | Next up is a refactoring that simplifies the code (quite a bit is removed) and reduces the 1717 | number of calls made to the database. The essence of the modification is in 1718 | lib/view-post.php — we merge two SQL calls together via 1719 | another sub-query. 1720 |

1721 | 1722 |

1723 | There's quite a few files to modify here, so make sure you get them all. 1724 |

1725 | 1726 | 1727 | 1728 |

1729 | I noticed when doing some testing at this point that posts which have comments cannot be 1730 | deleted - try that now. You'll get an error known as a foreign key constraint violation; 1731 | this is what happens when a row is deleted that a foreign key depends on (in our case we 1732 | are trying to delete a post to which comments are still attached). To fix this, comments 1733 | should be deleted first, prior to the post being deleted: 1734 |

1735 | 1736 | 1737 | 1738 |

1739 | Whilst we are looking at the database side of things, I noticed that the 1740 | user.is_enabled field hasn't yet been used. This was intended mainly for future 1741 | enhancements (in particular for a user administration page), but the application can make an 1742 | initial use of it immediately, by disallowing authenticated features to non-enabled users. 1743 |

1744 | 1745 |

1746 | The installer sets the test user to is_enabled=1 already, so all we need 1747 | to do is to adjust the SQL statements that fetch data from the user table. 1748 |

1749 | 1750 | 1751 | 1752 |

1753 | Our final improvement is to add some labels and make use of existing form styles: 1754 |

1755 | 1756 | 1757 | 1758 |

1759 | Well done, you have finished the tutorial! It is a good idea to regenerate the database one last 1760 | time, and then give the whole application a thorough testing. Add some posts and comments, both 1761 | while logged in and while logged out, and delete comments in the admin interface. 1762 |

1763 | --------------------------------------------------------------------------------