PHP MySQL Tutorial-C
PHP MySQL Tutorial-C
arman@php-mysql-tutorial.com
http://www.php-mysql-tutorial.com/
NOTE:
This tutorial currently only covers PHP4. When my web host support PHP5 I'll start adding some PHP5 specific features. Stay tuned for more php mysql tutorial.
1.
Installing Apache PHP and MySQL In case you haven't installed the trio yet don't skip this section. This page explains about installing Apache, PHP and MySQL on Windows plus some images to make things clearer. It also covers modifying Apache configuration and PHP configuration so the two can work together. PHP Tutorial Give you enough to get started. First, you will learn how to open and close PHP blocks, continued with using comments, a brief explanation about PHP variables and types. Then you'll learn about manipulating strings, control structures, functions and how to use web forms. MySQL Tutorial You will learn about starting MySQL, adding new MySQL user, creating a database and tables. Then you'll learn about the SQL queries to insert data,get the data, update and delete. Connecting to MySQL database This is where you start to put PHP and MySQL together. This page explains how to open and close MySQL connection with PHP. Creating a MySQL database Obviously you will need to create your database first. This part explains how to create MySQL database and table through PHP Insert Data To MySQL Database After you have the database and tables ready it's time to learn how to insert your data into the database. Getting The Data Once you have the data stored in the database surely you want to get it back. This page explains how to get your data out of MySQL plus how to convert your query result into Excel format Using Paging This one explains how to show your query result into multiple pages and how to create the navigation link. Also show the problem that might happen when using paging and the solution. Update and Delete Explains how to update and delete your data and how to use table locking to prevent violation of data integrity.
2.
3.
4.
5.
6.
7.
8.
9.
10. Using PHP To Backup MySQL Database In this page you can learn three different ways to backup your MySQL database 11. Form Validation This one explains how to validate HTML form on server side using PHP plus client side form validation using Javascript to make your form more user friendly. 12. Creating a Guestbook Guestbook is one of the most common feature for a website and this tutorial will teach you how to create your own guestbook using PHP and MySQL. It also explain how to use PHP functions to prevent code injection and the use of paging. 13. Uploading Files to MySQL This page describe how to upload a file to MySQL database and how to download it back. 14. Creating a Content Management System (CMS) Content Management System is getting more and more popular by the day. This tutorial explains how to create a simple CMS, how to add, modify and delete content using web form. 15. User Authentication This part explain three methods of authenticating a user. The first is hardcoding the user info in the script itself. The second one check for the user id and password in database. The third one add an random number verification. 16. Image Gallery Just another tutorial on making an image gallery 17. Finding Web Hosting for PHP and MySQL
Just some tips for choosing the right host. 18. Freelance PHP and MySQL jobs Internet is the best place to find freelance jobs. This page show one of them and also some list you need to think about before going freelancing. 19. Q & A I put some of the questions that i received here ( plus the answers ). Also added the common queries regarding php and mysql 20. PHP MySQL Bookstore Since this tutorial doesn't cover everything about php and mysql programming I decided to add a book store on this site. Here you can find several great books about programming with PHP and MySQL. And if you're thinking about replacing your old computer checkout the computer store 21. Shopping Cart Tutorial I finally complete this tutorial. It's kindof big so i put it in it's own domain. Because it's new i'm begging you to send me your critiques. The demo site is working with the administration stuff disabled ( like add/modify/delete product ) but you can download the source code so you can try it on your computer. 22. Online resources Some website related to PHP and MySQL. Actually a couple of these resources are not related to PHP or MySQL but I put them there anyway because they are useful. Check it out, you may find some that interest you
Contents
1. Installing Apache PHP and MySQL 2. PHP Tutorial 3. MySQL Tutorial o Starting MySQL o Add New MySQL User o Create New MySQL Database o Create New Table o Insert Data to MySQL o Get Data from MySQL o MySQL Update and Delet o Delete Data 4. Connect to MySQL Database 5. Create MySQL Database with PHP 6. Insert Data to MySQL Database 7. Get Data from MySQL database o Freeing the memory? o Convert MySQL Query to Excel 8. Using Paging 9. MySQL Update and Delete 10. Using PHP to Backup MySQL database o Execute a Database Backup Query from PHP file o Run mysqldump using system() function o Use phpMyAdmin to do the backup 11. Form Validation with PHP 12. Create A Guestbook o Create the Sign-Guestbook Form o View the entries o Showing the entries in multiple pages o Room for Improvements 13. Uploading Files to MySQL Database o Downloading Files from MySQL Database o Uploading Files to File Server Using PHP o Downloading o Preventing Direct Access o Handling duplicate file names 14. Content Management System (CMS) o Admin Page for Content Management System (CMS) o Delete Article o Edit Article 15. User Authentication o Basic User Authentication o Checking if the usesr is logged in or not o Better User Authentication: Storing o Uder id and Password in Database o User Authentication with Image Verification o The Login Form o Improving the Verification Image 16. Image Gallery o Image Gallery Administration Page o Login and Logout o Admin Page Layout o Admin: Add New Album o Admin: Album List o Modify and Delete Album o Delete Album o Admin: Add Image
o o o o
Image List Modify and Delete Image Album List Image List and Detail
17. Finding Web Hosting for PHP and MySQL 18. Freelance PHP MySQL Jobs 19. Q & A 20. PHP MySQL Bookstore 21. Shopping Cart Tutorial o Introduction o Shopping Cart Data Base Design o Database Abstract o Admin Control Panel o Admin login o Admin-View Category o Admin-Add Category o Admin-Edit Category o Admin-Delete Category o Admin-Add Product o Admin-View Product o Admin-Edit Product o Admin-Delet Product o Admin-Order Management o Admin-Site Configuration o Admin-User Management o Shop-Main Page o Shop-Browse Category o Shop-View Product List o Shop-View Product Detail o Shop-Add to Cart o Shop-View Shopping Cart o Shop-Chechkout o Shipping and Mayment info o Checkout confirmation o Payment Processing with PayPal o Handling PayPal Instant Payment Notification (IPN) o Shopping Cart Source Code o Shopping Cart Resources 22. PHP MySQL Resources
Installing Apache Installing PHP Modifying Apache Configuration Installing MySQL Modifying PHP Configuration File Installing Apache
Installing apache is easy if you download the Microsoft Installer ( .msi ) package. Just double click on the icon to run the installation wizard. Click next until you see the Server Information window. You can enter localhost for both the Network Domain and Server Name. As for the administrator's email address you can enter anything you want. I'm using Windows XP and installed Apache as Service so everytime I start Windows Apache is automatically started.
Click the Next button and choose Typical installation. Click Next one more time and choose where you want to install Apache ( I installed it in the default location C:\Program Files\Apache Group ). Click the Next button and then the Install button to complete the installation process. To see if you Apache installation was successful open up you browser and type http://localhost in the address bar. You should see something like this :
By default Apache's is set to htdocs directory. The document root is where you must put all your PHP or HTML files so it will be process by Apache ( and can be seen through a web browser ). Of course you can change it to point to any directory you want. The configuration file for Apache is stored in C:\Program Files\Apache Group\Apache2\conf\httpd.conf ( assuming you installed Apache in C:\Program Files\Apache Group ) . It's just a plain text file so you can use Notepad to edit it. For example, if you want to put all your PHP or HTML files in C:\www just find this line in the httpd.conf : DocumentRoot "C:/Program Files/Apache Group/Apache2/htdocs" and change it to : DocumentRoot "C:/www" After making changes to the configuration file you have to restart Apache ( Start > Programs > Apache HTTP Server 2.0.50 > Control Apache Server > Restart ) to see the effect.
document root
Another configuration you may want to change is the This is the file that Apache will show when you request a directory. As an example if you type http://www.php-mysql-tutorial.com/ without specifying any file the index.php file will be automatically shown. Suppose you want apache to use index.html, index.php or main.php as the directory index you can modify the DirectoryIndex value like this : DirectoryIndex index.html index.php main.php Now whenever you request a directory such as http://localhost/ Apache will try to find the index.html file or if it's not found Apache will use index.php. In case index.php is also not found then main.php will be used.
directory index.
Installing PHP
First, extract the PHP package ( php-4.3.10-Win32.zip ). I extracted the package in the directory where Apache was installed ( C:\Program Files\Apache Group\Apache2 ). Change the new created directory name to php ( just to make it shorter ). Then copy the file php.ini-dist in PHP directory to you windows directory ( C:\Windows or C:\Winnt depends on where you installed Windows ) and rename the file to php.ini. This is the PHP configuration file and we'll take a look what's in it later on. Next, move the php4ts.dll file from the newly created php directory into the sapi subdirectory. Quoting from php installation file you can also place php4ts.dll in other places such as :
Installing MySQL
First extract the package ( mysql-4.0.18-win.zip ) to a temporary directory, then run setup.exe. Keep clicking the next button to complete the installation. By default MySQL will be installed in C:\mysql. Open a DOS window and go to C:\mysql\bin and then run mysqld-nt --console , you should see
C:\mysql\bin>mysqld-nt --console
InnoDB: The first specified data file .\ibdata1 did not exist: InnoDB: a new database to be created! 040807 10:54:09 InnoDB: Setting file .\ibdata1 size to 10 MB InnoDB: Database physically writes the file full: wait... 040807 10:54:11 InnoDB: Log file .\ib_logfile0 did not exist: new to be created InnoDB: Setting log file .\ib_logfile0 size to 5 MB InnoDB: Database physically writes the file full: wait... 040807 10:54:12 InnoDB: Log file .\ib_logfile1 did not exist: new to be created InnoDB: Setting log file .\ib_logfile1 size to 5 MB InnoDB: Database physically writes the file full: wait... InnoDB: Doublewrite buffer not found: creating new InnoDB: Doublewrite buffer created InnoDB: Creating foreign key constraint system tables InnoDB: Foreign key constraint system tables created 040807 10:54:31 InnoDB: Started mysqld-nt: ready for connections. Version: '4.0.18-nt' socket: '' port: 3306 Now open another DOS window and type C:\mysql\bin\mysql if your installation is successful you will see the MySQL client running :
C:\mysql\bin>mysql
Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 1 to server version: 4.0.18-nt Type 'help;' or '\h' for help. Type '\c' to clear the buffer. mysql> Type exit on the mysql> prompt to quit the MySQL client.
Now let's install MySQL as a Service The process is simple just type mysqld-nt --install to install the service and net start mysql to run the service. But make sure to shutdown the server first using mysqladmin -u root shutdown C:\mysql\bin>mysqladmin -u root shutdown
C:\mysql\bin>mysqld-nt --install
Service successfully installed. C:\mysql\bin>net start mysql The MySQL service was started successfully.
C:\mysql\bin>mysql Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 1 to server version: 4.0.18-nt Type 'help;' or '\h' for help. Type '\c' to clear the buffer. mysql>
register_globals
On and after 4.2.0 the default value is Off. The reason for this change is because it is so easy to write insecure code with this value on. So make
Before PHP 4.2.0 the default value for this configuration is sure that this value is Off in php.ini.
session.save_path
This configuration tells PHP where to save the session data. You will need to set this value to an existing directory or you will not be able to use session. In Windows you can set this value as session.save_path = c:/windows/temp/
max_execution_time
The default value for max_execution_time is 30 ( seconds ). But for some scripts 30 seconds is just not enough to complete it's task. For example a database backup script may need more time to save a huge database. If you think your script will need extra time to finish the job you can set this to a higher value. For example to set the maximun script execution time to 15 minutes ( 900 seconds ) you can modify the configuration as max_execution_time = 900 PHP have a convenient function to modify PHP configuration in runtime, ini_set(). Setting PHP configuration using this function will
not make the effect permanent. It last only until the script ends.
That's it, now you have Apache, PHP and MySQL installed and running smoothly. It's time to move on to the next section, PHP Tutorial.
2. PHP Tutorial
What is PHP? PHP is a web programming language used to write dynamic webpages. In this tutorial you will learn basics of PHP. I will assume you already know a bit about programming language so I won't cover the whole thing. This PHP Tutorial will explain the followings :
Opening and ending PHP tags Comments Variables Types Playing with strings Control structures Functions Using Forms
First, make sure you already have PHP installed on your computer. If you don't have it installed visit the first part of this tutorial, Install Apache PHP and MySQL. By the way this tutorial only covers PHP 4 not the old PHP 3 ( I wonder if anyone still use it ? ). By the way, if you don't already use a suitable editor for coding check out this page. If you already have your favorite editor just skip it and move on to the next part : Opening and ending PHP tags
PHP Editors
Because PHP files are just plain text files you can use any editor to create and edit PHP files. But even though you can use simple text editor like Windows Notepad it is alot easier if you use an editor capable of syntax highlighting and advance text manipulation. For a simple file like hello.php syntax highlighting will do nothing good, but when you write long pages it's alot easier to find mistakes just by looking at the text color. Some editors you can use are :
The first pair (<? and ?>) is called short tags. You should avoid using short tags in your application especially if it's meant to be distributed on other servers. This is because short tags are not always supported ( though I never seen any web host that don't support it ). Short tags are only available only explicitly enabled setting the short_open_tag value to On in the PHP configuration file php.ini. So, for all PHP code in this website I will use the second pair, <?php and ?>. Now, for the first example create a file named hello.php ( you can use NotePad or your favorite text editor ) and put it in your web servers root directory. If you use Apache the root directory is APACHE_INSTALL_DIR\htdocs, with APACHE_INSTALL_DIR is the directory where you install Apache. So if you install Apache on C:\Program Files\Apache Group\Apache2\htdocs then you put hello.php in C:\Program Files\Apache Group\Apache2\htdocs Example : hello.php Source code : hello.phps <html> <head> <title>My First PHP Page</title> </head> <body>
<?php echo "<p>Hello World, How Are You Today?</p>"; ?> </body> </html> To view the result start Apache then open your browser and go to http://localhost/hello.php or http://127.0.0.1/hello.php. You should see something like this in your browser window. The example above shows how to insert PHP code into an HTML file. It also shows the echo statement used to output a string. See that the echo statement ends with a semicolon. Every command in PHP must end with a semicolon. If you forget to use semicolon or use colon instead after a command you will get an error message like this Parse error: parse error, unexpected ':', expecting ',' or ';' in c:\Apache\htdocs\examples\get.php on line 7 However in the hello.php example above omitting the semicolon won't cause an error. That's because the echo statement was immediately followed by a closing tag.
Using Comments
Comment is a part of your PHP code that will not be translated by the PHP engine. You can use it to write documentation of your PHP script describing what the code should do. A comment can span only for one line or span multiple line. PHP support three kinds of comment tags : 1. // This is a one line comment 2. # This is a Unix shell-style comment. It's also a one line comment 3. /* ..... */ Use this multi line comment if you need to. Example : comments.php Source code : comments.phps <?php echo "First line <br>"; // You won't see me in the output // I'm a one liner comment /* Same thing, you won't see this in the output file */ echo "The above comment spans two lines <br>"; # Hi i'm a Unix shell-style comment # Too bad you can't see me echo "View the source of this file, you'll see no comments here <br>"; ?>
PHP Variables
Variables in PHP are represented by a dollar sign followed by the name of the variable. The variable name is case-sensitive, so $myvar is different from $myVar. A valid variable name starts with a letter or underscore, followed by any number of letters, numbers, or underscores. Example : <?php $myvar = "Hello"; // valid $yourVar_is-123 = "World"; // valid $123ImHere = "Something"; // invalid, starts with number ?>
Variable Scope
The scope of a variable is the context within which it is defined. Basically you can not access a variable which is defined in different scope. The script below will not produce any output because the function Test() declares no $a variable. The echo statement looks for a local version of the $a variable, and it has not been assigned a value within this scope. Depending on error_reporting value in php.ini the script below will print nothing or issue an error
message. Example : scope.php Source code : scope.phps <?php $a = 1; // $a is a global variable function Test() { echo $a; // try to print $a, but $a is not defined here } Test(); ?> If you want your global variable (variable defined outside functions) to be available in function scope you need to use the$global keyword. The code example below shows how to use the $global keyword. Example : global.php Source code : global.phps <?php $a = 1; // $a is defined in global scope ... $b = 2; // $b too function Sum() { global $a, $b; // now $a and $b are available in Sum() $b = $a + $b; } Sum(); echo $b; ?>
PHP Superglobals
Superglobals are variables that available anywhere in the program code. They are :
$_SERVER
Variables set by the web server or otherwise directly related to the execution environment of the current script. One useful variable is $_SERVER['REMOTE_ADDR'] which you can use to know you website visitor's IP address Example : ip.php Source code : ip.phps Your computer IP is <?php echo $_SERVER['REMOTE_ADDR']; ?>
$_GET
Variables provided to the script via HTTP GET. You can supply GET variables into a PHP script by appending the script's URL like this : http://www.php-mysqltutorial.com/../examples/get.php?name=php&friend=mysql or set the a form method as method="get" Example: get.php Source code : get.phps <?php echo "My name is {$_GET['name']} <br>"; echo "My friend's name is {$_GET['friend']}"; ?> Note that I put $_GET['name'] and $_GET['friend'] in curly brackets. It's necessary to use these curly brackets when you're trying to place the value of an array into a string. You can also split the string like this : echo "My name is " . {$_GET['name']} . "<br>"; but it is easier to put the curly brackets.
$_POST
Variables provided to the script via HTTP POST. These comes from a form which set method="post"
$_COOKIE $_FILES
Variables provided to the script via HTTP cookies. Variables provided to the script via HTTP post file uploads. You can see the example in Uploading files to MySql Database
Variable Types
PHP supports eight primitive types. Four scalar types:
boolean : expresses truth value, TRUE or FALSE. Any non zero values and non empty string are also counted as TRUE. integer : round numbers (-5, 0, 123, 555, ...) float : floating-point number or 'double' (0.9283838, 23.0, ...) string : "Hello World", 'PHP and MySQL, etc
Two compound types:
array object
And finally two special types:
In PHP an array can have numeric key, associative key or both. The value of an array can be of any type. To create an array use the array() language construct like this. Example : array.php Source code : array.phps <?php $numbers = array(1, 2, 3, 4, 5, 6); $age = array("mom" => 45, "pop" => 50, "bro" => 25); $mixed = array("hello" => "World", 2 => "It's two"; echo echo echo echo ?> "numbers[4] = {$numbers[4]} <br>"; "My mom's age is {$age['mom']} <br>"; "mixed['hello'] = {$mixed['hello']} <br>"; "mixed[2] = {$mixed[2'}";
When working with arrays there is one function I often used. The print_r() function. Given an array this function will print the values in a format that shows keys and elements Example : printr.php Source code : printr.phps <?php $myarray = array(1, 2, 3, 4, 5); $myarray[5] = array("Hi", "Hello", "Konnichiwa", "Apa Kabar"); echo '<pre>'; print_r($myarray); echo '</pre>'; ?> Don't forget to print the preformatting tag <pre> and </pre> before and after calling print_r(). If you don't use them then you'll have to view the page source to see a result in correct format.
Type Juggling
In PHP you don't need to explicitly specify a type for variables. A variable's type is determined by the context in which that variable is used. That is to say, if you assign a string value to variable $var, $var becomes a string. If you then assign an integer value to $var, it becomes an integer.
An example of PHP's automatic type conversion is the addition operator '+'. If any of the operands is a float, then all operands are evaluated as floats, and the result will be a float. Otherwise, the operands will be interpreted as integers, and the result will also be an integer. Note that this does NOT change the types of the operands themselves; the only change is in how the operands are evaluated. Example : <?php $myvar $myvar $myvar $myvar ?>
= "0"; // $myvar is string (ASCII 48) += 2; // $myvar is now an integer (2) = $foo + 1.3; // $myvar is now a float (3.3) = 5 + "10 Piglets"; // $foo is integer (15)
Type Casting
To cast a variable write the name of the desired type in parentheses before the variable which is to be cast. Example : casting.php Source code : casting.phps <?php $abc = 10; // $abc is an integer $xyz = (boolean) $abc; // $xyz is a boolean echo "abc is $abc and xyz is $xyz <br>"; ?> The casts allowed are:
(int), (integer) - cast to integer (bool), (boolean) - cast to boolean (float), (double), (real) - cast to float (string) - cast to string (array) - cast to array (object) - cast to object
Playing With Strings
Strings are probably what you will use most in your PHP script. From concatenating, looking for patterns, trim, chop etc. So I guess it's a good idea to take a better look at this creature. We will also take a peek at some string functions that you might find useful for everyday coding.
Creating a string
To declare a string in PHP you can use double quotes ( " ) or single quotes ( ' ). There are some differences you need to know about using these two. If you're using double-quoted strings variables will be expanded ( processed ). Special characters such as line feed ( \n ) and carriage return ( \r ) are expanded too. However, with single-quoted strings none of those thing happen. Take a look at the example below to see what I mean. Note that browsers don't print newline characters ( \r and \n ) so when you open string.php take a look at the source and you will see the effect of these newline characters. Example : string.php Source code : string.phps <?php $fruit = 'jamblang'; echo "My favourite fruit is $fruit <br>"; echo 'I lied, actually I hate $fruit <br>'; echo "\r\n My first line \r\n and my second line <br>\r\n"; echo ' Though I use \r\n this string is still on one line <br>'; ?>
String Concatenation
To concat two strings you need the dot ( . ) operator so in case you have a long string and for the sake of readability you have to cut it into two you can do it just like the example below. Actually if you need to write a loong string and you want to write it to multiple lines you don't need concat the strings. You can do it just like the second example below where $quote2 is split into three lines. Example : concat.php Source : concat.phps <?php
$quote1 = "Never insult Dumbledore " . "in front of me!"; $quote2 = "Nami, you are my nakama!"; echo $quote1 . "<br>"; echo $quote2; ?>
String Functions
// print '12' echo substr('123456789', 0, 2); // print '56789' echo substr('123456789', 4); // print '89' echo substr('123456789', -2); // print '456' echo substr('123456789', 3, -4); ?>
str_repeat($string, $n)
For example if you want to print a series of ten asteriks ( * ) you can do it with a for loop like this : <?php for ($i = 0; $i < 10; $i++) { echo '*'; } ?> Or you can go the easy way and do it like this : <?php echo str_repeat('*', 10); ?>
strrchr($string, $char)
For example: you want to get the file extension from a file name. You can use this function in conjunction with substr() <?php $ext = substr(strrchr($filename, '.'), 1); ?> What the above code do is get a chunk of $filename starting from the last dot in $filename then get the substring of it starting from the second character ( index 1 ). To make things clearer suppose $filename is 'tutorial.php'. Using strrchr('tutorial.php', '.') yield '.php' and after substr('.php', 1) we get the file extension; 'php'
trim($string)
');
addslashes($string)
This function is usually used on form values before being used for database queries. You will see this function used a lot in this tutorial ( like this one ) so there's no need to present an example here.
explode($separator, $string)
This function is commonly used to extract values in a string which are separated by a a certain separator string. For example, suppose we have some information stored as comma separated values. To extract each values we ca do it like shown below <?php // extract information from comma separated values $csv = 'Uzumaki Naruto,15,Konoha Village'; $info = explode(',', $csv); ?> Now, $info is an array with three values : Array ( [0] => Uzumaki Naruto [1] => 15 [2] => Konoha Village ) We can further process this array like displaying them in a table, etc.
implode($string, $array)
This one do the opposite than the previous function. For example to reverse back the $info array into a string we can do it like this : <?php $info = array('Uzumaki Naruto', 15, 'Konoha Village'); $csv = implode(',', $info); ?> Another example : Pretend we have an array containing some values and we want to print them in an ordered list. We can use the implode() like this : <?php // print ordered list of names in array $names = array('Uchiha Sasuke', 'Haruno Sakura', 'Uzumaki Naruto', 'Kakashi'); echo '<ol><li>' . implode('</li><li>', $names) . '</li></ol>'; ?> The result of that code is like an ordered list just like shown below 1. Uchiha Sasuke 2. Haruno Sakura 3. Uzumaki Naruto 4. Kakashi By the way, i did write the above php code to print that list instead of writing the list directly
number_format($number)
When displaying numbers it's usuallly more readable if the numbers is properly formatted like 1,234,567 instead of 1234567. Using this function is very simple like shown below <?php // display 15,120,777 echo number_format(15120777); ?>
Control Structures
The next examples will show you how to use control structures in PHP. I won't go through all just the ones that i will use in the code examples in this site. The control structures are
If Else
The if statement evaluates the truth value of it's argument. If the argument evaluate as TRUE the code following the if statement will be executed. And if the argument evaluate as FALSE and there is an else statement then the code following the else statement will be executed. Example : visitor- info.php Source code : visitor-info.phps <?php $ip = $_SERVER['REMOTE_ADDR']; $agent = $_SERVER['HTTP_USER_AGENT']; if(strpos($agent, 'Opera') !== false) $agent = 'Opera'; else if(strpos($agent, "MSIE") !== false) $agent = 'Internet Explorer'; echo "Your computer IP is $ip and you are using $agent"; ?>
The strpos() function returns the numeric position of the first occurrence of it's second argument ('Opera') in the first argument ($agent). If the string 'Opera' is found inside $agent, the function returns the position of the string. Otherwise, it returns FALSE. When you're using Internet Explorer 6.0 on Windows XP the value of $_SERVER['HTTP_USER_AGENT'] would be something like: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1) and if you're using Opera the value the value may look like this : Mozilla/4.0 (compatible; MSIE 6.0; MSIE 5.5; Windows NT 5.1) Opera 7.0 [en] So if i you use Opera the strpos() function will return value would be 61. Since 61 !== false then the first if statement will be evaluated as true and the value of $agent will be set to the string 'Opera'. Note that I use the !== to specify inequality instead of != The reason for this is because if the string is found in position 0 then the zero will be treated as FALSE, which is not the behaviour that I want.
While Loop
The while() statement is used to execute a piece of code repeatedly as long as the while expresssion evaluates as true. For example the code below will print the number one to nine. Example : while.php Source code : while.phps <?php $number = 1; while ($number < 10) { echo $number . '<br>'; $number += 1; } ?> You see that I make the code $number += 1; as bold. I did it simply to remind that even an experienced programmer can sometime forget that a loop will happily continue to run forever as long as the loop expression ( in this case $number < 10 ) evaluates as true. So when you're creating a loop please make sure you already put the code to make sure the loop will end in timely manner.
Break
The break statement is used to stop the execution of a loop. As an example the while loop below will stop when $number equals to 6. Example : break.php Source code : break.phps <?php $number = 1; while ($number < 10) {
echo $number . '<br>'; if ($number == 6) { break; } $number += 1; } ?> You can stop the loop using the break statement. The break statement however will only stop the loop where it is declared. So if you have a cascading while loop and you put a break statement in the inner loop then only the inner loop execution that will be stopped. Example : break2.php Source code : break2.phps <?php $floor = 1; while ($floor <= 5) { $room = 1; while ($room < 40) { echo "Floor : $floor, room number : $floor". "$room <br>"; if ($room == 2) { break; } $room += 1; } $floor += 1; echo "<br>"; } ?> If you run the example you will see that the outer loop, while ($floor <= 5), is executed five times and the inner loop only executed two times for each execution of the outer loop. This proof that the break statement only stop the execution of the inner loop where it's declared.
For
The for loop syntax in PHP is similar to C. For example to print 1 to 10 the for loop is like this <?php for ($i = 1; $i <= 10; $i++) { echo $i . '<br>'; } ?> A more interesting function is to print this number in a table with alternating colors. Here is the code Example : alternate-colors.php Source : alternate-colors.phps <table width="200" border="0" cellspacing="1" cellpadding="2"> <tr> <td bgcolor="#CCCCFF">Alternating row colors</td> </tr> <?php for ($i = 1; $i <= 10; $i++) { if ($i % 2) { $color = '#FFFFCC'; } else { $color = '#CCCCCC'; } ?> <tr> <td bgcolor="<?php echo $color; ?>"><?php echo $i; ?></td> </tr> <?php } ?> </table> This code display different row colors depending on the value of $i. If $i is not divisible by two it prints yellow otherwise it prints gray colored rows.
Using Functions
Real world applications are usually much larger than the examples above. In has been proven that the best way to develop and maintain a large program is to construct it from smaller pieces (functions) each of which is more manageable than the original program. A function may be defined using syntax such as the following: <?php function addition($val1, $val2) { $sum = $val1 + $val2; return $sum; } ?>
Returning Values
Applications are usually a sequence of functions. The result from one function is then passed to another function for processing and so on. Returning a value from a function is done by using the return statement. Example : return.php Source code : return.phps <?php $myarray = array('php tutorial', 'mysql tutorial', 'apache tutorial', 'java tutorial', 'xml tutorial'); $rows = buildRows($myarray); $table = buildTable($rows); echo $table; function buildRows($array) { $rows = '<tr><td>' . implode('</td></tr><tr><td>', $array) . '</td></tr>'; return $rows;
} function buildTable($rows) { $table = "<table cellpadding='1' cellspacing='1' border='1'>$rows</table>"; return $table; } ?> You can return any type from a function. An integer, double, array, object, resource, etc. Notice that in buildRows() I use the built in function implode(). It joins all elements of $array with the string '</td></tr><tr><td>' between each element. I also use the '.' (dot) operator to concat the strings. You can also write buildRows() function like this. <?php ... function buildRows($array) { $rows = '<tr><td>'; $n = count($array); for($i = 0; $i < $n - 1; $i++) { $rows .= $array[$i] . '</td></tr><tr><td>'; } $rows .= $array[$n - 1] . '</td></tr>'; return $rows; } ... ?> Of course it is more convenient if you just use implode().
bgcolor='#FFCC00'
Using Form
Using forms in a web based application is very common. Most forms are used to gather information like in a signup form, survey / polling, guestbook, etc. A form can have the method set as post or get. When using a form with method="post" you can use $_POST to access the form values. And when the form is using method="get" you can use $_GET to access the values. The $_REQUEST superglobal can be used to to access form values with method="post" and method="get" but it is recommended to use $_POST or $_GET instead so you will know from what method did the values come from. Here is an example of HTML form : Example : form.php Source code : form.phps <form method="post" action="<?php echo $_SERVER['PHP_SELF'];?>"> Name : <input name="username" type="text"><br> Password : <input name="password" type="password"><br> <input name="send" type="submit" value="Send!"> </form> The form above use $_SERVER['PHP_SELF'] as the action value. It is not required for the form to perform correctly but it's considered good programming practice to use it. Below is the PHP code used to access form values : Example : form.php Source code : form.phps <?php if(isset($_POST['send'])) { echo "Accessing username using POST : " . echo "Accessing username using REQUEST : " . "<br>"; $password = $_POST['password']; echo "Password is $password"; }
?> The if statement is used to check if the send variable is set. If it is set then the form must have been submitted. The script then print the value of username using $_POST and $_REQUEST
That's it. You just completed the basics of PHP programming. When you're ready for more advance programming in PHP check out the book store. You can find plenty good books in there with deeper coverage on PHP programming As is said earlier if you haven't got the PHP manual you should download it now from php.net. You can find lots of interesting (and important) stuff in it. You can get the manual in various format but I recommend that you get the compiled HTML (CHM) version. It's easier to read plus it has search capability and loads of user contributed notes, very useful. Now let's move on the next section : MySQL Tutorial
3. MySQL Tutorial
This mysql tutorial covers the basic stuff that you can do with MySQL. But before we go any further make sure you already have MySQL installed, if you don't have it installed check out this page : Installing PHP and MySQL (plus Apache). This part of tutorial covers the following :
Starting MySQL
How to start mysql server and client from the command line ( DOS )
Create a new database Create tables Insert data Get the data Update & Delete data
Starting MySQL
Before starting the MySQL client make sure the server is turned on. If you run Windows 9x start the mysqld.exe or if you run Windows 2000/XP run the mysqld-nt.exe
The --console option tells mysqld-nt not to remove the console window. if you are running Windows NT/2000/XP it's better to install MySQL as a service. That way mysql server will be automatically started when you start Windows. Use mysqld-nt --install to install mysqld as a service and mysqld-nt --remove to remove mysqld from the service list Once the server is on, open another DOS window and type mysql, you should see something like this C:\>mysql Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 3 to server version: 4.0.18-nt Type 'help;' or '\h' for help. Type '\c' to clear the buffer. mysql> Or if you need to specify a user name and password you can start mysql like this : C:\>mysql -h localhost -u root -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 4 to server version: 4.0.18-nt Type 'help;' or '\h' for help. Type '\c' to clear the buffer. mysql> You can also specify the database you want to use. If you already have a database named petstore you can start mysql as C:\>mysql petstore Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 5 to server version: 4.0.18-nt Type 'help;' or '\h' for help. Type '\c' to clear the buffer. mysql> Once you're done you can end mysql client by typing quit or exit at the mysql> prompt mysql> exit Bye
C:\> Note : If you get this kind of error message when trying to run mysql from the DOS window C:\>mysql 'mysql' is not recognized as an internal or external command, operable program or batch file. that means you haven't set the path to mysql bin directory. To solve the problem you can add the following line to your autoexec.bat file : path=%path%;c:\mysql\bin assuming that you install MySQL in c:\mysql. Or if you're using Windows XP you can go to : Start->Settings->Control Panel->System->Advanced->Environment Variables Chose Edit on the System Variables section and add c:\mysql\bin to the path environment variable.
When adding a new user remember to encrypt the new password using PASSWORD() function provided by MySQL. As you can see in the above example the password mypass is encrypted to 6f8c114b58f2ce9e. Notice the the FLUSH PRIVILEGES statement. This tells the server to reload the grant tables. If you don't use it then you won't be able to connect to mysql using the new user account (at least until the server is reloaded). You can also specify other privileges to a new user by setting the values of these columns in user table to 'Y' when executing the INSERT query :
Select_priv Insert_priv Update_priv Delete_priv Create_priv Drop_priv Reload_priv Shutdown_priv Process_priv File_priv Grant_priv References_priv Index_priv Alter_priv
I think you can guess what those privileges serve by reading it's name
C:\>mysqladmin create petstore C:\>mysql Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 3 to server version: 4.0.18-nt Type 'help;' or '\h' for help. Type '\c' to clear the buffer. mysql> SHOW databases; +----------+ | Database | +----------+ | mysql | | petstore | | test | +----------+ 2 rows in set (0.00 sec) mysql> You can also type the query in mysql> prompt like this mysql> CREATE database petstore; Query OK, 1 row affected (0.00 sec) To show available databases in mysql use the command show databases on mysql> prompt. Now use the database by typing USE petstore and then type SHOW tables to see what tables are available in the database mysql> USE petstore; Database changed mysql> SHOW tables; Empty set (0.00 sec) Next I will show you how to create table in mysql database
For this example we'll create two tables. The first one describe the species of animals available in a pet store and the second will store the data of a pet in the store. The table names will be species and pet
The species table will consist of the id and the animal species, and the pet table will consist of animal id, species, sex, and price mysql> CREATE TABLE species (id INT NOT NULL AUTO_INCREMENT, species varchar(30) NOT NULL, primary key(id)); Query OK, 0 rows affected (0.02 sec) mysql> CREATE TABLE pet(id INT NOT NULL AUTO_INCREMENT, sp_id INT NOT NULL, sex CHAR(1) NOT NULL, price DECIMAL(4,2) NOT NULL, primary key(id)); Query OK, 0 rows affected (0.03 sec) mysql> SHOW tables; +--------------------+ | Tables_in_petstore | +--------------------+ | pet | | species | +--------------------+ 2 rows in set (0.00 sec) mysql> DESCRIBE species; +---------+-------------+----+-----+---------+---------------+ | Field | Type |Null| Key | Default | Extra | +---------+-------------+----+-----+---------+---------------+ | id | int(11) | | PRI | NULL | auto_increment| | name | varchar(30) | | | | | +---------+-------------+----+-----+---------+---------------+ 2 rows in set (0.05 sec) mysql> DESC pet; +-------+--------------+----+-----+---------+----------------+ | Field | Type |Null| Key | Default | Extra | +-------+--------------+----+-----+---------+----------------+ | id | int(11) | | PRI | NULL | auto_increment | | sp_id | int(11) | | | 0 | | | sex | char(1) | | | | | | price | decimal(4,2) | | | 0.00 | | +-------+--------------+----+-----+---------+----------------+ 4 rows in set (0.00 sec) The SQL sytax to create table is : CREATE TABLE <tablename> (<list of fields>)
The DESCRIBE or DESC statement is used to show a description of a table. You can also use EXPLAIN or SHOW COLUMNS mysql> EXPLAIN pet; +-------+--------------+----+-----+---------+----------------+ | Field | Type |Null| Key | Default | Extra | +-------+--------------+----+-----+---------+----------------+ | id | int(11) | | PRI | NULL | auto_increment | | sp_id | int(11) | | | 0 | | | sex | char(1) | | | | | | price | decimal(4,2) | | | 0.00 | | +-------+--------------+----+-----+---------+----------------+ 4 rows in set (0.00 sec) mysql> SHOW COLUMNS FROM pet; +-------+--------------+----+-----+---------+----------------+ | Field | Type |Null| Key | Default | Extra | +-------+--------------+----+-----+---------+----------------+ | id | int(11) | | PRI | NULL | auto_increment | | sp_id | int(11) | | | 0 | | | sex | char(1) | | | | | | price | decimal(4,2) | | | 0.00 | | +-------+--------------+----+-----+---------+----------------+ 4 rows in set (0.00 sec)
To select only the records that interest you, you can use WHERE statement followed by the definition. For example to select a record from species table where id equals 4 you can do this : mysql> SELECT * FROM species WHERE id = 4; +----+--------+ | id | name | +----+--------+ | 4 | Turtle | +----+--------+ 1 row in set (0.59 sec) If you want to order the returned rows by a criteria you can use ORDER BY like this : mysql> SELECT * FROM species ORDER BY name; +----+--------+ | id | name | +----+--------+ | 2 | Bird | | 1 | Cat | | 3 | Fish | | 4 | Turtle | +----+--------+ 4 rows in set (0.00 sec) By default the result is sorted in ascending order. So this query will give the same result : mysql> SELECT * FROM species ORDER BY name ASC; +----+--------+ | id | name | +----+--------+ | 2 | Bird | | 1 | Cat | | 3 | Fish | | 4 | Turtle | +----+--------+ 4 rows in set (0.00 sec) The ASC means ascending order. To get a descending order you just change the ASC with DESC like this : mysql> SELECT * FROM species ORDER BY name DESC; +----+--------+ | id | name | +----+--------+ | 4 | Turtle | | 3 | Fish | | 1 | Cat | | 2 | Bird | +----+--------+ 4 rows in set (0.00 sec)
Delete Data
The DELETE statement deletes rows from a table that satisfy the condition given by the WHERE clause. For example to delete a record from species table where id equals 1 mysql> DELETE FROM species WHERE id = 1; Query OK, 1 row affected (0.00 sec) mysql> SELECT * FROM species; +----+-------+ | id | name | +----+-------+ | 2 | Bird | | 3 | Fish | | 4 | Dog |
Now that you alredy know the basics of MySQL it's time to continue the tutorial. You will start learning how to connect to MySQL database using PHP. By the way this is a good time for you to download download the MySQL reference manual ( if you haven't done so). It covers in detail about everything you need to know about MySQL. As with the PHP manual you should download the compiled HTML version of MySQL manual because it's easier to browse than other formats.
Sometimes a web host will require you to specify the MySQL server name and port number. For example if the MySQL server name is db.php-mysql-tutorial.com and the port number is 3306 (the default port number for MySQL) then you you can modify the above code to : <?php $dbhost = 'db.php-mysql-tutorial.com:3306'; $dbuser = 'root'; $dbpass = 'password'; $conn = mysql_connect($dbhost, $dbuser, $dbpass) or die ('Error connecting to mysql'); $dbname = 'petstore'; mysql_select_db($dbname); ?> It's a common practice to place the routine of opening a database connection in a separate file. Then everytime you want to open a connection just include the file. Usually the host, user, password and database name are also separated in a configuration file. An example of config.php that stores the connection configuration and opendb.php that opens the connection are : Source code : config.phps , opendb.phps <?php // This $dbhost $dbuser $dbpass $dbname ?>
<?php // This is an example opendb.php $conn = mysql_connect($dbhost, $dbuser, $dbpass) or die ('Error connecting to mysql'); mysql_select_db($dbname); ?> So now you can open a connection to mysql like this : <?php include 'config.php'; include 'opendb.php'; // ... do something like insert or select, etc ?>
?>
Deleting a Database
As with creating a database, it is also preferable to use mysql_query() and to execute the SQL DROP DATABASE statement instead of using mysql_drop_db() <?php include 'config.php'; include 'opendb.php'; // ... do something here $query = 'DROP DATABASE phpcake'; $result = mysql_query($query); // ... probably do something here too include 'closedb.php'; ?> After the database and the tables are ready it's time to put something into the database.
There is another method for you to get the values from a row. You can use list(), to assign a list of variables in one operation. <?php include 'config.php'; include 'opendb.php'; $query = "SELECT name, subject, message FROM contact"; $result = mysql_query($query); while(list($name,$subject,$message)= mysql_fetch_row($result)) { echo "Name :$name <br>" . "Subject : $subject <br>" . "Message : $row <br><br>"; } include 'closedb.php'; ?> In above example, list() assign the values in the array returned by mysql_fetch_row() into the variable $name, $subject and $message. Of course you can also do it like this <?php include 'config.php'; include 'opendb.php'; $query = "SELECT name, subject, message FROM contact"; $result = mysql_query($query); while($row = { $name $subject $message mysql_fetch_row($result)) = $row[0]; = $row[1]; = $row[2];
echo "Name :$name <br>" . "Subject : $subject <br>" . "Message : $row <br><br>"; } include 'closedb.php'; ?> So you see you have lots of choices in fetching information from a database. Just choose the one appropriate for your program
Using PHP to convert MySQL query result to Excel format is also common especially in web based finance applications. The finance data stored in database are downloaded as Excel file for easy viewing. There is no special functions in PHP to do the job. But you can do it easily by formatting the query result as tab separated values or put the value in an HTML table. After that set the content type to application/vnd.ms-excel Example : convert.php Source : convert.php, students.txt <?php include 'library/config.php'; include 'library/opendb.php'; $query = "SELECT fname, lname FROM students"; $result = mysql_query($query) or die('Error, query failed'); $tsv = array(); $html = array(); while($row = mysql_fetch_array($result, MYSQL_NUM)) { $tsv[] = implode("\t", $row); $html[] = "<tr><td>" .implode("</td><td>", $row) . } $tsv = implode("\r\n", $tsv); $html = "<table>" . implode("\r\n", $html) . "</table>"; $fileName = 'mysql-to-excel.xls'; header("Content-type: application/vnd.ms-excel"); header("Content-Disposition: attachment; filename=$fileName"); echo $tsv; //echo $html; include 'library/closedb.php'; ?> In the above example $tsv is a string containing tab separated values and $html contain an HTML table. I use implode() to join the values of $row with tab to create a tab separated string. After the while loop implode() is used once again to join the rows using newline characters. The headers are set and the value of $tsv is then printed. This will force the browser to save the file as mysql-toexcel.xsl Try running the script in your own computer then try commenting echo $tsv and uncomment echo $html to see the difference.
"</td></tr>";
Next, how to split your query result to multiple pages by applying paging.
8. Using Paging
Ever use a Search Engine? I'm sure you have, lots of time. When Search Engines found thousands of results for a keyword do they spit out all the result in one page? Nope, they use paging to show the result little by little. Paging means showing your query result in multiple pages instead of just put them all in one long page. Imagine waiting for five minutes just to load a search page that shows 1000 result. By splitting the result in multiple pages you can save download time plus you don't have much scrolling to do. To show the result of a query in several pages first you need to know how many rows you have and how many rows per page you want to show. For example if I have 295 rows and I show 30 rows per page that mean I'll have ten pages (rounded up).
For the example I created a table named randoms that store 295 random numbers. Each page shows 20 numbers. Example: paging.php Source code :paging.phps <?php include 'library/config.php'; include 'library/opendb.php'; // how many rows to show per page $rowsPerPage = 20; // by default we show first page $pageNum = 1; // if $_GET['page'] defined, use it as page number if(isset($_GET['page'])) { $pageNum = $_GET['page']; } // counting the offset $offset = ($pageNum - 1) * $rowsPerPage; $query = " SELECT val FROM randoms " . " LIMIT $offset, $rowsPerPage"; $result = mysql_query($query) or die('Error, query failed'); // print the random numbers while($row = mysql_fetch_array($result)) { echo $row['val'] . '<br>'; } // ... more code here ?> Paging is implemented in MySQL using LIMIT that take two arguments. The first argument specifies the offset of the first row to return, the second specifies the maximum number of rows to return. The offset of the first row is 0 ( not 1 ). When paging.php is called for the first time the value of $_GET['page'] is not set. This caused $pageNum value to remain 1 and the query is : SELECT val FROM randoms LIMIT 0, 20 which returns the first 20 values from the table. But when paging.php is called like this http://www.phpmysql-tutorial.com/examples/paging/paging.php?page=4 the value of $pageNum becomes 4 and the query will be : SELECT val FROM randoms LIMIT 60, 20 this query returns rows 60 to 79. After showing the values we need to print the links to show any pages we like. But first we have to count the number of pages. This is achieved by dividing the number of total rows by the number of rows to show per page : $maxPage = ceil($numrows/$rowsPerPage); <?php // ... the previous code // how many rows we have in database
= = = =
"SELECT COUNT(val) AS numrows FROM randoms"; mysql_query($query) or die('Error, query failed'); mysql_fetch_array($result, MYSQL_ASSOC); $row['numrows'];
// how many pages we have when using paging? $maxPage = ceil($numrows/$rowsPerPage); // print the link to access each page $self = $_SERVER['PHP_SELF']; $nav = ''; for($page = 1; $page <= $maxPage; $page++) { if ($page == $pageNum) { $nav .= " $page "; // no need to create a link to current page } else { $nav .= " <a href=\"$self?page=$page\">$page</a> "; } } // ... still more code coming ?> The mathematical function ceil() is used to round up the value of $numrows/$rowsPerPage. In this case the value of total rows $numrows is 295 and $rowsPerPage is 20 so the result of the division is 14.75 and by using ceil() we get $maxPage = 15 Now that we know how many pages we have we make a loop to print the link. Each link will look something like this: <a href="paging.php?page=5">5</a> You see that we use $_SERVER['PHP_SELF'] instead of paging.php when creating the link to point to the paging file. This is done to avoid the trouble of modifying the code in case we want to change the filename.
We are almost complete. Just need to add a little code to create a 'Previous' and 'Next' link. With these links we can navigate to the previous and next page easily. And while we at it let's also create a 'First page' and 'Last page' link so we can jump straight to the first and last page when we want to.
<?php // ... the previous code // creating previous and next link // plus the link to go straight to // the first and last page if ($pageNum > 1) { $page = $pageNum - 1; $prev = " <a href=\"$self?page=$page\">[Prev]</a> "; $first = " <a href=\"$self?page=1\">[First Page]</a> "; } else { $prev = ' '; // we're on page one, don't print previous link $first = ' '; // nor the first page link } if ($pageNum < $maxPage) { $page = $pageNum + 1; $next = " <a href=\"$self?page=$page\">[Next]</a> "; $last = " <a href=\"$self?page=$maxPage\">[Last Page]</a> "; } else { $next = ' '; // we're on the last page, don't print next link $last = ' '; // nor the last page link } // print the navigation link echo $first . $prev . $nav . $next . $last; // and close the database connection
include '../library/closedb.php'; // ... and we're done! ?> Making these navigation link is actually easier than you may think. When we're on the fifth page we just make the 'Previous' link point to the fourth. The same principle also apply for the 'Next' link, we just need to add one to the page number. One thing to remember is that we don't need to print the 'Previous' and 'First Page' link when we're already on the first page. Same thing for the 'Next' and 'Last' link. If we do print them that would only confuse the one who click on it. Because we'll be giving them the exact same page.
for($page = 1; $page <= $maxPage; $page++) { if ($page == $pageNum) { $nav .= " $page "; // no need to create a link to current page } else { $nav .= " <a href=\"$self?page=$page\">$page</a> "; } } // ... the rest here ?> And then modify this one <?php // ... // print the navigation link echo $first . $prev . $nav . $next . $last; // ... ?>
Into this <?php // ... // print the navigation link echo $first . $prev . " Showing page $pageNum of $maxPage pages " . $next . $last; // ... ?> Take a look at the at the result here, and you can get the source code here
In the select query we just select all the columns. You can also use SELECT * instead of mentioning all the column names ( SELECT id, name, address, age, register_date ). But personally i prefer writing the column names in the query so that by reading the code i know what the column names in a table without having to check the database. Example: paging4.php Source code :paging4.phps <?php include 'library/config.php'; include 'library/opendb.php'; // how many rows to show per page $rowsPerPage = 3; // by default we show first page $pageNum = 1; // if $_GET['page'] defined, use it as page number if(isset($_GET['page'])) { $pageNum = $_GET['page']; } // counting the offset $offset = ($pageNum - 1) * $rowsPerPage; $query = "SELECT id, name, address, age, register_date FROM student LIMIT $offset, $rowsPerPage"; $result = mysql_query($query) or die('Error, query failed'); // print the student info in table echo '<table border="1"><tr><td>Student Id</td><td>Name</td><td>Address</td><td>Age</td><td>Register Date</td></tr>'; while(list($id, $name, $address, $age, $regdate) = mysql_fetch_array($result)) { echo "<tr><td>$id</td><td>$name</td><td>$address</td> <td>$age</td><td>$regdate</td></tr>"; } echo '</table>'; echo '<br>'; // ... more code here ?> In this example we print the result in table. Before looping through the array we just echo the starting table code and the header which displays the column names. Then in the loop we just print the values in a HTML table row. The next thing is finding out the total number of rows. There are several ways to do it. The first one is shown below. It's the same method used in previous examples. We just use the COUNT() function Example: paging4.php Source code :paging4.phps <?php // ... previous code here // how many rows we have in database
$query = "SELECT COUNT(id) AS numrows FROM student"; $result = mysql_query($query) or die('Error, query failed'); $row = mysql_fetch_array($result, MYSQL_ASSOC); $numrows = $row['numrows']; // ... just the same code that prints the prev & next link ?> You can also count any other columns since they all yield the same result. So your query can be rewritten into this : <?php // ... $query = "SELECT COUNT(name) AS numrows FROM student"; // ... ?> Or this : <?php // ... $query = "SELECT COUNT(age) AS numrows FROM student"; // ... ?> There is another way to count the total rows. Instead of using COUNT() function in the query you use a simple SELECT <column> and use myql_num_rows() to see how many rows returned. Take a look at the code below. We now separate the query into two parts. One is the normal SELECT query and the second is the SQL that performs the paging. After finish printing the result you can reuse the first part of the query to find the total number of rows. Example: paging5.php Source code :paging5.phps <?php // ... same old code to get the page number and counting the offset $query = "SELECT id, name, address, age, register_date FROM student "; $pagingQuery = "LIMIT $offset, $rowsPerPage"; $result = mysql_query($query . $pagingQuery) or die('Error, query failed'); // ... the code that prints the result in a table // how many rows we have in database $result = mysql_query($query) or die('Error, query failed'); $numrows = mysql_num_rows($result); // ... and here is the code that print the prev & next links ?>
There is another advantage in separating the original query with the paging query. In case you only wish to list all student whose age is older than 15. You just need to modify the original query and you don't have to worry about changing the query to find the total number of rows. The example is shown below : <?php // ... same old code to get the page number and counting the offset $query = "SELECT id, name, address, age, register_date FROM student WHERE age > 15"; $pagingQuery = "LIMIT $offset, $rowsPerPage"; $result = mysql_query($query . $pagingQuery) or die('Error, query failed'); // ... the code that prints the result in a table // how many rows we have in database $result = mysql_query($query) or die('Error, query failed'); $numrows = mysql_num_rows($result); // ... and here is the code that print the prev & next links ?> The disadvantage of this method is that the second execution of mysql_query() will retrieve all columns
from the database. This is very useless since we're not going to use them. We only interested in finding the total rows returned by that query. In the end it's really up to you to decide which method you prefer. To get all the source for this paging examples click here
Table Class
class_id
class_name
Silat
2 3 4
Table Student
student_id
student_name
cid
Uzumaki Naruto
Uchiha Sasuke
Haruno Sakura
Now the content of Table and Student class after the update query are :
Table Class
class_id
class_name
Silat
2 10 4
Table Student
student_id
student_name
cid
Uzumaki Naruto
Uchiha Sasuke
10
Haruno Sakura
You can go as far as creating your own functions in PHP to ensure the data integrity. I have done this before and I hope you don't do it. Save yourself the headache and just write appropriate queries to maintain your data integrity whenever you update / delete rows from a table. This means that whenever you make a query to update / delete always consult your database design to see if you need to update / delete another table to maintain data integrity. Your code will be more portable like this.
$query = "DELETE FROM Student WHERE class_id = 3"; mysql_query($query); $query = "UNLOCK TABLES"; mysql_query($query); The update queries above can be rewritten as : $query = "LOCK TABLES Class WRITE, Student WRITE"; mysql_query($query); $query = "UPDATE Class SET class_id = 10 WHERE class_id = 3"; mysql_query($query); $query = "UPDATE Student SET cid = 10 WHERE cid = 3"; mysql_query($query); $query = "UNLOCK TABLES"; mysql_query($query); By issuing the LOCK TABLES all other users are blocked from reading and writing to the tables. So you're update / delete query will continue to completion without any worries that the intended table already changed by another user
include 'closedb.php'; ?> To restore the backup you just need to run LOAD DATA INFILE query like this : <?php include 'config.php'; include 'opendb.php'; $tableName = 'mypet'; $backupFile = 'mypet.sql'; $query = "LOAD DATA INFILE 'backupFile' INTO TABLE $tableName"; $result = mysql_query($query);
include 'closedb.php'; ?> It's a good idea to name the backup file as tablename.sql so you'll know from which table the backup file is
Whenever you make a form you should not leave it alone without any form validation. Why? Because there is no guarantee that the input is correct and processing incorrect input values can make your application give unpredictable result. You can validate the form input on two places, client side and server side. Client side form validation usually done with javascript. Client side validation makes your web application respond 'faster' while server side form validation with PHP can act as a backup just in case the user switch off javascript support on her browser. And since different browsers can behave differently there is always a possibility that the browser didn't execute the javascript code as you intended.
empty values numbers only input length email address strip html tags
To show form validation with php in action I'll use the contact form in this website. Click here to see the contact form and then take a look at the source code. This contact form requires four input :
csubject.focus(); return false; } else if(trim(cmessage.value) == '') { alert('Please enter your message'); cmessage.focus(); return false; } else { cname.value = trim(cname.value); cemail.value = trim(cemail.value); csubject.value = trim(csubject.value); cmessage.value = trim(cmessage.value); return true; } } function trim(str) { return str.replace(/^\s+|\s+$/g,''); } function isEmail(str) { var regex = /^[-_.a-z0-9]+@(([-_a-z0-9]+\.)+(ad|ae|aero|af|ag| ai|al|am|an|ao|aq|ar|arpa|as|at|au|aw|az|ba|bb|bd|be|bf|bg| bh|bi|biz|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg| ch|ci|ck|cl|cm|cn|co|com|coop|cr|cs|cu|cv|cx|cy|cz|de|dj|dk| dm|do|dz|ec|edu|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb| gd|ge|gf|gh|gi|gl|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn| hr|ht|hu|id|ie|il|in|info|int|io|iq|ir|is|it|jm|jo|jp|ke|kg| kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly| ma|mc|md|mg|mh|mil|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|museum| mv|mw|mx|my|mz|na|name|nc|ne|net|nf|ng|ni|nl|no|np|nr|nt|nu| nz|om|org|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|pro|ps|pt|pw|py|qa| re|ro|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st| su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tm|tn|to|tp|tr|tt|tv|tw|tz| ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za| zm|zw)|(([0-9][0-9]?|[0-1][0-9][0-9]|[2][0-4][0-9]|[2][5][0-5])\.){3}([0-9][09]?|[0-1][0-9][0-9]|[2][0-4][0-9]|[2][5][0-5]))$/i; return regex.test(str); } </script> </head> <body> <form method="post" name="msgform"> <table width="500" border="0" align="center" cellpadding="2" cellspacing="1" class="maincell"> <tr> <td width="106">Your Name</td> <td width="381"><input name="sname" type="text" class="box" id="sname" size="30"></td> </tr> <tr> <td>Your Email</td> <td> <input name="email" type="text" class="box" id="email" size="30"> </td></tr> <tr> <td>Subject</td> <td><input name="subject" type="text" class="box" id="subject" size="30"></td> </tr> <tr> <td>Message</td> <td><textarea name="message" cols="55" rows="10" wrap="OFF" class="box" id="message"></textarea></td> </tr> <tr align="center"> <td colspan="2"><input name="send" type="submit" class="bluebox" id="send" value="Send Message" onClick="return checkForm();"></td> </tr> <tr align="center"> <td colspan="2"> </td> </tr> </table> </form> </body> </html>
Now we'll take a better look at checkForm() function : function checkForm() { var cname, cemail, csubject, cmessage; with(window.document.msgform) { cname = sname; cemail = email; csubject = subject; cmessage = message;
} // ... the rest of the code } In the beginning of the function I use the keyword var to declare four variables to reference the form input . They are cname, cemail, csubject and cmessage. These variables will reference the form input sname, email, subject and message respectively.
Javascript treats a document and it's element as object. The message form is named msgform so to access is we use window.document.msgform and to access the sname input text we can use window.document.msgform.sname. To avoid the hassle of writing the window.document.msgform part whenever we want to access a form object I use the with() keyword. Without it the checkForm() function would look like : function checkForm() { var cname, cemail, csubject, cmessage; cname cemail csubject cmessage = = = = window.document.msgform.sname; window.document.msgform.email; window.document.msgform.subject; window.document.msgform.message;
// ... the rest of the code } Next we'll validate each form input. function checkForm() { // variable declarations goes here ... if(trim(cname.value) == '') { alert('Please enter your name'); cname.focus(); return false; } else if(trim(cemail.value) == '') { alert('Please enter your email'); cemail.focus(); return false; } else if(!isEmail(trim(cemail.value))) { alert('Email address is not valid'); cemail.focus(); return false; } // The rest of validation code goes here ... } To access the value of the name input box we use cname.value. The name values is trimmed to remove extra spaces from the beginning and end of the name. If you do not enter your name or only entering spaces then an alert box will pop up. Using cname.focus() the cursor will be placed to the name input box and then checkForm() return false which cancel the form submit.
The code above uses trim() function. This is not a built in javascript function. I can't understand why there is no trim() function in javascript, even VBScript has it. Anyway it's not a big deal because we can just make our own trim() function. The solution here uses regular expression to replace any spaces in the beginning and end of a string with blank string. function trim(str) { return str.replace(/^\s+|\s+$/g,''); } The forward slash (/) is used to create a regular expression. Note that it is not a string, you don't have to use quotes and it won't work if you use quotes. Let's chop the regular expression notation so we can understand it better :
^ : the beginning of a string $ : end of string. \s : single whitespace character (tab also count as whitespace) + : one or more | : conditional (OR)
if(isset($_POST['send'])) { $sname = $_POST['sname']; $email = $_POST['email']; $subject = $_POST['subject']; $message = $_POST['message']; if(trim($sname) == '') { $errmsg = 'Please enter your name'; } else if(trim($email) == '') { $errmsg = 'Please enter your email address'; } else if(!isEmail($email)) { $errmsg = 'Your email address is not valid'; } else if(trim($subject) == '') { $errmsg = 'Please enter message subject'; } else if(trim($message) == '') { $errmsg = 'Please enter your message'; } // ... more code here ?> The PHP validation is doing the same thing as the javascript validation. It check each value to see if it's empty and if it is we consider that as an error. We also recheck the validity of the email address. When we find an error we set the value of $errmsg. We will print this value so the user can fix the error. If everything is okay the value of $errmsg will be blank. So we continue processing the input. <?php // ... previous validation code if($errmsg == '') { if(get_magic_quotes_gpc()) { $subject = stripslashes($subject); $message = stripslashes($message); }
$to = "email@yourdomain.com"; $subject = '[Contact] : ' . $subject; $msg = "From : $sname \r\n " . $message; mail($to, $subject, $msg, "From: $email\r\nReturn-Path: $email\r\n"); // ... more code here ?> Some web host set the PHP directive magic_quotes_gpc to On which runs addslashes() to every GET, POST, and COOKIE data so we got and extra work to strip the slashes from the input. Because the addslashes() function only add slashes before single quote ( ' ), double quote ( " ), backslash ( \ ) and NULL, we only need to worry about the $subject and $message. This is because (usually ) only these two can contain such characters. However, we can't be sure if magic_quotes_gpc is On or Off so we have to check it's value first using the get_magic_quotes_gpc() function After finishing all that boring job of validating the input we finally come to the last, and the most important step, sending the message using the mail() function. The first parameter we pass to the mail() function is the receiver's email address. The second is the email subject. The third is the message itself and the fourth is an additional headers. I'm sure you already understand the purpose of the first three parameters so I'll just discuss about the fourth one, the additional parameter ( additional headers ) "From: $email\r\nReply-To: $email\r\nReturn-Path: $email\r\n" Each headers are separated by the "\r\n" ( newline ) characters. The first two ( From and Reply-To ) is self explanatory. But what about the third one ( Return-Path )? The reason is some spam filter will check the Return-Path header and compare it with the From header. If these two don't match then the email is considered as spam and you're email won't get delivered ( or sent to the spam folder ). So it's better to play safe and put Return-Path header when we want to send an email to make sure it gets delivered.
That's it. I hope this tutorial can give you a clear idea on validating form, both on client side and server side. But if doesn't, then just use the contact form and let me know about it :-)
I think you should take a quick look what the finished guestbook look like. Just click here to see it.
I have put the SQL query needed to create the table in guestbook.txt. Below is the HTML form code. It's pretty simple, we have text box for name, email and url plus a textarea to hold the message. The submit button is attached with a javascript function because we want to check the input values before the page is submitted. Example :guestbook.php Source code : guestbook.phps, guestbook.txt <form method="post" name="guestform"> <table width="550" border="0" cellpadding="2" cellspacing="1"> <tr> <td width="100">Name *</td> <td> <input name="txtName" type="text" size="30" maxlength="30"></td> </tr> <tr> <td width="100">Email</td> <td> <input name="txtEmail" type="text" size="30" maxlength="50"></td> </tr> <tr> <td width="100">Website URL</td> <td> <input name="txtUrl" type="text" value="http://" size="30" maxlength="50"></td> </tr> <tr> <td width="100">Message *</td> <td> <textarea name="mtxMessage" cols="80" rows="5"></textarea></td> </tr> <tr> <td width="100"> </td> <td> <input name="btnSign" type="submit" value="Sign Guestbook" onClick="return checkForm();"></td> </tr> </table> </form> Below is the javascript code to check the input form. The checkForm() function is called when the "Sign Guestbook" button is clicked. The mandatory fields are name and message so if either is empty we pop an alert box to tell the visitor to enter the name and message. Email is not a mandatory field so we only check if in an email address is
entered but we won't complain if there's none . function checkForm() { // the variables below are assigned to each // form input var gname, gemail, gurl, gmessage; with(window.document.guestform) { gname = txtName; gemail = txtEmail; gurl = txtUrl; gmessage = mtxMessage; } // if name is empty alert the visitor if(trim(gname.value) == '') { alert('Please enter your name'); gname.focus(); return false; } // alert the visitor if email is empty or // if the format is not correct else if(trim(gemail.value) != '' && !isEmail(trim(gemail.value))) { alert('Please enter a valid email address or leave it blank'); gemail.focus(); return false; } // alert the visitor if message is empty else if(trim(gmessage.value) == '') { alert('Please enter your message'); gmessage.focus(); return false; } else { // when all input are correct // return true so the form will submit return true; } } /* Strip whitespace from the beginning and end of a string */ function trim(str) { return str.replace(/^\s+|\s+$/g,''); } /* Check if a string is in valid email format. */ function isEmail(str) { var regex = /^[-_.a-z0-9]+@(([-a-z0-9]+\.)+(ad|ae|aero|af|ag| ai|al|am|an|ao|aq|ar|arpa|as|at|au|aw|az|ba|bb|bd|be|bf|bg|bh| bi|biz|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci| ck|cl|cm|cn|co|com|coop|cr|cs|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz| ec|edu|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gh| gi|gl|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie| il|in|info|int|io|iq|ir|is|it|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr| kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|mg|mh|mil|mk| ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|museum|mv|mw|mx|my|mz|na|name|nc| ne|net|nf|ng|ni|nl|no|np|nr|nt|nu|nz|om|org|pa|pe|pf|pg|ph|pk| pl|pm|pn|pr|pro|ps|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|sd|se|sg|sh| si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tm| tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn| vu|wf|ws|ye|yt|yu|za|zm|zw)|(([0-9][0-9]?|[0-1][0-9][0-9]|[2] [0-4][0-9]|[2][5][0-5])\.){3}([0-9][0-9]?|[0-1][0-9][0-9]|[2] [0-4][0-9]|[2][5][0-5]))$/i; return regex.test(str); }
After the form is submitted our job turns to saving the input into the database. In the code below I include config.php and opendb.php which contain the database configuration and the code needed to open a connection to MySQL. It's a good practice to put these actions in separate file. That way everytime you need to connect to MySQL you can include these files instead of rewriting the code. Also you can change the database information from just one file instead of changing it in every file that use MySQL. To see what the content of config.php, opendb.php and closedb.php go to : Connecting to MySQL database <?php include 'library/config.php';
= = = =
// if the visitor do not enter the url // set $url to an empty string if ($url == 'http://') { $url = ''; } $query = "INSERT INTO guestbook (name, email, url, message, entry_date) VALUES ('$name', '$email', '$url', '$message', current_date)"; mysql_query($query) or die('Error, query failed'); header('Location: ' . $_SERVER['REQUEST_URI']); exit; } ?> The script check if the $_POST['btnSign'] variable is set. If it is then the "Sign Guestbook" button must have been clicked and now we can read name, email, url and message from the $_POST global variable. After that we create an INSERT query string and execute the query using mysql_query(). Sometimes a message can contain single quotes, we need to escape these single quotes ( replacing it with \' ) otherwise MySQL will think that it's the end of a string and the query will fail. We use the addslashes() function to escape the string. Unfortunately some web hosts set the magic_quotes_gpc setting on. This will make values containing single-quotes in $_GET, $_POST and $_COOKIE will be automatically escaped. If we use addslashes() when the string is already escaped the result would be a mess. To check if magic_quotes_gpc is On use get_magic_quotes_gpc(). If it returns true then we don't have to call addslashes(). Ok, now affter all input is ready we can build the query string to enter the name, email, url, message and entry date. Note that for the entry_date field we use current_date. This is not a PHP variable or function, it's a built in MySQL function that returns ( guess what? ) the current date. You also see that I didn't explicitly insert the value of id field. This is because id is set as auto_increment so when we insert a new row into the table a new value for id is automatically generated ( incremented for each new row). After inserting the new guestbook entry the next thing we do is redirect back to current page using header('Location: ' . $_SERVER['REQUEST_URI']); Why? The redirect is just to prevent double submission. Suppose we don't use the redirect and the visitor hit the refresh button after signing up the guestbook then the form will be submitted again. Note : If you get this kind of error message Warning: Cannot modify header information - headers already sent by (output started at C:\webroot\guestbook\library\config.php:7) in C:\webroot\guestbook\guestbook.php on line 43 this mean the redirect failed because you already sent something to the browser. I got the error message above because i "accidentally" have a space right after the closing tag ( ?> ) in config.php. By removing this space the error is fixed.
This kind of errror is actually very common to see when your code is sending headers and fixing it is easy like the example above. Just check the file pointed by the error message and see if you accidentally sent ( print ) anyhing to the browser.
Pheww, we just finished the first part of our guestbook. Now it's time to create the script which will show the guestbook entries. We'll also try to split the entries into multiple pages when they are too much to be shown in one page.
<table width="550" border="1" cellpadding="2" cellspacing="0"> <tr> <td width="80" align="left"> <a href="mailto:<?=$email;?>"> <?=$name;?> </a> </td> <td align="right"><small><?=$date;?></small></td> </tr> <tr> <td colspan="2"> <?=$message;?> <?php if($url != '') { // make the url clickable by formatting it as HTML link $url = "<a href='$url' target='_blank'>$url</a>"; ?> <br> <small>Homepage : <?=$url;?></small> <?php } ?> </td> </tr> </table> <br> <?php } // end while When you just created the guestbook, there are no entry in guestbook table. We use mysql_num_rows()to check how many guestbook entries we have. If mysql_num_rows() returns 0 that means the table is empty and we can print a message saying that the guestbook is empty. If there are already entries in the guestbook we then loop to get all the rows. I use list() to extract the values of a row into the variables $id, $name, $email, $url and $message. An additional step is needed for the $name and $message. For these two we use htmlspecialchars() before printing their value. This function will convert all special characters to HTML entities. As an example suppose I enter the string <b>I am a wizard</b> in the message textarea. After applying htmlspecialchars() it will be converted to <b>I am a wizard</b> What's the point of using htmlspecialchars()? Well, the answer is because some people may try to abuse your guestbook. Some will enter a simple HTML bold formatted message like the example above but some may even try to input a javascript code in the message. As an example I could enter a script like this : <script> while(true) { window.open("http://www.google.com"); } </script> If I don't use htmlspecialchars() and show it as is then when we view the guestbook entries this code will continously open a new window of www.google.com. Won't do any harm if you have a popup blocker ready. But for those unlucky people who haven't got it installed will have their desktop filled with new windows in no time. Very annoying indeed. One more thing added for $message. We also use the function nl2br() to convert any newline characters ( that's \r OR \n OR both ) into HTML break tags ( <br> ). Because web browser "ignores'" newline characters, we need nl2br() to preserve the message formatting. This way if you explicitly enter a three line message it will also be shown as a three line message. Ok, now that we have the values ready we just need to put them in the HTML table. In above example I use <?=$name;?> to print the value of $name. I can also use <?php echo $name; ?>, but it's easier to use the first form. Now we're one step closer to finishing the guestbook. We just need to add a little more code for paging. Surely you don't want to show all the entries in one page. If you have a hundred entries the page will take forever to load. So let's add that little code to split the result into multiple pages.
$offset = ($pageNum - 1) * $rowsPerPage; // prepare the query string $query = "SELECT id, name, email, url, message, DATE_FORMAT(entry_date, '%d.%m.%Y') ". "FROM guestbook ". "ORDER BY id DESC ". "LIMIT $offset, $rowsPerPage"; // ... the rest of the code ?> First we set how many entries we want to show per page ( $rowsPerPage ). We will use this value with the LIMIT keyword in our query so the query will only get a chunk of all entries available. The logic flow is like this. When the page is first loaded the $_GET['page'] is not yet initialized so we use the default $pageNum = 1. We then use $pageNum to count the offset ( the index of the first result we want to show ). As an example, if $pageNum = 1, $offset will be (1 - 1) * 10 = 0. Our limit query will be "LIMIT 0, 10". This will select the first ten entries from our guestbook table. Another example . When $pageNum = 3, $offset = 20, limit query is "LIMIT 20, 10" which will select ten result starting from the 20th index Now that we have the query ready we need to create the navigation link so our visitor can easily move from the first page to other pages. We simply print the page number as a hyperlink. So when a visitor click on a page number the script will show the entries for the specified page. The code needed is shown below <?php // .... previous code $query $result $row $numrows = = = = "SELECT COUNT(id) AS numrows FROM guestbook"; mysql_query($query) or die('Error, query failed'); mysql_fetch_array($result, MYSQL_ASSOC); $row['numrows'];
$maxPage = ceil($numrows/$rowsPerPage); $nextLink = ''; if($maxPage > 1) { $self = $_SERVER['PHP_SELF']; $nextLink = array(); for($page = 1; $page <= $maxPage; $page++) { $nextLink[] = "<a href=\"$self?page=$page\">$page</a>"; } $nextLink = "Go to page : " . implode(' » ', $nextLink); } include 'library/closedb.php'; ?> <table width="550" border="0" cellpadding="2" cellspacing="0"> <tr> <td align="right" class="text"> <?=$nextLink;?> </td> </tr> </table> <?php } ?> First we count the total number of entries we have ( $numrows ) then we find the maximum page numbers. To do this we just need the ceil() function to round the number up. For example, suppose we have 34 entries in our guestbook database and we want to show 10 entries per page. From these numbers we know that we wil split the result in ceil( 34 / 10) = 4 pages. If the entries span in more than one page we do a loop to create the links. The link will look something like this : guestbook.php?page=3 Note that in above code we use $_SERVER['PHP_SELF'] instead of using the filename itself, guestbook.php. This is done to save the trouble of modifying the code if someday we want to change the
filename. We temporarily put the links in an array, $nextLink. Once we get all the links in there we just join them all using implode(). And now our guestbook is done. Congratulations to you :-). If you want the source code for this guestbook tutorial just click here . The zip file contain all the files required but dont' forget to modify library/config.php to match your own settings.
Flood prevention
Prevent the visitor from signing the guestbook over and over again. You can log the visitor's IP and before saving the entry check the database if there's already an entry from such IP in the past hour ( or minute ). You can also use cookie for this
Emoticons
You simply need to replace some special set of characters like :) or :( into an image tag. For example changing :) into <img src="emoticons/smile.gif">
Example : upload.php Source code : upload.phps <form method="post" enctype="multipart/form-data"> <table width="350" border="0" cellpadding="1" cellspacing="1" class="box"> <tr> <td width="246"> <input type="hidden" name="MAX_FILE_SIZE" value="2000000"> <input name="userfile" type="file" id="userfile"> </td> <td width="80"><input name="upload" type="submit" class="box" id="upload" value=" Upload "></td> </tr> </table> </form> An upload form must have encytype="multipart/form-data" otherwise it won't work at all. Of course the form method also need to be set to method="post". Also remember to put a hidden input MAX_FILE_SIZE before the file input. It's to restrict the size of files. After the form is submitted the we need to read the autoglobal $_FILES. In the example above the input name for the file is userfile so the content of $_FILES are like this : $_FILES['userfile']['name'] The original name of the file on the client machine. $_FILES['userfile']['type'] The mime type of the file, if the browser provided this information. An example would be "image/gif". $_FILES['userfile']['size'] The size, in bytes, of the uploaded file. $_FILES['userfile']['tmp_name'] The temporary filename of the file in which the uploaded file was stored on the server. $_FILES['userfile']['error'] The error code associated with this file upload. ['error'] was added in PHP 4.2.0
Example : upload.php Source code : upload.phps <?php if(isset($_POST['upload']) && $_FILES['userfile']['size'] > 0) { $fileName = $_FILES['userfile']['name']; $tmpName = $_FILES['userfile']['tmp_name']; $fileSize = $_FILES['userfile']['size']; $fileType = $_FILES['userfile']['type']; $fp = fopen($tmpName, 'r'); $content = fread($fp, filesize($tmpName)); $content = addslashes($content); fclose($fp); if(!get_magic_quotes_gpc()) { $fileName = addslashes($fileName); } include 'library/config.php'; include 'library/opendb.php'; $query = "INSERT INTO upload (name, size, type, content ) ". "VALUES ('$fileName', '$fileSize', '$fileType', '$content')"; mysql_query($query) or die('Error, query failed'); include 'library/closedb.php'; echo "<br>File $fileName uploaded<br>"; } ?> Before you do anything with the uploaded file. You should not assume that the file was uploaded successfully to the server. Always check to see if the file was successfully uploaded by looking at the file size. If it's larger than zero byte then we can assume that the file is uploaded successfully. PHP saves the uploaded file with a temporary name and save the name in $_FILES['userfile']['tmp_name']. Our next job is to read the content of this file and insert the content to database. Always make sure that you use addslashes() to escape the content. Using addslashes() to the file name is also recommended because you never know what the file name would be. That's it now you can upload your files to MySQL. Now it's time to write the script to download those files.
When you click the download link, the $_GET['id'] will be set. We can use this id to identify which files to get from the database. Below is the code for downloading files from MySQL Database. Example : download.php Source code : download.phps <?php if(isset($_GET['id'])) { // if id is set then get the file with the id from database include 'library/config.php'; include 'library/opendb.php'; $id = $_GET['id']; $query = "SELECT name, type, size, content " . "FROM upload WHERE id = '$id'"; $result = mysql_query($query) or die('Error, query failed'); list($name, $type, $size, $content) = mysql_fetch_array($result); header("Content-length: $size"); header("Content-type: $type"); header("Content-Disposition: attachment; filename=$name"); echo $content; include 'library/closedb.php'; exit; } ?> Before sending the file content using echo first we need to set several headers. They are : 1. header("Content-length: $size") This header tells the browser how large the file is. Some browser need it to be able to download the file properly. Anyway it's a good manner telling how big the file is. That way anyone who download the file can predict how long the download will take. 2. header("Content-type: $type") This header tells the browser what kind of file it tries to download. 3. header("Content-Disposition: attachment; filename=$name"); Tells the browser to save this downloaded file under the specified name. If you don't send this header the browser will try to save the file using the script's name (download.php). After sending the file the script stops executing by calling exit. NOTE : When sending headers the most common error message you will see is something like this : Warning: Cannot modify header information - headers already sent by (output started at C:\Webroot\library\config.php:7) in C:\Webroot\download.php on line 13 This error happens because some data was already sent before we send the header. As for the error message above it happens because i "accidentally" add one space right after the PHP closing tag ( ?> ) in config.php file. So if you see this error message when you're sending a header just make sure you don't have any data sent before calling header(). Check the file mentioned in the error message and go to the line number specified
Next we will use a different method for uploading files. Instead of saving the file content to the database we will store the file in the server and just save the file path.
The HTML form we use is no different with the previous one since the real changes will take place in the PHP codes. Example : upload2.php Source code : upload2.phps <form method="post" enctype="multipart/form-data"> <table width="350" border="0" cellpadding="1" cellspacing="1" class="box"> <tr> <td width="246"> <input type="hidden" name="MAX_FILE_SIZE" value="2000000"> <input name="userfile" type="file" id="userfile"> </td> <td width="80"><input name="upload" type="submit" class="box" id="upload" value=" Upload "></td> </tr> </table> </form> Okay, now let's take a look at the upload process. First we need to specify the directory to store the uploaded files. We store the directory name in $uploadDir. Note that PHP must have write access to $uploadDir or else the upload will fail. If you're web host using a Linux server you may need to set the permission for the upload directory to 777. Example : upload2.php Source code : upload2.phps <?php $uploadDir = 'C:/webroot/upload/'; if(isset($_POST['upload'])) { $fileName = $_FILES['userfile']['name']; $tmpName = $_FILES['userfile']['tmp_name']; $fileSize = $_FILES['userfile']['size']; $fileType = $_FILES['userfile']['type']; $filePath = $uploadDir . $fileName; $result = move_uploaded_file($tmpName, $filePath); if (!$result) { echo "Error uploading file"; exit; } include '../library/config.php'; include '../library/opendb.php'; if(!get_magic_quotes_gpc()) { $fileName = addslashes($fileName); $filePath = addslashes($filePath); } $query = "INSERT INTO upload2 (name, size, type, path ) ". "VALUES ('$fileName', '$fileSize', '$fileType', '$filePath')"; mysql_query($query) or die('Error, query failed : ' . mysql_error()); include '../library/closedb.php'; echo "<br>Files uploaded<br>"; }
?> The key here is the move_uploaded_file() function. This function will move the uploaded files from the temporary upload directory to the location that we earlier ( $uploadDir . $fileName ). If for some reason the functioncannot move the file it will return false and we exit the script because continuing the script is no use.
Downloading
For listing the download files we just need to copy from the previous script. The real difference start when you click on the download link. Example : download2.php Source code : download2.phps if(isset($_GET['id'])) { include '../library/config.php'; include '../library/opendb.php'; $id = $_GET['id']; $query = "SELECT name, type, size, path FROM upload2 WHERE id = '$id'"; $result = mysql_query($query) or die('Error, query failed'); list($name, $type, $size, $filePath) = mysql_fetch_array($result); header("Content-Disposition: attachment; filename=$name"); header("Content-length: $size"); header("Content-type: $type"); readfile($filePath); include '../library/closedb.php'; exit; } After fetching the file info from the database and sending the required headers the next thing we need to do is read the file content from the server and send it to the browser. We can accomplish this by using readfile() function.
The Problems
When using this method of uploading files there are two problems that we need to take care of. They are : 1. Preventing direct access to the uploaded files 2. Handling duplicate file names
<?php // ... same code as before // get the file extension first $ext = substr(strrchr($fileName, "."), 1); // make the random file name $randName = md5(rand() * time()); // and now we have the unique file name for the upload file $filePath = $uploadDir . $randName . '.' . $ext; $result = move_uploaded_file($tmpName, $filePath); // ... same code as before } ?> First we extract the file extension from the file name using strrchr() function combined with substr(). Then using md5() we generate the 32 characters long of random string. It will look something like 7d1d1da5aac5ad72b293165e8e6fe89b. After we join them up we get the new unique file name. This way the chance of stumbling upon a duplicate name problem is very very slim. As for the download part there's no change required. All we did was change the file name on the server so the previous download script ( download2.phps ) will do just fine
That's it. We're done. If you need the source codes for this php upload tutorial you can download them as zip file here. Make sure you change the library/config.php file to match your own settings and change the $uploadDir too if necessary.
id : The article's id title : The title of an article content : The article itself
First we need to create a script to add an article. It is just a form where a user can enter the article's title and content. Example : cms-add.php Source code : cms-add.phps , cms.txt <form method="post"> <table width="700" border="0" cellpadding="2" cellspacing="1" align="center"> <tr> <td width="100">Title</td> <td><input name="title" type="text"></td> </tr> <tr> <td width="100">Content</td> <td><textarea name="content" cols="50" rows="10"></textarea></td> </tr> <tr> <td width="100"> </td> <td> </td> </tr> <tr> <td colspan="2" align="center"><input name="save" type="submit" value="Save Article"></td> </tr> </table> </form> Whe an article is added the script just insert the article into the database. An article id is automatically generated by MySQL because the id column was created with AUTO_INCREMENT parameter . <?php if(isset($_POST['save'])) { $title = $_POST['title']; $content = $_POST['content']; if(!get_magic_quotes_gpc()) { $title = addslashes($title); $content = addslashes($content); } include 'library/config.php'; include 'library/opendb.php'; $query = " INSERT INTO news (title, content) ". " VALUES ('$title', '$content')"; mysql_query($query) or die('Error ,query failed'); include 'library/closedb.php'; echo "Article '$title' added"; } ?>
Now that we have the script to add articles let's create another script to view those articles. The script is list the title of articles available in database as clickable links. The article link have the article id appended like this http://www.php-mysql-tutorial.com/examples/cms/article1.php?id=3
One possible implementation of article1.php is presented below : Example : article1.php Source code : article1.phps <?php include 'library/config.php'; include 'library/opendb.php'; // if no id is specified, list the available articles if(!isset($_GET['id'])) { $self = $_SERVER['PHP_SELF']; $query = "SELECT id, title FROM news ORDER BY id"; $result = mysql_query($query) or die('Error : ' . mysql_error()); // create the article list $content = '<ol>'; while($row = mysql_fetch_array($result, MYSQL_NUM)) { list($id, $title) = $row; $content .= "<li><a href=\"$self?id=$id\">$title</a></li>\r\n"; } $content .= '</ol>'; $title = 'Available Articles'; } else { // get the article info from database $query = "SELECT title, content FROM news WHERE id=".$_GET['id']; $result = mysql_query($query) or die('Error : ' . mysql_error()); $row = mysql_fetch_array($result, MYSQL_ASSOC); $title = $row['title']; $content = $row['content']; } include 'library/closedb.php'; ?> // ... more code here When article1.php is first called the $_GET['id'] variable is not set and so it will query the database for the article list and save the list in the$content variable as an ordered list. The variable $title and $content will be used later when we print the result page. Take a look at the code below : Example : article1.php Source code : article2.phps <?php // ... previous code ?> <html> <head> <title> <?php echo $title; ?> </title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <style type="text/css"> // ... some css here to make the page look nicer </style> </head> <body> <table width="600" border="0" align="center" cellpadding="10" cellspacing="1" bgcolor="#336699"> <tr> <td bgcolor="#FFFFFF"> <h1 align="center"><?php echo $title; ?></h1> <?php echo $content; // when displaying an article show a link // to see the article list if(isset($_GET['id'])) { ?> <p> </p> <p align="center"><a href="<?php echo $_SERVER['PHP_SELF']; ?>">Article List</a></p> <?php } ?> </td>
</tr> </table> </body> </html> If you click on an article link the script will fetch the article's title and content from the database, save it to $title and $content variable and print the HTML file . At the bottom of the page we place a code to show the link to the article list which is the file itself without any query string ( $_SERVER['PHP_SELF'] ) With this implementation each article request involve one database query. For a heavy load website with lots of articles using the above implementation can cause a very high amount of database-request. So we need a better cms solution to reduce the load. One feasible solution is to implement caching ( cache ) which load an article from the database only once when the article was first requested. The article is then saved to a cache directory as a regular HTML file. Subsequent request to the article will no longer involve any database request. The script just need to read the requested article from the cache directory. Example : article2.php Source code : article2.phps <?php include 'library/config.php'; include 'library/opendb.php'; $cacheDir = dirname(__FILE__) . '/cache/'; if (isset($_GET['id'])) { $cacheFile = $cacheDir . '_' . $_GET['id'] . '.html'; } else { $cacheFile = $cacheDir . 'index.html'; } if (file_exists($cacheFile)) { header("Content-Type: text/html"); readfile($cacheFile); exit; } // ... more code coming ?>
First we need to specify the cache directory where all cache files are located. For this example the cache directory is located in the same place as the article2.php script. I mean if article2.php is stored in C:/webroot then the cache dir is in C:/webroot/cache/ The script thent check if the article was already in the cache. An article is saved into the cache directory using a filename generated from it's id. For example if you request the article using a link like this : http://www.php-mysql-tutorial.com/examples/cms/article2.php?id=3 Then the cache file for the article is _3.html This filename is just an underscore ( _ ) followed by the article id. In case article2.php is called like this : http://www.php-mysql-tutorial.com/examples/cms/article2.php no id is defined so we make the cache file name as index.html If the cache file is found , the content is read and printed using readfile() and the script terminate. When the article is not found in the cache then we need to look in the database and get the page content from there. Example : article2.php Source code : article2.phps <?php // ... previous code
$query = "SELECT id, title FROM news ORDER BY id"; $result = mysql_query($query) or die('Error : ' . mysql_error()); $content = '<ol>'; while($row = mysql_fetch_array($result, MYSQL_NUM)) { list($id, $title) = $row; $content .= "<li><a href=\"$self?id=$id\">$title</a></li>\r\n"; } $content .= '</ol>'; $title = 'Available Articles'; } else { // get the article info from database $query = "SELECT title, content FROM news WHERE id=".$_GET['id']; $result = mysql_query($query) or die('Error : ' . mysql_error()); $row = mysql_fetch_array($result, MYSQL_ASSOC); $title = $row['title']; $content = $row['content']; } include 'library/closedb.php'; // ... still more code coming ?> As you can see above the process of fetching the article list and content is the same as article1.php. But before showing the page we have to start output buffering so we can save the content of the generated HTML file. See the code below. Just before printing the html we callob_start() to activate output buffering. From this point no output is sent from the script to the browser. So in the code example below anything between <html> and </html> tag is not sent to the browser but stored in an internal buffer first. After the closing html tag we useob_get_contents() to get the buffer content and store int in a temporary variable, $buffer. We then call ob_end_flush() which stop the output buffering ( so the page is now sent to the browser ). Example : article2.php Source code : article2.phps <?php // ... previous code ob_start(); ?> <html>
</html> <?php // get the buffer $buffer = ob_get_contents(); // end output buffering, the buffer content // is sent to the client ob_end_flush(); // now we create the cache file $fp = fopen($cacheFile, "w"); fwrite($fp, $buffer); fclose($fp); ?>
Now that we have the file content we can write the cache file using the filename generated earlier ( using underscore plus the article id ). From now on any request to the article will no longer involve a database query. At least until the article is updated.
Next we will need an admin page for our content management system. It is where we can edit and delete the articles.
Delete Article
Go the admin page, and try to delete an article ( if there's no article create one first ). When you want to delete an article, a little javascript confirmation window will pop up. You should always put this kind of confirmation when trying to delete something. Just to make sure you don't delete by accident. Example : cms-admin.php Source code : cms-admin.phps <?php include 'library/config.php'; include 'library/opendb.php'; if(isset($_GET['del'])) { $query = "DELETE FROM news WHERE id = '{$_GET['del']}'"; mysql_query($query) or die('Error : ' . mysql_error());
// then remove the cached file $cacheDir = dirname(__FILE__) . '/cache/'; $cacheFile = $cacheDir . '_' . $_GET['id'] . '.html'; @unlink($cacheFile); // and remove the index.html too because the file list // is changed @unlink($cacheDir . 'index.html'); // redirect to current page so when the user refresh this page // after deleting an article we won't go back to this code block header('Location: ' . $_SERVER['PHP_SELF']); exit; } // ... more code here ?> Deleting an article is a simple process. When you click on an 'Delete' link. The admin page will reload to something like : cms-admin.php?del=3 where 3 is the article id that we want to delete.
Once we have the article id we just need to execute a delete query and article will be deleted from the database. Then we delete the cache file ( if it exists ) and the index.html file too because the file list already changed. In the code above we use an @ before the unlink() command. This is to supress any error message in case the cache file is not found ( i.e. the article is never requested before / recently updated ) After that we reload the admin page ( omitting the 'del' query string ). We need to do this because if the admin is refreshing the page we will go back executing the delete query again. It's a waste of database resource. Here is the rest of admin page code. We just fetch the article list from the database, print the article title and add some link to view, edit, delete and add article. Example : cms-admin.php Source code : cms-admin.phps // ... previous code <html> <head> <title>Admin Page For Content Management System (CMS)</title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <script language="JavaScript"> function delArticle(id, title) { if (confirm("Are you sure you want to delete '" + title + "'")) { window.location.href = 'cms-admin.php?del=' + id;
} } </script> </head> <body> <?php $query = "SELECT id, title FROM news ORDER BY id"; $result = mysql_query($query) or die('Error : ' . mysql_error()); ?> <table width="600" border="0" align="center" cellpadding="5" cellspacing="1" bgcolor="#999999"> <tr align="center" bgcolor="#CCCCCC"> <td width="500"><strong>Title</strong></td> <td width="150"><strong>Action</strong></td> </tr> <?php while(list($id, $title) = mysql_fetch_array($result, MYSQL_NUM)) { ?> <tr bgcolor="#FFFFFF"> <td width="500"> <?php echo $title;?> </td> <td width="150" align="center"> <a href="article2.php?id=<?php echo $id;?>" target="_blank">view</a> | <a href="cms-edit.php?id=<?php echo $id;?>">edit</a> | <a href="javascript:delArticle('<?php echo $id;?>', '<?php echo $title;?>');">delete</a></td> </tr> <?php } include 'library/closedb.php'; ?> </table> <p align="center"><a href="cms-add.php">Add an article</a></p> </body> </html>
Edit Article
To complete our content management sytem we'll make the edit page. It's interface is basically the same with cmsadd.php but we just need to add some code to fetch the article information from database. One important thing to remember when editing a page is that the page content may contain html tags or even javascript tags. If we put the content as is in the textarea wrong stuff could happen. To prevent this we need to use htmlspecialchars(). This function will change special HTML characters like < and > into < and >. If you're curious what could happen when the content are put as is, go ahead and try removing the htmlspecialchars() code and see the result.. Example : cms-edit.php Source code : cms-edit.phps <html> <head> <title>Edit An Article</title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <style type="text/css"> <!-.box { font-family: Arial, Helvetica, sans-serif; font-size: 12px; border: 1px solid #000000; } --> </style> </head> <body> <?php include 'library/config.php'; include 'library/opendb.php'; if(isset($_GET['id'])) { $query = "SELECT id, title, content ". "FROM news ". "WHERE id = '{$_GET['id']}'"; $result = mysql_query($query) or die('Error : ' . mysql_error()); list($id, $title, $content) = mysql_fetch_array($result, M); $content = htmlspecialchars($content); }
MYSQL_NU
else if(isset($_POST['save'])) { $id = $_POST['id']; $title = $_POST['title']; $content = $_POST['content']; if(!get_magic_quotes_gpc()) { $title = addslashes($title); $content = addslashes($content); } // update the article in the database $query = "UPDATE news ". "SET title = '$title', content = '$content' ". "WHERE id = '$id'"; mysql_query($query) or die('Error : ' . mysql_error()); // then remove the cached file $cacheDir = dirname(__FILE__) . '/cache/'; $cacheFile = $cacheDir . '_' . $_GET['id'] . '.html'; @unlink($cacheFile); // and remove the index.html too because the file list // is changed @unlink($cacheDir . 'index.html'); echo "Article '$title' updated"; // now we will display $title & content // so strip out any slashes $title = stripslashes($title); $content = stripslashes($content); } include 'library/closedb.php'; ?> <form method="post"> <input type="hidden" name="id" value="<?=$id;?>"> <table width="700" border="0" cellpadding="2" cellspacing="1" class="box"> <tr> <td width="100">Title</td> <td><input name="title" type="text" class="box" id="title" value="<?=$title;?>"></td> </tr> <tr> <td width="100">Content</td> <td><textarea name="content" cols="50" rows="10" class="box" id="content"><?=$content;?></textarea></td> </tr> <tr> <td width="100"> </td> <td> </td> </tr> <tr> <td colspan="2" align="center"><input name="update" type="submit" class="box" id="update" value="Update Article"></td> </tr> </table> <p align="center"><a href="cms-admin.php">Back to admin page</a></p> </form> </body> </html> When the form is submitted we begin the updating process. First we use an UPDATE query to modify the article in the database. Then we remove the cache file and the index.html because the content of these two files may no longer accurate. We also use an @ before the unlink command to supress any error message. You may already notice this but we already use the $cacheDir variable in three scripts ( article2.php, cmsadmin.php, cms-edit.php ). It's a good idea to put this variable in the config.php file. So if we want to specify a different cache directory we only need to change the variable in one file instead of three.
Okay, now that we have completed the tutorial maybe you start thinking that this system is too simple. If you do think so, you are absolutely right :-). There are a whole bunch of great content management system solutions out there. Mambo and Website Baker are two of them. These two are really cool ( and free ). Mambo has lots of features, it is really a complete content management system solution. Website Baker is similar to Mambo but it's simpler and easier to use and learn. I suggest you get one of those two, use it and learn from the source code. You really can learn a lot from it. By the way all the files needed for this cms tutorial can be downloaded here
</table> </form> </body> </html> Nothing sophisticated in that form. It's just a basic login form with two input for entering the user id and password. Make sure that the form method is set to post since we certainly don't want to show up the user id and password in the address bar. Right before the login form we there's a code for printing an error message. We can ignore this for now since we'll be talking about it shortly. Once we submit the form we can start the authentication process. We simply check if the user id and password exist in $_POST and check if these two match the hardcoded user id and password. Example : basic/login.php Source : basic/login.phps <?php // we must never forget to start the session session_start(); $errorMessage = ''; if (isset($_POST['txtUserId']) && isset($_POST['txtPassword'])) { // check if the user id and password combination is correct if ($_POST['txtUserId'] === 'theadmin' && $_POST['txtPassword'] === 'chumbawamba') { // the user id and password match, // set the session $_SESSION['basic_is_logged_in'] = true; // after login we move to the main page header('Location: main.php'); exit; } else { $errorMessage = 'Sorry, wrong user id / password'; } } ?> // ... here is the login form shown previously But before we start matching the user id and password. We must start the session first. Never forget to start the session before doing anything to the session since it won't work. You can see above that the hardcoded user id and password are "theadmin" and "chumbawamba". If the submitted user id and password match these two then we set the value of $_SESSION['basic_is_logged_in'] to true. After that we move the application's main page. In this case it's called main.php If the user id and password don't match we set the error message. This message will be shown on top of the login form. Note : When starting the session you may stumble upon this kind of error : Warning: session_start(): Cannot send session cache limiter - headers already sent (output started at C:\Webroot\examples\user-authentication\basic\login.php:1) in C:\Webroot\examples\userauthentication\basic\login.php on line 3 PHP will spit this error message if the script that call session_start() already send something ( a blank space, newline, etc ). The error above happen when i add a single space on the first line right before the php opening tag ( <?php ). Thankfully the error message shows where the output started so fixing this kind of error is simple. After removing that extra space the error is fixed.
// is the one accessing this page logged in or not? if (!isset($_SESSION['basic_is_logged_in']) || $_SESSION['basic_is_logged_in'] !== true) { // not logged in, move to login page header('Location: login.php'); exit; } ?> <html> <head> <title>Main User Page</title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> </head> <body> <p>This is the main application page. You are free to play around here since you are an autenthicated user :-) </p> <p> </p> <p><a href="logout.php">Logout</a> </p> </body> </html> A little note about naming a session variable. As you can see the session that we used to mark whether a user is logged in or not is named 'basic_is_logged_in'. When setting a name for a session variable it's a good thing to use the application name as the prefix. In this case the prefix is 'basic_' . This is especially important when you have multiple application on one site where each requires different login information. For example, suppose we have a cms application and a link exchange application where each have their own user authentication system. In both application we use the session variable $_SESSION['is_logged_in']. In this case if we already logged in in the cms application we will no longer be required to login in the link exchange application since both are using the same session name. This is usually not an intended feature. To avoid that kind of thing we can instead use $_SESSION['cms_is_logged_in'] and $_SESSION['exchange_is_logged_in']
Next we will make a better login method where the user info is stored in the database
// ... same html login form as previous example Instead of checking the user id and password against a hardcoded info we query the database if these two exist in the database using the SELECT query. If we found a match we set the session variable and move to the main page. Note that the session name is prefixed by 'db_' to make it different than the previous example. For the next two scripts ( main.php and logout.php ) the code is similar to previous one. The only difference is the session name. Here is the code for these two Example : database/main.php Source : database/main.phps <?php session_start(); // is the one accessing this page logged in or not? if (!isset($_SESSION['db_is_logged_in']) || $_SESSION['db_is_logged_in'] !== true) { // not logged in, move to login page header('Location: login.php');
exit; } ?> // ... some html code here Example : database/logout.php Source : dabase/logout.phps <?php session_start(); // if the user is logged in, unset the session if (isset($_SESSION['db_is_logged_in'])) { unset($_SESSION['db_is_logged_in']); } // now that the user is logged out, // go to login page header('Location: login.php'); ?>
The next part is the most interesting one of the authentication methods. We will display an image and the user must enter the random number displayed in the image to complete the login process.
// send the content type header so the image is displayed properly header('Content-type: image/jpeg'); // send the image to the browser imagejpeg($image); // destroy the image to free up the memory imagedestroy($image); ?> To create a five digit random number we use rand() function and specify that the random number must be between 10000 and 99999. We put the hash value of this random number in the session. This hash value will be used by the login script to check if the entered number is correct. Next we create a small image, 60 x 30 pixels, using imagecreate(). We set the background color to white ( RGB = 255, 255, 255 ) using imagecolorallocate() function. Note that the first call to imagecolorallocate() will always set the background color for the image. Then we set the text color as black ( RGB = 0, 0, 0 ). Feel free to change the color text to your liking. To print the random number to the image we use the function imagestring(). In the script above we call this function like this : imagestring ($image, 5, 5, 8, $rand, $textColor); The first argument passed to this function is the image handler ( $image ). The second one ( 5 ) is the font. You can choose from one to five where one is the smallest font. The third and fourth parameter is the horizontal and vertical coordinate where we will print the image. The top left corner is defined as 0, 0. The last one is the text color which is black as mentioned earlier. After we got the image ready we can now send it to the browser. But before doing that we must set several headers to make sure that the image is not cached. If the image is cached then the login form will show the same image even if you refresh it. That will cause a problem since the random number is always different. Finally after everything is set we send the image to the browser using imagejpeg() and to free the memory we use imagedestroy().
To check if the login information is correct we first check if the entered number is the same one as displayed in the image. To do this we check the hash of the entered number and see if it match the one saved in the session. If the number don't match we just set an error message. If the number do match we continue checking the given user id and password just like the previous example. If the userid and password combination is correct we set $_SESSION['image_is_logged_in'] to true and move on to the main page Example : image-verification/login.php Source : image-verification/login.phps <?php // we must never forget to start the session session_start(); $errorMessage = ''; if (isset($_POST['txtUserId']) && isset($_POST['txtPassword'])) { // first check if the number submitted is correct $number = $_POST['txtNumber']; if (md5($number) == $_SESSION['image_random_value']) { include 'library/config.php'; include 'library/opendb.php'; $userId = $_POST['txtUserId']; $password = $_POST['txtPassword'];
// check if the user id and password combination exist $sql = "SELECT user_id FROM tbl_auth_user WHERE user_id = '$userId' AND user_password = PASSWORD('$password')"; $result = mysql_query($sql) or die('Query failed. ' . mysql_error()); if (mysql_num_rows($result) == 1) { // the user id and password match, // set the session $_SESSION['image_is_logged_in'] = true; // remove the random value from session $_SESSION['image_random_value'] = ''; // after login we move to the main page header('Location: main.php'); exit; } else { $errorMessage = 'Sorry, wrong user id / password'; } include 'library/closedb.php'; } else { $errorMessage = 'Sorry, wrong number. Please try again'; } } ?> We don't need to discuss about main.php and logout.php since they are the same as previous example except the session name is now called $_SESSION['image_is_logged_in']. So instead of working on those two files let's move on to a more interesting stuff...
We start by defining the characters that we want to use in the verification code. For this example we use upper case alphabet plus numbers. The code is generated using the combination of str_shuffle() and substr() function. Using str_shuffle() we jumble all the characters in $aplhanum and then using substr() we take just the first five characters. The result will look something like "D79ZG". Just run the example and see it for yourself. The second improvement is by using background images. Maybe you already know this but there are software/scripts that can extract the characters displayed as images. And if the verification image only use plain background color identifying the characters wil be quite easy. For this reason we will make the verification code displayed on a background image. In this tutorial we will only use four different background images. You can add as many background images as you want in your own code. Here are the background images :
Okay that is it. The three method of user authentication. Just pick one that fit your application. Btw, all the code for the three methods can be downloaded here
Add New Album Album List Edit & Delete Album Add Image Image List Edit & Delete Image
And the visitor page contain these :
Database Design
For a simple image gallery like this we only need two tables, tbl_album and tbl_image. Here is the SQL to create these tables Source code : image-gallery.sql CREATE TABLE tbl_album ( al_id INT NOT NULL AUTO_INCREMENT, al_name VARCHAR(64) NOT NULL, al_description TEXT NOT NULL, al_image VARCHAR(64) NOT NULL, al_date DATETIME NOT NULL, PRIMARY KEY(al_id) );
CREATE TABLE tbl_image ( im_id INT NOT NULL AUTO_INCREMENT, im_album_id INT NOT NULL, im_title VARCHAR(64) NOT NULL, im_description TEXT NOT NULL, im_type VARCHAR(30) NOT NULL, im_image VARCHAR(60) NOT NULL, im_thumbnail VARCHAR(60) NOT NULL, im_date DATETIME NOT NULL, PRIMARY KEY(im_id) );
Directory Layout
The image below show the file organization for the image gallery
The images directory is where we kept all of the images. The image icons are stored in the thumbnail sub-directory uder the gallery. Please remember to set write access to the album, gallery, and thumbnail directories otherwise the gallery script will not be able to save the images.
Configurations
There are some constants in the config file that you should change :
1. ALBUM_IMG_DIR, GALLERY_IMG_DIR These are the absolute path to the images directories 2. THUMBNAIL_WIDTH The PHP script will create a thumbnail ( icons ) for each image that you upload. In addition when you add an album image that image will also resized automatically. One more note. If you want to test this gallery on your own computer please make sure you already have GD library installed. To check if GD library is installed on your system save the following code and run it. <?php if (function_exists('imagecreate')) { echo 'OK, you already have GD library installed'; } else { echo 'Sorry, it seem that GD library is not installed/enabled'; } ?> After taking care of the configurations we'll take a quick tour on the admin page
Take a look at the code snippet below. Source code : admin/index.phps // ... some code here $page = (isset($_GET['page']) && $_GET['page'] != '') ? $_GET['page'] : 'list-album'; $allowedPages = array('list-album', 'add-album', 'album-detail', 'modify-album', 'list-image', 'add-image', 'image-detail', 'modify-image'); if (in_array($page, $allowedPages)) { include $page . '.php'; } else { ?> <table width="100%" border="0" align="center" cellpadding="2" cellspacing="1"> <tr> <td width="30" align="center"><strong>Error : The Page You're Looking For Doesn't Exist</strong></td> </tr> </table> <?php } ?> To decide which page to include index.php will look up the $_GET variable to see if $_GET['page'] is available. For example the url for the image list page is "index.php?page=list-image". So in this case the value of $_GET['page'] is "list-image" so the file we need to include is "list-image.php". If $_GET['page'] is empty or doesn't exist then we'll just use "list-album" as the default page. Before including any file we first check if the intended file is in our list of allowed pages. You see, the bad thing about including other pages is that it can cause serious security problem if you don't take some precautions. Here's an example. Suppose an evil ( or nosy ) guy decided to enter this url "index.php?page=../../../super-secret-file", then without checking the requested page against the list of allowed pages you could expose some secret code to him or even worse. In the code above you can see that if a user is requesting a page which is not in the list we just display an error message. We don't check if the file actually exist or not. If it's not in the allowed list then we will never include it.
Okay, now let's start making the image gallery from the first part, adding a new album
And here is the code snippet : Example : admin/add-album.php Source code : admin/add-album.phps require_once '../library/config.php'; require_once '../library/functions.php'; if(isset($_POST['txtName'])) { $albumName = $_POST['txtName'];
$albumDesc = $_POST['mtxDesc']; $imgName = $_FILES['fleImage']['name']; $tmpName = $_FILES['fleImage']['tmp_name']; // we need to rename the image name just to avoid // duplicate file names // first get the file extension $ext = strrchr($imgName, "."); // then create a new random name $newName = md5(rand() * time()) . $ext; // the album image will be saved here $imgPath = ALBUM_IMG_DIR . $newName; // resize all album image $result = createThumbnail($tmpName, $imgPath, THUMBNAIL_WIDTH); if (!$result) { echo "Error uploading file"; exit; } if (!get_magic_quotes_gpc()) { $albumName = addslashes($albumName); $albumDesc = addslashes($albumDesc); } $query = "INSERT INTO tbl_album (al_name, al_description, al_image, al_date) VALUES ('$albumName', '$albumDesc', '$newName', NOW())"; mysql_query($query) or die('Error, add album failed : ' .
mysql_error());
Since we save the images as files instead inserting them to the database we need to make sure there won't be any name duplication problem. To prevent this we just generate some random name for every images that we upload. Take a look at code below : $ext = strrchr($imgName, "."); $newName = md5(rand() * time()) . $ext; The first line is to extract the file extension from the file name. As an example let say we upload an image named "hyperalbum.jpg". Then strrchr("hyperalbum.jpg", ".") will return ".jpg". On the second line we generate a random number using rand() multiply it with current time and generate the hash code using md5(). It is a very common practice to use the combination of md5(), rand() and time() functions to generate a random name. After we append the file extension to the new name we can then use it to save the uploaded image. But before we save the image we need to resize the image if it's too large. As you can see in the album list we only need small images for the album icons. To make the thumbnail we use createThumbnail() function defined in functions.php . Once everything is saved we print a little javascript code to go to the album list page. Note that we cannot simply use header("Location: index.php?page=list-album") to redirect to the album list page since a call to header() will only have an effect when no other output in sent before the call.
In the "Images" column you can see how many images contained in an album. If you click on the number you will go to the image list so can see all the images in a particular album. And i'm sure i don't need to explain what that button with "Add Album" written on it does. If you re-read the sql containing the table definitions of this gallery you can see that tbl_album doesn't contain any column storing the number of images in it. That number is the result of the left join in the sql query. You can see the sql code below. $sql = "SELECT al_id, al_name, al_image, COUNT(im_album_id) AS al_numimage FROM tbl_album al LEFT JOIN tbl_image im ON al.al_id = im.im_album_id GROUP by al_id ORDER BY al_name "; In this query we must use LEFT JOIN instead of INNER JOIN because an album can have zero image in it. If we use INNER JOIN then the empty albums will not be shown in the list. Now, if you right click on an album icon and view it's properties you can see that the url fo the icon is pointing to a PHP script instead of an image. The url look like this : viewImage.php?type=album&name=3b6a267a967d7535ff3b1ebc3d9e3c1e.jpg In this image gallery whenever we would want to display an album or image icon or the full-size image we always use the viewImage.php file instead of linking to the actual image. There are several reasons to do this. The first is so you could move the images directory outside of your web root to prevent leechers from taking all the images. The image gallery in our example doesn't do this. You could go to the images directory and list all the images in the gallery. If you set the value of ALBUM_IMG_DIR and GALLERY_IMG_DIR to a directory outside your webroot then you can prevent this. For example if your web root is /home/myname/public_html you can set ALBUM_IMG_DIR to /home/myname/images/album and GALLERY_IMG_DIR to /home/myname/images/gallery/. The second reason is that you may want to restrict the access your gallery. For example the visitors must login before they can see the images. In viewImage.php you could check for the session variable to determine that. So if the visitors hasn't login yet you just display some blank or warning images You can checkout the code here. It's really a simple script which requires two inputs. The type of image you wish to display ( album icon, image icon or full size image ) and the image file name. Then we only need to set the appropriate headers, read the image file and send it to the browser. Next we'll see how to modify & delete an album.
We display the album thumbnail here just to make sure we're updating the right album and because i think the form is way too dull without any images. After you hit the "Modify" button we just need to save the new name & description to the database. However, before we changing the thumbnail we must first check if a new thumbnail image is provided or not. Here is the code snippet : Source : modify-album.phps // ... some code here if(isset($_POST['txtName'])) { $albumId = $_POST['hidAlbumId']; $albumName = $_POST['txtName']; $albumDesc = $_POST['mtxDesc']; if (!get_magic_quotes_gpc()) { $albumName = addslashes($albumName); $albumDesc = addslashes($albumDesc); } if ($_FILES['fleImage']['tmp_name'] != '') { $imgName = $_FILES['fleImage']['name']; $tmpName = $_FILES['fleImage']['tmp_name']; // just like when we add this album // we will need to rename the image name to avoid // duplicate file name problem $newName = md5(rand() * time()) . strrchr($imgName, "."); // resize the new album image $result = createThumbnail($tmpName, ALBUM_IMG_DIR . $newName, THUMBNAIL_WIDTH); if (!$result) { echo "Error uploading file"; exit; } // since a new image for this album is specified // we'll need to delete the old one $sql = "SELECT al_image FROM tbl_album WHERE al_id = $albumId"; $result = mysql_query($sql) or die('Error, get album info failed. ' . mysql_error()); $row = mysql_fetch_assoc($result); unlink(ALBUM_IMG_DIR . $row['al_image']); $newName = "'$newName'"; } else { // don't change the image $newName = "al_image"; } $query = "UPDATE tbl_album SET al_name = '$albumName', al_description = '$albumDesc', al_image = $newName WHERE al_id = $albumId";
mysql_query($query) or die('Error, modify album failed : ' . mysql_error()); // after saving the modification go to the detail page echo "<script>window.location.href='index.php?page=album-detail&alId=$albumId';</script>"; } // ... more code down here As you can see on the code above we update the album thumbnail only if a new image is provided. The process is no different than when we add a new album. After the new image is uploaded and the thumbnail is created successfully we then delete the old one using unlink(). That's the php function you'll need when you want to delete a file. Now. you need to take a look at the above code again but this time focus on this little piece // ... some code here if ($_FILES['fleImage']['tmp_name'] != '') { // ... upload the new image & delete the old one $newName = "'$newName'"; } else { $newName = "al_image"; } $query = "UPDATE tbl_album SET al_name = '$albumName', al_description = '$albumDesc', al_image = $newName WHERE al_id = $albumId"; // ... and more down here If you're wondering why in the if block $newName is surrounded by single quotes but in the else block $newName is given the value "al_image" without the single quotes, here's the explanation. Note that in the sql query string $albumName and $albumDesc is surrounded by quotes but $newName isn't. We need to do this so that when the image is not changed we can set the query to leave the image alone. Here's an example so you can understand it better. For the first scenario imagine that the admin update an album ( id = 123 ) and modify it's name to "my new album", description is changed to "my new description" and he also give a new image and it is saved under the new name "3b6a267a967d7535ff3b1ebc3d9e3c1e.jpg". The script will go through the if block because the value of $_FILES['fleImage']['tmp_name'] won't be empty and then the value of $query will be ... UPDATE tbl_album SET al_name = 'my new album', al_description = 'my new description', al_image = '3b6a267a967d7535ff3b1ebc3d9e3c1e.jpg' WHERE al_id = 123 ... which will update the image information including the image name. For the second scenario the admin only change the name and description but leave the image alone.Now the script will go to the else block and the value of $query will be ... UPDATE tbl_album SET al_name = 'my new album', al_description = 'my new description', al_image = al_image WHERE al_id = 123 ... which will update the album name and description but leave the old image name as it is. This is certainly not the only way to do it. You can easily add an if else statement and make a new query for each scenario. I'm just showing an alternative.
Delete Album
You can delete an album by clicking the "Delete" link on the album list. You'll see a confirmation box to make sure you really want to delete an album because when you delete album all images are deleted also. The code for deleting an album is located in the index.php file. The code flow is like this : 1. Get the album id 2. Make a query to get the name of that album and the thumbnail filename. Print an error message if the album doesn't exist 3. If the album exist get images file name and delete the images plus the album icon 4. Delete the album data and the images from the database Here is the code Source code : admin/index.phps // ... some code on top
if (isset($_GET['deleteAlbum']) && isset($_GET['album']) ) { $albumId = $_GET['album']; // get the album name since we need to display // a message that album 'foo' is deleted $result = mysql_query("SELECT al_name, al_image FROM tbl_album WHERE al_id = $albumId") or die('Delete image failed. ' . mysql_error()); if (mysql_num_rows($result) == 1) { $row = mysql_fetch_assoc($result); $albumName = $row['al_name']; $albumImage = $row['al_image']; // get the image filenames first so we can delete them // from the server $result = mysql_query("SELECT im_image, im_thumbnail FROM tbl_image WHERE im_album_id = $albumId") or die(mysql_error()); while ($row = mysql_fetch_assoc($result)) { unlink(GALLERY_IMG_DIR . $row['im_image']); unlink(GALLERY_IMG_DIR . 'thumbnail/' . $row['im_thumbnail']); } unlink(ALBUM_IMG_DIR . $albumImage); $result = mysql_query("DELETE FROM tbl_image WHERE im_album_id = $albumId") or die('Delete image failed. ' . mysql_error()); $result = mysql_query("DELETE FROM tbl_album WHERE al_id = $albumId") or die('Delete album failed. ' . mysql_error()); // album deleted successfully, let the user know about it echo "<p align=center>Album '$albumName' deleted.</p>"; } else { echo "<p align=center>Cannot delete a non-existent album.</p>"; } } // ... and some more on the bottom Now we start adding some images into the albums
The image title is limited to 64 characters however there is no restriction on the length of the image description And, you are free to use any html tags in the description. The code for this part is very similar to the one when adding new album so i won't explain it in detail here. You could read the source code and i'm sure you'll understand. The difference is that the image is stored in GALLERY_IMG_DIR and the image icon and store it in GALLERY_IMG_DIR/thumbnail.
Image List
After you add a new image or when you click on "List Image" on the left navigation link you can see the list of all images. By default this page will show all images from all albums. To show images just from one album you can select the album name from the combo box on the top right corner. When
you select an album it will trigger the onChange event of the combo box and call a javascript function called viewImage() along with the id of the selected album. The function will then redirect you to the appropriate list.
The javascript code is show below. Source code : admin/index.phps function viewImage(albumId) { if (albumId != '') { window.location.href = 'index.php?page=list-image&album=' + albumId; } else { window.location.href = 'index.php?page=list-image'; } } When you select a specific album from the combo box the album id is sent to this function. The if block is executed and you go to the page for that specific album. But if you select the "--- All Album--" option then the value of albumId will be an empty string. The else block is executed and you'll go the the default image list. Next, modify and delete image
After you click the "Modify Image" button we continue saving the new information and also save the new image ( if a new one is provided ). The process is just a repetition of the code to modify album data and image. The difference is that now we modify two images, the full-sized image and the thumbnail. Example : admin/index.php?page=modify-image&imgId=13 Source code : admin/modify-image.phps // ... some code here if (isset($_POST['txtTitle'])) { $albumId = $_POST['cboAlbum']; $imgTitle = $_POST['txtTitle']; $imgDesc = $_POST['mtxDesc']; if ($_FILES['fleImage']['tmp_name'] != '') { $images = uploadImage('fleImage', GALLERY_IMG_DIR); if ($images['image'] == '' && $images['thumbnail'] == '') { echo "Error uploading file"; exit; } $image = "'" . $images['image'] . "'"; $thumbnail = "'" . $images['thumbnail'] . "'"; $sql = "SELECT im_image, im_thumbnail FROM tbl_image WHERE im_id = $imgId"; $result = mysql_query($sql) or die('Error, get image info failed. ' . mysql_error()); $row = mysql_fetch_assoc($result); unlink(GALLERY_IMG_DIR . $row['im_image']); unlink(GALLERY_IMG_DIR . 'thumbnail/' . $row['im_thumbnail']); } else { // the old image is not replaced $image = "im_image"; $thumbnail = "im_thumbnail"; } if (!get_magic_quotes_gpc()) { $albumName = addslashes($albumName); $albumDesc = addslashes($albumDesc); } $sql = "UPDATE tbl_image SET im_album_id = $albumId, im_title = '$imgTitle', im_description = '$imgDesc', im_image = $image, im_thumbnail = $thumbnail, im_date = NOW() WHERE im_id = $imgId"; mysql_query($sql) or die('Error, update image failed : ' .
Delete Image
To delete an image we just need the image id an the album id. Then we get the image and icon name so we can delete them from the server. Once the files are deleted we continue removing the image information from the database. Source code : admin/list-image.phps // ... a little code here if (isset($_GET['delete']) && isset($_GET['album']) && isset($_GET['imgId'])) { // get the image file name so we // can delete it from the server $sql = "SELECT im_image, im_thumbnail FROM tbl_image WHERE im_id = {$_GET['imgId']} AND im_album_id = {$_GET['album']}"; $result = mysql_query($sql) or die('Delete album failed. ' . mysql_error()); if (mysql_num_rows($result) == 1) { $row = mysql_fetch_assoc($result); // remove the image and the thumbnail from the server unlink(GALLERY_IMG_DIR . $row['im_image']); unlink(GALLERY_IMG_DIR . 'thumbnail/' . $row['im_thumbnail']); // and then remove the database entry $sql = "DELETE FROM tbl_image WHERE im_id = {$_GET['imgId']} AND im_album_id = {$_GET['album']}"; mysql_query($sql) or die('Delete album failed. ' . mysql_error()); } } // ... a little code there Next, we continue to the "ordinary user" part. Starting from the album list.
Album List
Here is the page that the gallery visitors see. The code behind it is mostly the same as the one in the admin pages. For instance the album list, image list and image detail page are pretty much the same code. The main page ( index.php ) also act like a frame just like the admin main page. The big difference is that we need to make these pages more pretty than the admin pages. I added some CSS to make these pages look better but since my web design skill really sucks all i can add is just some greenish look for it.
The other difference is the way the thumbnails are arranged. On the admin pages we list the albums and images in a simple numbered list in a table. But now the thumbnails in the album list and image list are arranged in rows and columns. It look better that way. Coding the script to display the thumbnails in rows and columns is a little bit tricky ( just a little bit ). Here is the code that does it. // ... some code here
echo '<table width="700" border="0" cellspacing="1" cellpadding="2" align="center">'; $colsPerRow = 4; // width of each column in percent $colWidth = (int)(100/$colsPerRow); // ... more code here First we specify how many columns we want in one table row. For this gallery we set the value to four images in one row. Change this value and the code will automatically rearranged the way the thumbnails are displayed. From the value of $colsPerRow we then calculate the value of each column width using this formula : $colWidth = (int)(100/$colsPerRow). So if $colsPerRow is four then the width is 25 ( in percent ) and if we set $colsPerRow value to three then the column width will be 33 ( percent ). The (int) part in the formula works just like the mathematical function floor(). It round down a fraction to it's closest integer. You can rewrite the formula into this : $colWidth = floor(100/$colsPerRow)but i simply prefer using (int). // ... previous code while ($row = mysql_fetch_assoc($result)) { if ($i % $colsPerRow == 0) { // start a new row echo '<tr>'; } $numImages = $row['al_numimage'] > 1 ? $row['al_numimage'] . ' images' : $row['al_numimage'] . ' image'; echo '<td width="' . $colWidth . '%">' . '<a href="index.php?page=list-image&album=' . $row['al_id'] . '">' . '<img src="viewImage.php?type=album&name=' . $row['al_image'] . '" border="0">' . '<br>' . $row['al_name'] . '</a><br />' . $numImages . '</td>'; if ($i % $colsPerRow == $colsPerRow - 1) { // end this row echo '</tr>'; } $i += 1; } // ... more to come In the while loop we start arranging the thumbnails. Since we build the table rows dynamically we must know when to start and end a row. Here we use the modulo of a counter ( $i ) with the number of columns on each row ( $colsPerRow ). If the result is zero then we start a new row by printing <tr> and if the value is $colsPerRow - 1 we end that row by printing </tr>. Making the columns is the easy part. Just print the opening tag ( <td> ) fill it with the thumbnail, album name and how many images in that album then print the closing tag ( </td> ). After the while loop complete there is one more work to be done. That is to check if the last row is full or not. From the snapshot above you can see that we have five albums so the last row only contain one thumbnail. If we just finish the code here and do nothing the layout could be broken ( not always, depending on how smart your browser is ). To mend it we print blank columns to fill in the empty spaces. To find out how many blank columns needed we use the last value of the counter ( $i ) and keep increasing it's value by one until it's modulo with $colsPerRow reach zero, like this ... // ... previous code // print blank columns if ($i % $colsPerRow != 0) { while ($i++ % $colsPerRow != 0) { echo '<td width="' . $colWidth . '%"> </td>'; } echo '</tr>'; } echo '</table>'; To make things clearer take a look at the snapshot below. It's the same snapshot of the album list above but this time the table border is visible so you can see that on the second row we have printed three empty columns.
Click any of the image icons and you will go to the image detail page.
Image Detail
This is the main stage of the gallery which displays the full-sized image and the description along with other trinkets. Take a look at this one page . You may feel that this is the wrong way to design a web page because the image is too large to fit in the screen. But personally i prefer preserving the original art instead of resizing it to fit into the page design Now it's time to bring your attention to that navigation link that you see on top of every page. I'm talking about this one :
This kind of navigation link is widely known as breadcrumbs. Everytime you browse around the albums and images the breadcrumb always display your current location in the gallery. I'm sure you've seen this many times when surfing the web. This simple piece of navigation is very convenient for the visitors to move around the gallery, moving back from the image detail page to the image list or the album list. The function needed to display the breadcrumb is called showBreadcrumb(). You can find this function in library/functions.php. Here's the function code : Source code : library/functions.phps function showBreadcrumb() { if (isset($_GET['album'])) { $album = $_GET['album']; $sql = "SELECT al_name FROM tbl_album WHERE al_id = $album"; $result = mysql_query($sql) or die('Error, get album name failed. ' . mysql_error()); $row = mysql_fetch_assoc($result); echo ' > <a href="index.php?page=list-image&album=' . $album . '">' . $row['al_name'] . '</a>';
if (isset($_GET['image'])) { $image = $_GET['image']; $sql = "SELECT im_title FROM tbl_image WHERE im_id = $image"; $result = mysql_query($sql) or die('Error, get image name failed. ' . mysql_error()); $row = mysql_fetch_assoc($result); echo ' > <a href="index.php?page=image-detail&album=' . $album . '&image=' . $image . '">' . $row['im_title'] . '</a>'; } } } The first if statement check if there's an album id in the page url. If an album id is found we create a link to the album. The next if block check if an image id is available in the url and if we found one we create a link to the image ( which is the currently displayed page ) There's another navigation means in that page. If you scroll down the page you can see the next and previous link. Using this link you can navigate from one image to another that are still in one album. To find the previous image we search the database for one image in the same album as current image where the id is less than the current image id. // ... above here is the sql query to fetch the image detail. // $image contain the image's information $image = mysql_fetch_assoc($result); // set the initial value for previous and next image id $prev = $next = 0; // get the previous image $sql = "SELECT im_id FROM tbl_image WHERE im_id < $imageId AND im_album_id = {$image['im_album_id']} ORDER BY im_id DESC LIMIT 0, 1"; $result = mysql_query($sql) or die('Error, get image info failed. ' . mysql_error()); if (mysql_num_rows($result) > 0) { $row = mysql_fetch_assoc($result); $prev = $row['im_id']; } // ... the code to get the next image I highlighted the ORDER BY clause in the above code because it is really important. We really need it since there is no guarantee that the database will store the records in orderly fashion. For example, after you update an image the internal order of the records in the database would change so without the ORDER BY clause we get an image which id is less than the current image id BUT it may not be the closest one. So if the current image id is 20 and there are three other images whose id are 15, 17 and 18 then without the ORDER BY clause the sql query may return any one of those ids while the one we actually want is image id 18. Using the ORDER BY clause the query will return the images in descending order ( 18, 17, 15 ). And because we add the LIMIT clause the query will return only one record. The one containing image id 18. The following code is the one to fetch the next image. For this one we just need to find an image in the same album as current image where the id is greater than the current image id. Note that in this code the order clause use ASC instead of DESC so that the returned record will be sorted in ascending order. // ... the code to get previous image // get the next image $sql = "SELECT im_id FROM tbl_image WHERE im_id > $imageId AND im_album_id = {$image['im_album_id']} ORDER BY im_id ASC LIMIT 0, 1"; $result = mysql_query($sql) or die('Error, get image info failed. ' . mysql_error()); if (mysql_num_rows($result) > 0) { $row = mysql_fetch_assoc($result); $next = $row['im_id']; }
This is the end of the image gallery tutorial. To download the code for this image gallery click here . Be sure to modify the configuration file before running the script.
Get a clear description of the project. Ask whenever you are unsure about something. Create a demo /
mockup whenever possible so you can be sure that you and your client are talking about the same thing.
Can you do it? Seriously, can you complete each and every features requested? Your client won't be
happy if you say you can complete the job but fail to do so. One quick way to be sure is to make a prototype before even bidding the project then propose it to your (future) client to see if it is what she expected.
Don't be too optimistic. If you think you can complete a job in one day make sure you can do it in one
day. This include all the testing and bug fixing.
Can you accept the payment? Some will want to pay using PayPal if you can accept PayPal it's no
problem but if you can't maybe you should consider finding another project.
Ask for the PHP and MySQL version used by your client and develop the project using the same
version. This will reduce the risk of creating buggy scripts just because your version and your client's version is different
Test and retest. Be ready to fix bugs. All software have bugs, but make sure you are ready to fix them when it's found.
Your clients will certainly expect a prompt response so give it to them. Even if your script is buggy but if you are prompt in responding and fixing it you can get good reviews.
KYOB
That's short for Kick Your Own Butt ! Freelancing requires hard discipline and because there's no boss or supervisor breathing on your neck it can be hard to feel that you are working. Sure, you still have deadlines, but the client is probably on the other side of the planet and most likely you will never see her face to face. It just doesn't feel the same as an ordinary job. KYOB is probably the most important ability you must master if you really want to be a successful freelancer. If you have the time try reading this e-book : The Webmaster Business Masters Course ( it's free ). It's a bit long ( 61 pages ) but pretty good.
I want to extract the file extentions from a file name. How do i do that from php? Answer There are several ways to do that. First is using the combination of strrpos() and substr() function like this : $ext = substr($fileName, strrpos($fileName, '.') + 1); For example, if $fileName is my-new-house.jpg then strrpos($fileName, '.') will return the last location a dot character in $fileName which is 15. So substr($fileName, strrpos($fileName, '.') + 1) equals to substr($fileName, 16) which return 'jpg' The second is using strrchr() and substr() : $ext = substr(strrchr($fileName, '.'), 1); strrchr($fileName) returns '.jpg' so substr(strrchr($fileName, '.'), 1) equals to substr('.jpg', 1) which returns 'jpg'
7. How to format numbers in php How do i format number using php? For example if i have 123456 and i want to print it as 123.456 or 123,456.00 what functions should i use
Answer PHP's solution to formatting number is the number_format() function. This function take 4 arguments but only the first is mandatory. The arguments are : 1. The number itself 2. Decimals 3. Decimal points 4. Thousand separators Here are some examples : 1. number_format(123456) yield 123,456 2. number_format(123456, 2) yield 123,456.00 3. number_format(123456, 3) yield 123,456.000 4. number_format(123.456, 2) yield 123.46 5. number_format(123.456, 3) yield 123.456 6. number_format(123456, 2, ',', '.') yield 123.456,00 7. number_format(123456, 2, '.', ',') yield 123,456.00 8. number_format(123456.7, 2, '.', ',') yield 123,456.70
Note : this function CANNOT accept three arguments. Only one, two, or four. That means something like number_format(123456, 2, ',') is not a valid function call
8. How to disable mysql warning I have a PHP - MySQL related question. Sorry if you mention this later in the tutorial , i am not there yet. Okay: $conn = mysql_connect($dbhost, $dbuser, $dbpass) or die('The Server seems to be down');
Lets pretend that the connection failed, it will report something like this: Warning: mysql_connect() [function.mysql-connect]: Access denied for user 'root'@'localhost' (using password: YES) in C:\Program Files\Apache Group\Apache2\htdocs\prev0~.php on line 10 The Server seems to be down Is there anyway of doing it so it does not show the actual Warning: mysql.... and ONLY shows "The Server seems to be down" Answer You can force mysql_connect() to be quiet by using '@' like this : @$conn = mysql_connect($dbhost, $dbuser, $dbpass) or die('The Server seems to be down'); or you can directly tell php to 'shut up' when it found any errors by placing this code at the beginning of your script : error_reporting(0);
9. Error about loading the header I'm trying to get your tutorial to work, but seem to have a problem : after I click the 'sign guestbook' the page loads blank, in otherwords I dont see the form or the records, even though the entry is saved to the db ! What I noticed is that the line header('Location: ' . $_SERVER['REQUEST_URI']); I had to comment otherwise I get an error about loading the header. I am running this on the localhost. Otherwise this is a real great tutorial. Answer The error about sending header is usually because there's an extra space or newline at the end of config.php file. PHP always complain if there's already an output before sending headers. Check your config.php file to see if there's any extra spaces or newlines before the opening php tags ( ) The code still work if you remove the redirect code. I only add the redirection cod to prevent multiple form submission if the user refresh the page.
10. Upload & Download limited to 64 kilobytes I recently made an aplication to control files in a organization (thanks for the upload and download blob files with php and mysql). The only question or issue that i have, is that .doc files or word files, are save ok in the db, but when i download them, they appear to size 64k (any file 64k) and when i open them it send me an error. Would you know what is happening here? i would apreciate very much you help. Thank you Answer The old file upload tutorial is using BLOB data type to store the files. It is limited to store up to 64 kilobytes of data so the new tutorial is using MEDIUMBLOB so we can store larger files ( up to 16 megabytes )
Nothing complex here. The customer doesn't need to register for an account. She just buy then leave.
Features Okay, here are the shop features ( or maybe i should call this restrictions ) 1. Flat shipping cost. No complex shipping calculation for this shop right now and i don't have any plan to change this in near future. Payment options including COD ( cash on delivery ) and Paypal For now this shop can only handle COD and payment through Paypal IPN. The reason i pick paypal is because they provide excellent resource for developers so i can test the payment process easily. Configurable image and thumbnail size You can restrict the product image width from the config file. You can also set the thumbnail width you want for all product images that you upload.
2.
3.
File Organization The files in our shop will be organized like this :
config.php : this is the main configuration file for our shop category-functions.php :functions required for fetching the categories product-functions.php : contain product related functions cart-functions.php : shopping cart specific functions checkout-functions.php : checkout processes are in here common.php : common functions required for the shop and admin pages database.php : contain the database abstraction functions
header.php The shop common header. top.php You can place your shop banner here. footer.php Common footer, display the shop address, phone number and email. You can add more information here when needed. shop.css Style sheet file for our shop leftNav.php The left navigation you see on the shop categoryList.php Show the top categories we have productList.php Show the products in certain category productDetail.php You know what this is for, right ? miniCart.php Shown on the right portion of the shop pages, it shows the products in the shoping cart. shippingAndPaymentInfo.php The form to enter shipping and payment info ( step 1 of checkout ) checkoutConfirmation.php Show the order items, shipping & payment info ( step 2 of checkout )
paypal.inc.php The configuration file for the paypal payment module ipn.php The script that process payment verification payment.php Contain the form that submit the payment information from this website to paypal website
The plaincart/admin folder will contain all the admin files.You can see that admin folder also contain include and library folder. These will contain specific library files for the admin pages All images required in our shop will be put in plaincart/images directory. The category and product images are put in the category and product sub-folder respectively. The Requirements For this tutorial i'm using these :
Apache 2 PHP 4.3.10 with GD ( graphics library ) support ( you can also use lower version but >= 4.3.7 ) MySQL 4
If you don't have these ready check out this tutorial to install them : Installing Apache PHP & MySQL
What Configurations You Will Need Database Configuration When you install the shopping cart script you will need to modify library/config.php. You need to change the database connection info ( database host, username, password and name ) according to your own configurations. Enabling GD Support The next thing you may need to do is to enable the GD support. This is usually enabled by default by web hosting company but in case you test it on your own computer you may need to enable it manually. First, open the php.ini file using a text editor ( notepad is okay ) and search for this line of code : extension=php_gd2.dll If you see that code preceded by a semicolon ( ; ) that means GD library is not enabled yet. Remove the semicolon to enable GD and then restart the web server ( apache ) for the changes to take effect.
I really hope this shopping cart tutorial is useful for you. Now to take the first step, let's start with the database design
Description Storing all product categories The products ( what else ) When the shopper decided to put an item into the shopping cart we'll add the item here This is where we save all orders The items ordered Store all shop admin user account Contain the shop configuration like name, address, phone number, email, etc
Now, let's take a better look at each table tbl_category This table store the product categories. From the ER diagram you can see that our current database design enables a category to have a child category and for the child category to have another child category and so on. But for this tutorial we make a restriction that the category will only two level deep like this "Top Category > Manga > Naruto". The reason is to reduce the number of clicks required by a visitor when browsing a category. Another rule is that a product can only be added on the second level category. For example if we have this category structure : Top Category > Manga > Naruto then we can only add a product in "Naruto", not in "Manga". The top level categories will not contain any products and a product can only belong to one category. tbl_product In this table we store the product's name, category id, description, image and thumbnail. For now a product can only have one image. It may not be enough if you want to show a picture of you product from multiple angles so i plan to improve this on future version. When adding a product image in the admin page we don't need to upload the thumbnail too. The script will generate the thumbnail from the main image. The thumbnail size is defined in library/config.php ( THUMBNAIL_WIDTH ) and currently it is set to 75 pixels.
tbl_cart
This table will store all items currently put by the customer. Here we have ct_session_id to save the id of a shopping session. We will explore this further when adding a product to shopping cart tbl_order Finally when the customer finally place the order, we add the new order in this table. The shipping and payment information that the customer provided during checkout are alos saved in this table including the shipping cost. For the order id i decided to use an auto increment number starting from 1001. Why start at 1001 ? Because an order id looks ugly ( at least for me ^^ ) if it' s too short like 1, 2 or 3 so starting the order id from 1001 seems to be a good idea for me. To make the order id start from 1001 we use the following sql : CREATE TABLE tbl_order ( id int(10) unsigned NOT NULL auto_increment, date datetime default NULL, last_update datetime NOT NULL default '0000-00-00 00:00:00', status enum('New', 'Paid', 'Shipped','Completed','Cancelled') NOT NULL default 'New', memo varchar(255) NOT NULL default '', shipping_first_name varchar(50) NOT NULL default '', shipping_last_name varchar(50) NOT NULL default '', shipping_address1 varchar(100) NOT NULL default '', shipping_address2 varchar(100) NOT NULL default '', shipping_phone varchar(32) NOT NULL default '', shipping_city varchar(100) NOT NULL default '', shipping_state varchar(32) NOT NULL default '', shipping_postal_code varchar(10) NOT NULL default '', shipping_cost decimal(5,2) default '0.00', payment_first_name varchar(50) NOT NULL default '', payment_last_name varchar(50) NOT NULL default '', payment_address1 varchar(100) NOT NULL default '', payment_address2 varchar(100) NOT NULL default '', payment_phone varchar(32) NOT NULL default '', payment_city varchar(100) NOT NULL default '', payment_state varchar(32) NOT NULL default '', payment_postal_code varchar(10) NOT NULL default '', PRIMARY KEY ( id) ) TYPE=MyISAM AUTO_INCREMENT=1001 ; You see, we just need to add AUTO_INCREMENT = 1001 right after the create definition. tbl_order_item All ordered items are put here. We simply copy the items from the cart table when the customer place the order. tbl_shop_config This table store the shop information. For now it only have the shop name, address, phone number, contact email address, shipping cost, the currency used in the shop and a flag whether we want to receive an email whenever a customer place an order. tbl_user This table save all the user or admin account. Currently all user is an admin and all can do everything to the shop. I'm planning to add permission level so one admin can do everything, while the other user can only add / update product, manage orders, etc. By the way, we will be using indexes on the tables to speed up queries. As a matter of fact whatever application you make using indexes is a good idea because it can improve the database query performance.
Okay, next we talk about the database abstraction. It's not a difficult stuff so you can skim read it if you like.
Database Abstraction
For our shopping cart we will use a simple database abstraction library. Although this tutorial only use MySQL but using an abstraction layer is still a good thing. We do this just in sow we can easily migrate to another database. In case you don't know, a database abstraction is simply making our own interfaces ( functions ) to call the database function provided by PHP. This way if we change to another database or if the PHP function itself change we just need to modify one file ( database.php ) instead of modifying all our source code. The database functions is stored in library/database.php. You can see the content below
<?php
require_once 'config.php'; $dbConn = mysql_connect ($dbHost, $dbUser, $dbPass) or die ('MySQL connect failed. ' . mysql_error()); mysql_select_db($dbName) or die('Cannot select database. ' . mysql_error()); function dbQuery($sql) { return mysql_query($sql) or die('Query failed. ' . mysql_error()); } function dbAffectedRows() { global $dbConn; return mysql_affected_rows($dbConn); } function dbFetchArray($result, $resultType = MYSQL_NUM) { return mysql_fetch_array($result, $resultType); } function dbFetchAssoc($result) { return mysql_fetch_assoc($result); } function dbFetchRow($result) { return mysql_fetch_row($result); } function dbFreeResult($result) { return mysql_free_result($result); } function dbNumRows($result) { return mysql_num_rows($result); } function dbSelect($dbName) { return mysql_select_db($dbName); } ?>
How to use it ? Below you can see an example on how to use this database abstration library. The first one is using PHP database function directly : <?php require_once 'config.php'; $conn = mysql_connect($dbHost, $dbUser, $dbPass); mysql_select_db($dbName); $sql = "SELECT * FROM tbl_product ORDER BY pd_name"; $result = mysql_query($sql); $products = array(); while ($row = mysql_fetch_assoc($result)) { $products[] = $row; } ?> And the second one is using our database abstraction. We don't have to initiate the connection this time because it is done from database.php : <?php require_once 'config.php'; require_once 'database.php'; $sql = "SELECT * FROM tbl_product
ORDER BY pd_name"; $result = dbQuery($sql); $products = array(); while ($row = dbFetchAssoc(&$result)) { $products[] = $row; } ?> As you can see, using database abstraction isn't so hard at all. Now that we have taken care the database issues we start building the admin pages.
Category
Add Category Add a new category. View Category List all the category we have. We can also see all the child categories and show many products in each category Modify Category Update a category information, the name, description and image Delete Category Remove a category. All products in it will be set to have cat_id = 0.
Product
Add Product Insert an item into our store. We also need to supply the product image and we'll create a thumbnail automatically from this image View Product View all the products we have. Since our online shop can have many products we can view the products grouped by category. Modify Product Modify product information. We can also remove the product image from this page Delete Product Remove a product from the shop
Order
View Orders Here we can see all the orders we have and their status. When you click the "Order" link on the left navigation you will go straight to the "Paid" orders. The reason is so you can respond immediately upon your customers that already paid for their purchase. Modify Orders Sometimes a customer might contact us saying that she made the wrong order like specifying the wrong product quantity or simply want her order cancelled so she can repeat the buying process again. This page enables the admin to do such a thing.
Shop Configuration This is where we can set and change our online shop appearance, behaviour and information ( like the shop name, main url, etc ).
By the way the shopping cart name is PlainCart :-) Each sub-module (category, product, etc) will have similar file structure. They are :
The admin/index.php only serves as a simple display when the admin enters the administrator section. On this page (and all other pages in the admin sections ) we check if the one requesting the file is already logged in or not. This way we can be sure that anyone who plays around with the admin pages are those who have the required permission. All admin pages will be using the same template so they will all look alike. Basically each admin file will set the page title, what javascript to include and the main content. If you want to customize the look of the admin pages you only need to modify the template and the css file ( admin.css ) . Here is the code for template.php Source code : admin/include/template.php <?php if (!defined('WEB_ROOT')) { exit; } $self = WEB_ROOT . 'admin/index.php'; ?> <html> <head> <title><?php echo $pageTitle; ?></title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <link href="<?php echo WEB_ROOT;?>admin/include/admin.css" rel="stylesheet" type="text/css"> <script language="JavaScript" type="text/javascript" src="<?php echo WEB_ROOT;?>library/common.js"></script> <?php $n = count($script); for ($i = 0; $i < $n; $i++) { if ($script[$i] != '') { echo '<script language="JavaScript" type="text/javascript" src="' . WEB_ROOT. 'admin/library/' . $script[$i]. '"></script>'; } } ?> </head> <body> <table width="750" border="0" align="center" cellpadding="0" cellspacing="1" class="graybox"> <tr> <td colspan="2"><img src="<?php echo WEB_ROOT; ?>admin/include/banner-top.gif" width="750" height="75"></td> </tr> <tr> <td width="150" valign="top" class="navArea"><p> </p> <a href="<?php echo WEB_ROOT; ?>admin/" class="leftnav">Home</a> <a href="<?php echo WEB_ROOT; ?>admin/category/" class="leftnav">Category</a> <a href="<?php echo WEB_ROOT; ?>admin/product/" class="leftnav">Product</a> <a href="<?php echo WEB_ROOT; ?>admin/order/?status=New" class="leftnav">Order</a> <a href="<?php echo WEB_ROOT; ?>admin/config/" class="leftnav">Shop Config</a> <a href="<?php echo WEB_ROOT; ?>admin/user/" class="leftnav">User</a>
<a href="<?php echo $self; ?>?logout" class="leftnav">Logout</a> <p> </p> <p> </p> <p> </p> <p> </p></td> <td width="600" valign="top" class="contentArea"><table width="100%" border="0" cellspacing="0" cellpadding="20"> <tr> <td> <?php require_once $content; ?> </td> </tr> </table></td> </tr> </table> <p> </p> <p align="center">Copyright © 2005 - <?php echo date('Y'); ?> <a href="http://www.phpwebcommerce.com"> www.phpwebcommerce.com</a></p> </body> </html> You see the bolded section on the top? All file that is meant to be included from another file will have this right at the beginning of the code. So if we request the file directly like this : plaincart/admin/include/template.php it won't display anything. But first, we have to postpone making the admin pages and create the login page first. Because a shop admin certainly must login before doing anything to the shop. NOTE : When you play with the admin pages demo you will see that any changes you make doesn't have any effect at all. This is because i've commented some of the code in the demo. I do this just to make sure all the settings, products, and categories stay the same. The code for download, however, are not commented. So when you install it on your server you can make any changes as you wish
Admin Login
All user account is saved in tbl_user. For simplicity the table will only contain the bare necessities such as user id and password. You can add more column if you want to. This is how the login works 1. 2. 3. 4. The admin enter it's username and password The script check whether that username and password combination do exist in the database If it is set the session then go the admin main page If it's not then show an error message
The login function is called doLogin() and it's located in admin/library/functions.php Source code : admin/library/functions.php function doLogin() { // if we found an error save the error message in this variable $errorMessage = ''; $userName = $_POST['txtUserName']; $password = $_POST['txtPassword']; // first, make sure the username & password are not empty if ($userName == '') { $errorMessage = 'You must enter your username'; } else if ($password == '') { $errorMessage = 'You must enter the password'; } else { // check the database and see if the username and // password combo do match $sql = "SELECT user_id FROM tbl_user WHERE user_name = '$userName' AND
user_password = PASSWORD('$password')"; $result = dbQuery($sql); if (dbNumRows($result) == 1) { $row = dbFetchAssoc($result); $_SESSION['plaincart_user_id'] = $row['user_id']; // log the time when the user last login $sql = "UPDATE tbl_user SET user_last_login = NOW() WHERE user_id = '{$row['user_id']}'"; dbQuery($sql); now that the user is verified we move on to the next page if the user had been in the admin pages before we move to the last page visited (isset($_SESSION['login_return_url'])) { header('Location: ' . $_SESSION['login_return_url']); exit; } else { header('Location: index.php'); exit; } } else { $errorMessage = 'Wrong username or password'; } } return $errorMessage; } If the login is successful this function will set the session variable $_SESSION['plaincart_user_id']. All admin pages will check for this session id using the checkUser() function. If the session id is not found then the function will set a redirection to the login page. The checkUser() function look like this : Source code : admin/library/functions.php function checkUser() { if (!isset($_SESSION['plaincart_user_id'])) { header('Location: ' . WEB_ROOT . 'admin/login.php'); } if (isset($_GET['logout'])) { doLogout(); } } You see that if $_SESSION['plaincart_user_id'] is not set we just redirect to the login page. Very simple right? Another thing that this function check is if there's a 'logout' in the query string. If it is then we call the doLogout() function which will remove the session id. Source code : admin/library/functions.php function doLogout() { if (isset($_SESSION['plaincart_user_id'])) { unset($_SESSION['plaincart_user_id']); session_unregister('plaincart_user_id'); } header('Location: login.php'); } Next we start making the category pages // // // if
Take a look at the code snippet below : Source code : admin/category/list.php <?php // ... $catId = (isset($_GET['catId']) && (int)$_GET['catId'] >= 0) ? (int)$_GET['catId'] : 0; //... ?>
When the page loads we check for the existence of catId ( category id ) in the query string. This category id is then used as the paramater for the javascript function addCategory() . When you click on the 'Add Category' button the parent id will be sent to category/add.php. Go look at the source code and scroll to the bottom you will see this code : Source code : admin/category/list.php <input name="btnAddCategory" type="button" id="btnAddCategory" value="Add Category" class="box" onClick="addCategory(<?php echo $catId; ?>)">
The addCategory() function is defined in admin/library/category.js. It simply perform a redirect to show the add category page. More detail on adding a category can be found on the next page.
Take a look at the form source code. The form has a hidden variable called hidParentId. The value is set from category/list.php as explained on the previous page. Go look at the source code and scroll to the bottom you will see this code : <input name="btnAddCategory" type="button" id="btnAddCategory" value="Add Category" class="box" onClick="addCategory(<?php echo $catId; ?>)">
When you submit the form the process then handed to processCategory.php. All kind of category processing ( add, modify, delete ) are done in this file. On top of the script there's a simple switch to call the appropriate function based on the action required. $action = isset($_GET['action']) ? $_GET['action'] : ''; switch ($action) { case 'addCategory' : addCategory(); break; case 'modifyCategory' : modifyCategory(); break; case 'deleteCategory' : deleteCategory(); break; case 'deleteImage' : deleteImage(); break; default : // if action is not defined or unknown // move to main category page header('Location: index.php'); }
On the add category form the form action is set as processCategory.php?action=addCategory so if you look at the code above the script will call addCategory();. If no action is defined we just redirect to category main page. When saving the product image there is a possibility of name conflict. It may seem weird for two categories to have the same image name, but in some cases it can happen. To avoid such conflict we will generate a new name for each category image we upload using the combination of rand(), time() and md5() functions like this : // get the image extension $ext = substr(strrchr($image['name'], "."), 1); // generate a random new file name to avoid name conflict $imagePath = md5(rand() * time()) . ".$ext"; The image name wil then become something like 6c444ed816ce251d610c25154dc28462.jpg. Now it's almost impossible for us to ever hit the name conflict problem. We will use the same name generation for the product image and thumbnail. How does it work ? The time() function will return the number of seconds elapsed since the beginning of ( computer ) time which is January 1, 1970. Using rand() function we get a random value less or equal to the number of seconds. We need to use rand() because this shopping cart can have more than one admin. If two admins happen to submit the form at the same second the result of time() will be the same. As the final step md5() use the random value and return the hash ( a string with 32 characters ). If you feel that using 32 characters for a filename is too much you can use substr() function to cut it like this :
// get the image extension $ext = substr(strrchr($image['name'], "."), 1); // generate a random new file name to avoid name conflict $imagePath = substr(md5(rand() * time()), 0, 10) . ".$ext"; The code above will use only the first ten characters as the file name. Next is the modify category page.
Another difference is that in this form we also display the category image. If you change the category image then the old image will be deleted from the server and the new image is uploaded. Take a look a the code below. To get the category info from database we need the category id from the query string. If $_GET['catId'] is not present or empty we just redirect to index.php. If it's present and not empty we fetch the category info.
Source code : admin/category/modify.php <?php if (!defined('WEB_ROOT')) { exit; } // make sure a category id exists if (isset($_GET['catId']) && (int)$_GET['catId'] > 0) { $catId = (int)$_GET['catId']; } else { header('Location:index.php'); } $sql = "SELECT cat_id, cat_name, cat_description, cat_image FROM tbl_category WHERE cat_id = $catId"; $result =& dbQuery($sql); $row =& dbFetchAssoc(&$result); extract($row); // ... put the form down here ?> On the screenshot you can see that we next to the category image we have a delete link. Clicking on the link will call the javascript function deleteImage(). This function will pop a confirmation box and if you confirm the deletion the function will redirect you to processCategory.php where all category related process is taken care of. Below is the code that perform the image deletion Source code : admin/category/processCategory.php function deleteImage()
{ if (isset($_GET['catId']) && (int)$_GET['catId'] > 0) { $catId = (int)$_GET['catId']; } else { header('Location: index.php'); } _deleteImage($catId); // update the image name in the database $sql = "UPDATE tbl_category SET cat_image = '' WHERE cat_id = $catId"; dbQuery($sql); header("Location: index.php?view=modify&catId=$catId"); } To delete the image from the server the deleteImage() function calls _deleteImage(). Please excuse this lame function naming. I just cant' find other name that fit perfectly for this function. After deleting the image we update the category information in database. We only need to set cat_image to an empty string and we're done. The final thing that deleteImage() do is redirect back to the category modification page. We don't redirect to category listing page because the admin may still want to modify the category further.
Pretty easy huh? I don't think we need to dive into the source code here because it is quite simple. Just take a look at the code and you will understand Okay, now the category stuff is done we start playing with the next sub modul, the product pages.
All is mandatory except for the image. We can add the product image later.The add product form look like this :
Not much difference from the add category form. We just have more input box. On top of the form you can see the category combo box. We build this so that you can only select the second level category. This is to ensure that all product are added on the second level category and not put in the top level category by mistake. The one responsible to build the list is the buildCategoryOptions() in admin/library/function.php. Below is the snippet for the code that build the combo box. If you happen to click the "Add Product" button while viewing the product list in a category you can see that the category list is preselected to the right category. When the function is building the list options it always check if the current category id is the same as the category id in the function parameter. Source code : admin/library/functions.php <?php // ... some code to fetch the categories from database // build combo box options $list = ''; foreach ($categories as $key => $value) { $name = $value['name']; $children = $value['children']; $list .= "<optgroup label=\"$name\">"; foreach ($children as $child) { $list .= "<option value=\"{$child['id']}\""; if ($child['id'] == $catId) { $list.= " selected"; } $list .= ">{$child['name']}</option>\r\n"; } $list .= "</optgroup>"; } // ... more code here ?> The product image you need to supply is the large size product image which will be shown in the product detail page. The script will generate a thumbnail for it to be shown in the product browsing page. We define the maximum image size and the thumbnail size in config php. We need to restrict the image size so it won't destroy the site layout. Imagine if the image is 1000 pixels wide and 2000 pixels high. It will make the product detail page look awful. Image resizing can be turned on or off. If you set LIMIT_PRODUCT_WIDTH to false on config.php the script will just upload the image without worying about it's size. It's not recommended though. Please note that the image resizing function can only handle jpeg and gif image. That function was actually taken from the php
There's a javascript function called viewProduct() attached to the combo box on the top right portion of the page. If you select a category then the page will show product list only from that category. For example if you choose "Naruto" the this page will look like this :
If you wish to view all products again just choose "All Category" from the combo box The code for this page is in admin/product/list.php. It's a very simple page really. It just perform a simple SELECT query and loop through the result to print the contents. If the products returned from the query is more than five we print the paging links to navigate from one result page to another. The code snippet below shows how the paging is done. Source code : admin/product/list.php <?php if (!defined('WEB_ROOT')) { exit; }
if (isset($_GET['catId']) && (int)$_GET['catId'] > 0) { $catId = (int)$_GET['catId']; $sql2 = " AND p.cat_id = $catId"; $queryString = "catId=$catId"; } else { $catId = 0; $sql2 = ''; $queryString = ''; } // for paging // how many rows to show per page $rowsPerPage = 5; $sql = "SELECT pd_id, c.cat_id, cat_name, pd_name, pd_thumbnail FROM tbl_product p, tbl_category c WHERE p.cat_id = c.cat_id $sql2 ORDER BY pd_name"; $result = dbQuery(getPagingQuery($sql, $rowsPerPage)); $pagingLink = getPagingLink($sql, $rowsPerPage, $queryString); $categoryList = buildCategoryOptions($catId); ?> Instead of executing the query directly like this dbQuery($query) we add some paging code first to the sql query by feeding it to getPagingQuery() along with how many results that we want to show on each page. The getPagingQuery() function is located in library/common.php. Here is the code : Source code : library/common.php function getPagingQuery($sql, $itemPerPage = 10) { if (isset($_GET['page']) && (int)$_GET['page'] > 0) { $page = (int)$_GET['page']; } else { $page = 1; } // start fetching from this row number $offset = ($page - 1) * $itemPerPage;
return $sql . " LIMIT $offset, $itemPerPage"; } This function first check the page number. When you click on a paging link there's a page variable embedded in the query string. If the function can't find any page variable on the query string then it just assume that the first page is wanted. Paging on MySQL is done using the LIMIT keyword. The offset is the index where we want to start fetching the result. We also supply how many result that we want . Now after we get the paging query and execute it the next thing we must do is making the page links. This is done by getPagingLink() function. This function is the one responsible for printing the paging link you see on the bottom of the product list. The function only does the followings : 1. 2. 3. 4. Find out how many total results returned by a query Calculate how many pages the results should be split into Determine the first and last page Print page link from first to last.
Somehow i don't feel like explaining the paging process in detail here. If you're interested you can read the full tutorian on pagination here : paging tutorial on www.php-mysql-tutorial.com Next we discuss about adding a new product. When you click on the "Add Product" button you will be taken to the add product screen.
The process of updating the product information is also the same as the category. So we better move on and working on the process of deleting a product.
2. 3. 4.
Delete all references from tbl_cart Delete the product image and thumbnail Delete the product from database
And here is the code for deleteProduct() function that responsible for this process : Source code : admin/product/processProduct.php function deleteProduct() { if (isset($_GET['productId']) && (int)$_GET['productId'] > 0) { $productId = (int)$_GET['productId']; } else { header('Location: index.php'); } // remove any references to this product from // tbl_order_item and tbl_cart $sql = "DELETE FROM tbl_order_item WHERE pd_id = $productId"; dbQuery($sql); $sql = "DELETE FROM tbl_cart WHERE pd_id = $productId"; dbQuery($sql); // get the image name and thumbnail $sql = "SELECT pd_image, pd_thumbnail FROM tbl_product WHERE pd_id = $productId"; $result =& dbQuery($sql); $row =& dbFetchAssoc($result); // remove the product image and thumbnail if ($row['pd_image']) { unlink(SRV_ROOT . 'images/product/' . $row['pd_image']); unlink(SRV_ROOT . 'images/product/' . $row['pd_thumbnail']); } // remove the product from database; $sql = "DELETE FROM tbl_product WHERE pd_id = $productId"; dbQuery($sql); header('Location: index.php'); }
Let's take a better look at each status New All orders initially have this status Paid An order's status is changed from "New" to "Paid" by the IPN script after completing the payment process. Shipped After we pack the ordered items and ship it we can change the order status to "Shipped" Completed We got the payment, the customer receveived the goods that mean the order is completed Cancelled
In case you see a suspicious order and you feel that it's a fraud you can set the status to "Cancelled". Or in some case a customer calls you and for some reason she ask you to cancel her order. Here is what the order page look like :
Using the combo box on the top right corner we can view the orders with a certain status. Clicking on an order number will take you to the order detail page. On this page we can also modify the order status
Site Configuration
Here we can set some settings which are used sitewide. They are
Online shop information Shop Name Real world address Phone number Contact email Shipping cost Currency A flag whether to send a notification email to admin when a customer place an order
The shop information is shown on the footer ( include/footer.php ). You really shouldn't leave any field blank. Having these information can assure the customers that your online shop is a real business. This can improve customer trust and also improve your credibility and without those two you really can't expect anyone to buy. The currency chooser is simply a drop down box where you can choose between Dollar, Euro, Poundsterling or Yen. Why only four currencies ? Because i'm making the shop using Dreamweaver and the only special characters for currency that i can find are , , and . Plus the dollar sign ( $ ) that makes four currencies.
Under the currency list is the shipping cost. Since this shop use flat shipping cost a simple text box is sufficient. If you plan to offer different shipping cost like depending on the product weight or shipping courier then you really have to change it to suit your need. The last setting is for sending email whenever a new order is placed.The notification email is sent to the address you specify as the contact email ( in the screenshot above it's franky@tomsworkers.com ). If you run a super busy shop it would be better to turn this off otherwise you will be flooded with emails and your mail server might think someone is spamming you.
A user of the shop is actually the shop admin itself. Currently all user are granted the permission to do all administration task. In future version i'll modify this so one user can be assigned to specific task such as managing the products or managing the orders, etc. View User List This display just display all user on the shop
Add User The only information needed are the user name and password.
Here is the code for adding a user. Source code : admin/user/processUser.php function addUser() { $userName = $_POST['txtUserName']; $password = $_POST['txtPassword']; // check if the username is taken $sql = "SELECT user_name FROM tbl_user WHERE user_name = '$userName'"; $result =& dbQuery($sql); if (dbNumRows($result) == 1) { header('Location: index.php?view=add&error=' . urlencode('Username already taken. Choose another one')); } else { $sql = "INSERT INTO tbl_user (user_name, user_password, user_regdate) VALUES ('$userName', PASSWORD('$password'), NOW())"; dbQuery($sql); header('Location: index.php'); } } Before adding the new user info we need to check if the username is taken or not. We just query the database looking for that username. If a row is found that mean the username is already used and we set a redirect to go back to the 'add user' page and send an error message. Since the error message is put in the query string we need to use urlencode(). This function is commonly used when passing a string to a url. Using the above example the url will look like this : index.php?view=add&error=Username+already+taken.+Choose+another+one If the username is still available we just insert the new user info to database. You see that in the insert query we use PASSWORD() function. This is a MySQL function that will encrypt the given password.
Modify User Password For simplicity the user name cannot be changed. Only the password can be changed. The function used for modifying the password simply perform an UPDATE query to update the password
Delete User The deleteUser() function just remove a user from database. No extra steps needed.
The center part is the main area. Here we show the product categories and products. The visitor will ( hopefully ) find her way through the shop, find the item she want, put it into the shopping cart and then buy. What we show in the main area depends on the visitor action. When she first arrive to main page she get the category list. If she click on one of the category then we show the product list for that category. And if she click on a product from the list we show the product detail.
Take a look at the code below, it's the code for index.php, the main page Source code : index.php <?php require_once require_once require_once require_once
$_SESSION['shop_return_url'] = $_SERVER['REQUEST_URI']; $catId = (isset($_GET['c']) && $_GET['c'] != '1') ? $_GET['c'] : 0; $pdId = (isset($_GET['p']) && $_GET['p'] != '') ? $_GET['p'] : 0; // ... more code here ?> First we load the required libraries. The config.php file contain all kind of initialization like setting the database connection, defining constants, and it also load the common libraries. I won't explain the detail, just take a look at the file and i'm sure you will understand what it does. Next is category-functions.php. This file contain the several functions, they are :
formatCategories() This function is used by include/leftNav.php. It's job is to generate the category list where the currently selected category will expand and show the children categories ( if it have any ). getCategoryList() Get a list of first level categories ( i.e. the parent id is zero ) getChildCategories() Get all children categories fetchCategories() Get all categories ( first and second level )
After finish loading the libraries we set a session variable $_SESSION['shop_return_url']. The value for this session is the url of the page that we currently see. This variable is used by the shopping cart. When you click on the 'Continue Shopping' button it will redirect to the url pointed by this session variable. Next step is checking the existence of $_GET['c'] ( category id ) and $_GET['p'] ( product id ). If none exist in the query string we show the category list ( categoryList.php ). If only the category id exist then we show all product in that category ( productList.php ). And if the product id also exist we show that product information ( productDetail.php ). Source code : index.php <?php // ... previous code include 'header.php'; ?> <table width="780" border="1" align="center" cellpadding="0" cellspacing="0"> <tr> <td colspan="3"> <?php include 'top.php'; ?> </td> </tr>
<tr valign="top"> <td width="150" height="400" id="leftnav"> <?php include 'leftNav.php'; ?> </td> <td> <?php if ($pdId) { include 'productDetail.php'; } else if ($catId) { include 'productList.php'; } else { include 'categoryList.php'; } ?> </td> <td width="130" align="center"><?php include 'miniCart.php'; ?></td> </tr> </table> <?php include 'footer.php'; ?> As you can see from the above code, the main page doesn't do much. It "out sourced" almost everything. The header, navigation, content, mini cart and the footer. Of course this is a good thing because if we need to modify some part ( like the left navigation ) we can do it wit less risk of messing with the other part. Now, lets take a deeper look at the header file. One important usage of this file is to set the page title. You may have noticed it that when you're browsing the categories and products the page title change accordingly. Setting up a descriptive page title is especially important when you consider SEO ( search engine optimization ). You see, search engine put heavy emphasize on the page title so we better get this one thing right. Here how it works. By default we set the page title as "My Online Shop" but if a product id is detected in the query string we search the database to find the name of the product. Then we use the product's name as the page title. If we can't find any product id in the query string we do another test to see if there's a category id in there. If we found one we fetch the category name from the database and use it as the page title. Here is the code : Source : include/header.php <?php if (!defined('WEB_ROOT')) { exit; } // set the default page title $pageTitle = 'My Online Shop'; if (isset($_GET['p']) && (int)$_GET['p'] > 0) { $pdId = (int)$_GET['p']; $sql = "SELECT pd_name FROM tbl_product WHERE pd_id = $pdId"; $result = dbQuery($sql); $row = dbFetchAssoc($result); $pageTitle = $row['pd_name']; } else if (isset($_GET['c']) && (int)$_GET['c'] > 0) { $catId = (int)$_GET['c']; $sql = "SELECT cat_name FROM tbl_category WHERE cat_id = $catId"; $result = dbQuery($sql); $row = dbFetchAssoc($result); $pageTitle = $row['cat_name']; } ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title><?php echo $pageTitle; ?></title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <link href="include/shop.css" rel="stylesheet" type="text/css"> <script language="JavaScript" type="text/javascript" src="library/common.js"></script> </head> <body> Next we explore further about the category browsing ( that navigation links on the left section of the page ).
On the left side of the shop pages we show the category list. When the customer clicks on a category on the left side she can see all products in that category. The shop main page include this navigation from include/leftNav.php and as mentioned before, the one responsible to get the list of categories for this navigation is the formatCategories() function in library/category-functions.php In summary here is what formatCategories() do : 1. 2. 3. Get all the children categories Get the parent category and other categories on the same level as the parent Keep looping to get the parent's parent until we reach the top level category
This function is actually a bit too complex for handling two level deep categories. However in the future i want this shop to handle categories more than two level deep so i made this function so it can cope with that. Okay, let's not talk about this function too much and move on with the code for leftNav.php Source code : include/leftNav.php <?php if (!defined('WEB_ROOT')) { exit; } // get all categories $categories = fetchCategories(); // format the categories for display $categories = formatCategories($categories, $catId); ?> <ul> <li><a href="<?php echo $_SERVER['PHP_SELF']; ?>">All Category</a></li> <?php foreach ($categories as $category) { extract($category); // now we have $cat_id, $cat_parent_id, $cat_name $level = ($cat_parent_id == 0) ? 1 : 2; $url = $_SERVER['PHP_SELF'] . "?c=$cat_id"; // for second level categories we print extra spaces to give // indentation look if ($level == 2) { $cat_name = ' » ' . $cat_name; } // assign id="current" for the currently selected category // this will highlight the category name $listId = ''; if ($cat_id == $catId) { $listId = ' id="current"'; } ?> <li<?php echo $listId; ?>><a href="<?php echo $url; ?>"><?php echo $cat_name; ?></a></li> <?php } ?> </ul>
The categories are printed as unordered lis ( <ul> ). I guess you already know that unordered list usually shows up like this :
child 1 child 2
But thanks to CSS ( Cascading Style Sheet ) we can alter how the browser display the list and give them than boxy look. I must admit that i'm a beginner to css and i learn this from projectseven.com, so if you want to know how the css works you can visit the site later. Next up is the product browsing.
If you checkout the source code for productList.php you can see that it first call getChildCategories() function so if you browse "Manga" productList.php will show all products in "Manga" ( zero products ) and it's children "Naruto" and "HunterXHunter" ( four products ). Source code : include/productList.php <?php if (!defined('WEB_ROOT')) { exit; } $productsPerRow = 2; $productsPerPage = 4; $children = array_merge(array($catId), getChildCategories(NULL, $catId)); $children = ' (' . implode(', ', $children) . ')'; $sql = "SELECT pd_id, pd_name, pd_price, pd_thumbnail, pd_qty, c.cat_id FROM tbl_product pd, tbl_category c WHERE pd.cat_id = c.cat_id AND pd.cat_id IN $children ORDER BY pd_name"; $result = dbQuery(getPagingQuery($sql, $productsPerPage)); $pagingLink = getPagingLink($sql, $productsPerPage, "c=$catId"); // ... we got more codes here ?>
The rest of this page code should be pretty clear to you. There's a SELECT query and since we also want to use paging we feed the query to getPagingQuery(). If you don't know what getPagingQuery() does you should go back and read the page about product list in admin section. Below is a snapshot of the product listing
On productList.php the products are displayed so one row display three products like shown above. The number of product in one row can be changed. You just need to modify the value of $productsPerRow. For example modifying this value into 2 give this look :
And here is the code that make this happen Source code : include/productList.php <?php // ... previous code $numProduct = dbNumRows($result); $columnWidth = (int)(100 / $productsPerRow); ?> <table width="100%" border="0" cellspacing="0" cellpadding="20"> <?php if ($numProduct > 0 ) { $i = 0; while ($row = dbFetchAssoc($result)) { extract($row); if ($pd_thumbnail) { $pd_thumbnail = WEB_ROOT . 'images/product/' . $pd_thumbnail; } else { $pd_thumbnail = WEB_ROOT . 'images/no-image-small.png'; } if ($i % $productsPerRow == 0) { echo '<tr>'; } // format how we display the price $pd_price = displayAmount($pd_price); echo "<td width=\"$columnWidth%\" align=\"center\"><a href=\"" . $_SERVER['PHP_SELF'] . "?c=$catId&p=$pd_id" . "\"><img src=\"$pd_thumbnail\" border=\"0\"><br>$pd_name</a><br>Price : $pd_price"; // if the product is no longer in stock, tell the customer if ($pd_qty <= 0) { echo "<br>Out Of Stock"; } echo "</td>\r\n"; if ($i % $productsPerRow == $productsPerRow - 1) { echo '</tr>'; } $i += 1; } if ($i % $productsPerRow > 0) { echo '<td colspan="' . ($productsPerRow - ($i % $productsPerRow)) . '"> </td>'; } } else { ?> <tr><td width="100%" align="center" valign="center">No products in this category</td></tr>
Before printing the product we first check we actually have any product at all. If dbNumRows($result) returns zero that means we have no product in current category. So we show a message saying that there are no products in that category. If we do have some products to show we display them in a table where each column's width depend on how many products to show in a row. Using the above example the column width is 100 / 3 = 33%. Next thing we do is loop through the product list and print each product. If the value of ($i % 3) == 0 we start a new row by printing <tr> and if ($i % 3) == 2 end that row using </tr>. When printing the product info we use displayAmount() function to format the look of the product price. This function is located in library/common.php. Here is what the function look like : Source code : library/common.php function displayAmount($amount) { global $shopConfig; return $shopConfig['currency'] . number_format($amount); } What this function does is concat the currency symbol with the formattted amount. So if the amount is 123456 it will be displayed as $123,456. If the product price contains fraction ( like cents ) you need to change this function to this : function displayAmount($amount) { global $shopConfig; return $shopConfig['currency'] . number_format($amount, 2); } Notice that we add extra parameter to number_format(). This extra parameter will make the function display the amount with two decimal point such as $19.95 Back to productList.php. After printing the product price we check if we already run out of that product in inventory. If so we print additional message saying that the product is no longer in stock. Since there is always a possibility that the last row we print does not contain three products we check the value of $i % 3 after the loop . If it's greater than zero that means the last row does not have three products in it so we need to print the empty column like shown on the first snapshot. In case the picture isn't clear enough here is the same snapshot but the table border is set to 1. You can see that on the last row we have 2 empty ( merged ) columns
No weird stuff here. This kind of layout is actually very common to see on shopping sites across the internet. And by following the common stuff we can be sure that the visitor won't be confused with this layout. There's one more important thing about the 'Add To Cart' button. We only show this if we still have this product in stock. After we run out of this product we just display 'Out Of Stock' . Here is the code snippet from include/productDetail.php Source code : include/productDetail.php <?php // if we still have this product in stock // show the 'Add to cart' button if ($pd_qty > 0) { ?> <input type="button" name="btnAddToCart" value="Add To Cart" onClick="window.location.href='<?php echo $cart_url; ?>';" class="box">
Since we have action=add in the query string the addToCart() function will be called. In short the function will do these : 1. 2. 3. 4. Check if the product exist in database Check if we still have this product in stock ( quantity > 0 ) If the product is already in cart increase the quantity If not add the product to cart
The addToCart() function is located in library/cart-functions.php and the content can be seen below. Source code : library/cart-functions.php function addToCart() { // make sure the product id exist if (isset($_GET['p']) && (int)$_GET['p'] > 0) { $productId = (int)$_GET['p']; } else { header('Location: index.php'); } // does the product exist ? $sql = "SELECT pd_id, pd_qty FROM tbl_product WHERE pd_id = $productId"; $result = dbQuery($sql); if (dbNumRows($result) != 1) { // the product doesn't exist header('Location: cart.php'); } else { // how many of this product we // have in stock $row = dbFetchAssoc($result); $currentStock = $row['pd_qty']; if ($currentStock == 0) { // we no longer have this product in stock // show the error message setError('The product you requested is no longer in stock'); header('Location: cart.php'); exit; } } // current session id
$sid = session_id(); // check if the product is already // in cart table for this session $sql = "SELECT pd_id FROM tbl_cart WHERE pd_id = $productId AND ct_session_id = '$sid'"; $result = dbQuery($sql); if (dbNumRows($result) == 0) { // put the product in cart table $sql = "INSERT INTO tbl_cart (pd_id, ct_qty, ct_session_id, ct_date) VALUES ($productId, 1, '$sid', NOW())"; $result = dbQuery($sql); } else { // update product quantity in cart table $sql = "UPDATE tbl_cart SET ct_qty = ct_qty + 1 WHERE ct_session_id = '$sid' AND pd_id = $productId"; $result = dbQuery($sql); } deleteAbandonedCart(); header('Location: ' . $_SESSION['shop_return_url']); } After finish placing an item into the cart this function do something else. It calls deleteAbandonedCart(). I know it's kind of weird to call this function here but currently it is the best place to call it. We actually have at least three options on how and when to delete abandoned: 1. 2. 3. Call deleteAbandonedCart() when adding a product into cart like shown above Make a new submodul on admin page where we have a button saying 'Delete All Abandoned Cart' Using cron job to remove the abandoned cart periodically.
The first option is what i see as the best option right now, because adding an admin submodul requires extra work, and using cron means installing the shop will become a hassle. Before i forget here is what deleteAbandonedCart() look like : Source code : library/cart-functions.php function deleteAbandonedCart() { $yesterday = date('Y-m-d H:i:s', mktime(0,0,0, date('m'), date('d') - 1, date('Y'))); $sql = "DELETE FROM tbl_cart WHERE ct_date < '$yesterday'"; dbQuery($sql); } We consider a cart is abandoned if it's older than one day. So first we find out what date yesterday was and send a query to database to remove any cart entry where the cart date is less than yesterday date. Okay, after adding the product and removing all abandoned carts we don't send the customer to the shopping cart page. She will stay on the product detail page. But this time the mini cart on the right side will show the product in her shopping cart. The mini cart is included from include/miniCart.php. It displays all product currently in shopping cart. It also show the total amount after the shipping cost. This behaviour ( sending the customer to product page after adding a product ) is actually depend on what kind of shop we're running. If it's a shop where people usually buy more than one kind of product then it's better if we skip showing the shopping cart page and display the product page. But if people on this kind of shop usually only buy one product in a shopping session then it's best if we display the shopping cart page. You just need to modify the redirection to header('Location: cart.php?action=view'); If you're still not sure where to send the shopper after add to cart there's a discussion thread in Webmasterworld forum discussing this issue, check it out. Next we start working on the shopping cart page.
"If you find you are unable to add anything to your cart, please ensure that your internet browser has cookies enabled and that any other security software is not blocking your shopping session."
Now if there are already items in the cart we present it to the customer like shown below. Each row shows the product thumbnail name, unit price, quantity and sub total. On each row we have a delete button so the customer can easily remove the item. If you plan to customize the shopping cart interface do not remove the delete button. It will make the delete process difficult for the customer and it certainly not a good thing.
You may have seen it on another shopping cart solution that to remove an item the customer must set the quantity to zero then click the 'Update Cart' button. That is the wrong way to do it because it makes a very simple action difficult.
Shop - Checkout
There are three steps to complete the checkout 1. 2. 3. Fill out the shipping and payment info Confirm the ordered items, shipping and payment info and enter the payment method. Save the order information to the database. If the payment method is COD ( cash on delivery ) go straight to the thank you page. If the customer choose to pay with paypal submit the payment info to paypal server.
Below is the code for checkout.php Source code : checkout.php <?php require_once 'library/config.php'; require_once 'library/cart-functions.php'; require_once 'library/checkout-functions.php'; if (isCartEmpty()) { // the shopping cart is still empty // so checkout is not allowed header('Location: cart.php'); } else if (isset($_GET['step']) && (int)$_GET['step'] > 0 && (int)$_GET['step'] <= 3) { $step = (int)$_GET['step']; $includeFile = ''; if ($step == 1) { $includeFile = 'shippingAndPaymentInfo.php'; $pageTitle = 'Checkout - Step 1 of 2'; } else if ($step == 2) { $includeFile = 'checkoutConfirmation.php'; $pageTitle = 'Checkout - Step 2 of 2'; } else if ($step == 3) { $orderId = saveOrder(); $orderAmount = getOrderAmount($orderId); $_SESSION['orderId'] = $orderId; // our next action depends on the payment method // if the payment method is COD then show the // success page but when paypal is selected
// send the order details to paypal if ($_POST['hidPaymentMethod'] == 'cod') { header('Location: success.php'); exit; } else { $includeFile = 'paypal/payment.php'; } } } else { // missing or invalid step number, just redirect header('Location: index.php'); } require_once 'include/header.php'; ?> <script language="JavaScript" type="text/javascript" src="library/checkout.js"></script> <?php require_once "include/$includeFile"; require_once 'include/footer.php'; ?> On top of this file we check if the shoppping cart is empty. If it is empty the customer is redirected to the cart page. Just to let her know that her shopping cart is still empty and so she cannot checkout. Just like the main page ( index.php ) the checkout page "out sourced" almost everything to other pages. The main part of this file is the switch to load appropriate file depending on which checkout step the customer is on. And now, let's take a better look at these checkout processes one step at a time.
In case the shipping and payment info are the same, the customer can tick the checkbox that says 'Same as shipping information' and using javascript the payment info fields will have the same value as the shipping info. It's just added for convenience so the customer don't have to repeat all those typings. And lastly she must choose the payment method. The shop can handle payment using Paypal but if the customer is not comfortable with it she can just choose Cash on Delivery. When the form is submitted we don't save the info to database yet. They are just passed to the next page ( the order confirmation page ) as hidden input. Showing the confirmation page is important so that the customer can recheck their info and make sure everything is okay.
Checkout Confirmation
On the order confirmation page we display the ordered items with the shipping and payment information provided earlier. You can't see it in the screenshot but you can be sure that the shipping and payment info is there as hidden inputs. If you don' believe just checkout the source code
If the customer want to modify the shipping/payment info she can click on the "Modify Shipping/Payment Info" and if everything is okay she click on the 'Confirm Order' button and so we begin saving the order to database. First, we insert the shipping and payment info to tbl_order and the ordered items into tbl_order_item. We also modify the product quantity in stock. If you play with the demo you can see that this is disabled so the product quantity is always the same no matter how many items you ordered. Next thing we do is remove the ordered items from cart table because they are no longer needed.
If the payment method is COD the customer will go immediately to the thankyou page. But when paypal is chosen the process is a bit longer. Next we'll start playing with the Paypal stuff
paypal.inc.php : containing the configuration variables and paypal specific functions payment.php : this is the form that contain the hidden inputs to send to paypal server ipn.php : checks the POST from paypal and update the database accordingly
Before going straight to the code please take a look at the picture below. It displays the checkout flow starting from the checkout confirmation page up to the thankyou page. The ones with blue background takes place on the merchant site ( plaincart.com ) and the ones with white background takes place in Paypal website.
Here is what happen on each stage : 1. Checkout Confirmation After rechecking the shipping and payment information on this page the customer click on the "Confim Order" button.
2. Submit Payment Info An HTML form is generated containing the payment information ( order id, amount, etc ). You will probaly only see a message like this : "Processing Transaction ... ". But if you take a look at the source you can see the HTML code containing the form and some hidden inputs. This form is submitted to paypal site during page load so if you have a fast connection you may only see it for a split second. The file that generate these hidden inputs is include/paypal/payment.php which is included from checkout.php. You can see that payment.php is not doing anything complex. It just prints the hidden inputs with values coming from the paypal configuration in paypal.inc.php plus the order id, order amount and the item name ( which is hardcoded to "Plaincart Purchase" ).
3. Paypal Login Page Once on the Paypal site you will be asked to login or create a new account. I already made a test account for this so you don't need to signup with paypal just to test this. You can use use this information to login : Email Address: testme@phpwebcommerce.com PayPal Password: phpwebco 4. Payment Detail After login you will see the payment detail page. It look something like this :
There's an optional message box that the customer ( you ) can fill in. Paypal will pass this message along with other payment information to merchat server ( this site ). When you go to the order page in the admin section you can see the message on the order detail page. To continue with the payment process just click on the "Pay" button. 5. Payment Confirmation The next screen you will see is the payment confirmation screen from paypal :
6. Thank You Page Once you click the "Continue" button Paypal will redirect the customer back to the merchant site ( this site ). Since i'm not using a secure connection ( no https:// stuff ) there will be a warning message about you're being redirected to unsecure site. You can ignore it and just click on "Continue" and you will see the thankyou page. Here is the screenshot :
The thankyou page can only be accessed when an order id is found in the session. The order id is put in the session variable after you click the "Confirm" button on the checkout confirmation page. If no order id is found then the customer is sent directly to the shop main page. On this last step of checkout an email is sent to the shop admin notifying about the new order. This behaviour can be changed from the admin page on the shop configuration section.
Here is the code for the thankyou page Source : success.php <?php require_once 'library/config.php'; // if no order id defined in the session // redirect to main page if (!isset($_SESSION['orderId'])) { header('Location: ' . WEB_ROOT); exit; } $pageTitle = 'Checkout Completed Successfully'; require_once 'include/header.php'; // send notification email if ($shopConfig['sendOrderEmail'] == 'y') { $subject = "[New Order] " . $_SESSION['orderId']; $email = $shopConfig['email']; $message = "You have a new order. Check the order detail here \n http://" . $_SERVER['HTTP_HOST'] . WEB_ROOT . 'admin/order/index.php?view=detail&oid=' . $_SESSION['orderId'] ; mail($email, $subject, $message, "From: $email\r\nReturn-path: $email"); } unset($_SESSION['orderId']); ?> <p> </p><table width="500" border="0" align="center" cellpadding="1" cellspacing="0"> <tr> <td align="left" valign="top" bgcolor="#333333"> <table width="100%" border="0" cellspacing="0" cellpadding="0"> <tr> <td align="center" bgcolor="#EEEEEE"> <p> </p> <p>Thank you for shopping with us! We will send the purchased item(s) immediately. To continue shopping please <a href="index.php">click here</a></p> <p> </p></td> </tr> </table></td> </tr> </table> <br> <br> <?php require_once 'include/footer.php'; ?> This is really an oversimplified thank you page. Normally an online shop also provide a link where the customer can check her order online or a notice that an order confirmation email has been sent ,etc. However i haven't made any of those so for now this is sufficient.
There's a "hidden step" between step 4 and 5. The customer can't see it because it happen between Paypal server and the merchant server. On this "hidden step" these things happen : 1. 2. 3. Paypal send a POST request to merchat server to notify about the payment. Then our server send a reply POST to let Paypal know that we have receive it. Paypal send a reply notifying the payment status whether it's verified or not
Paypal Configuration File The script that process the notification is located in include/paypal/ipn.php. But before talking about that file let's see the paypal configuration first. Source code : paypal.inc.php <?php $paypal = array(); $paypal['business'] = "armanpi@phpwebcommerce.com"; $paypal['site_url'] = "http://www.phpwebcommerce.com/plaincart/"; $paypal['image_url'] = ""; $paypal['success_url'] = "success.php"; $paypal['cancel_url'] = "error.php"; $paypal['notify_url'] = "include/paypal/ipn.php"; $paypal['return_method'] = "2"; //1=GET 2=POST $paypal['currency_code'] = "USD"; $paypal['lc'] = "US";
// ... other "not so important" settings down here ?> Here is the description for each configuration : 1. $paypal['business'] The email address that shown as the recipient when you pay $paypal['site_url'] The store's main url. It is NOT ALWAYS the same as the site's homepage url $paypal['image_url'] I'm not using any images to display on paypal site during the checkout process so i just left this blank $paypal['success_url'] When paypal can verify the customer's credit card and everything gone smoothly the customer will be sent back to this url $paypal['cancel_url'] And if something went wrong the customer are taken to this one $paypal['notify_url'] This is the main script that check the verification message sent by paypal. It also update the database if the payment is successful $paypal['return_method'] It sets the http method used by paypal to send the verification message $paypal['currency_code'] What currency will the customer be paying. Other currencies supported are GBP,JPY,CAD, and EUR
2.
3.
4.
5.
6.
7.
8.
9.
$paypal['lc'] The language ( locale ) $paypal['url'] We send all payment information to this url. For testing we use the sandbox url : https://www.sandbox.paypal.com/cgi-bin/webscr But when the store goes live we'll use the real paypal url : https://www.paypal.com/cgi-bin/webscr $paypal['post_method'] The function fsockPost($url,$data) can use several method to send transaction data. They are fso ( fsockopen() ), curl ( curl command line), and libCurl ( php compiled with libCurl support ). We're using fso because a hosting company may not provide PHP with curl support $paypal['curl_location'] If you're using curl as the post method you must supply the path to the curl command on your server
10.
11.
12.
Submitting The Payment Information From the second step on the flowchart above we post some form values to paypal server so they can process the payment. Below you can see the variables sent from payment.php and their description 1. business The merchant's email address. The payment will be sent to the paypal account identified by this email address. The value for this variable is taken from the configuration file ( $paypal['business'] ) amount The amount of payment that the customer must pay. invoice The order id. We need this so we can know which order is being paid item_name Since the customer can buy multiple items i just hard code this one to "Plaincart purchase". return The url where you want to send the customer to after the payment is complete ( $paypal['success_url'] ) cancel_return When the payment fails the customer is redirected here ( $paypal['cancel_url'] ) notify_url The url of the script that checks the IPN notification from paypal and send back a confirmation to paypal ( $paypal['notify_url'] ) rm The return method. The available values are 1 ( GET ) and 2 ( POST ). Since we use POST in this example the value of rm is 2. currency_code In what currency do you want to be paid in ( $paypal['currency_code'] ) . lc The language ( $paypal['lc'] ) cmd The value is _xclick which is hardcode. Paypal provides several kinds of payment processing services so we need to tell them what kind of service we're using. no_shipping Since we already ask the shipping address during checkout. We don't need to ask it again on the paypal site. So we set this value to 1. Use 0 ( zero ) if you want paypal to ask the shipping address instead.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
Processing The Payment Here is the code for the IPN script : Source code : include/paypal/ipn.php <?php if (strpos($_SERVER['REMOTE_ADDR'], '66.135.197.') === false) { exit; } require_once './paypal.inc.php'; // repost the variables we get to paypal site // for validation purpose $result = fsockPost($paypal['url'], $_POST);
//check the ipn result received back from paypal if (eregi("VERIFIED", $result)) { require_once '../../library/config.php'; // check that the invoice has not been previously processed $sql = "SELECT od_status FROM tbl_order WHERE od_id = {$_POST['invoice']}"; $result = dbQuery($sql); // if no invoice with such number is found, exit if (dbNumRows($result) == 0) { exit; } else { $row = dbFetchAssoc($result); // process this order only if the status is still 'New' if ($row['od_status'] !== 'New') { exit; } else { // check that the buyer sent the right amount of money $sql = "SELECT SUM(pd_price * od_qty) AS subtotal FROM tbl_order_item oi, tbl_product p WHERE oi.od_id = {$_POST['invoice']} AND oi.pd_id = p.pd_id GROUP by oi.od_id"; $result = dbQuery($sql); $row = dbFetchAssoc($result); $subTotal = $row['subtotal']; $total = $subTotal + $shopConfig['shippingCost']; if ($_POST['payment_gross'] != $total) { exit; } else { $invoice = $_POST['invoice']; $memo = $_POST['memo']; if (!get_magic_quotes_gpc()) { $memo = addslashes($memo); }
$sql = "UPDATE tbl_order SET od_status = 'Paid', od_memo = '$memo', od_last_update = NOW() WHERE od_id = $invoice"; $result = dbQuery($sql); } } } } else { exit; } ?> Right at the top of the script we check if the ip address of the page requester. Since this page is meant only to be accessed by paypal server we disallow any connection that didn't come from paypal. Paypal server's IP address begin with 66.135.197 so if we don't see that in the remote address we just assume someone trying to mess with our script and exit immediately. If the request does come from paypal we send a reply POST to let Paypal know that we have receive their message. Upon receiving our reply Paypal send another reply notifying the payment status whether it's verified or not. If we found the word VERIFIED in the reply that means payment has been made and we move on updating the status for the order Here is the series of checking that the script perform : 1. 2. 3. 4. Check the database to see if the invoice id really exist If it does exist check the order status is still 'New' to prevent double action If it's a new order check the amount of money sent and make sure the currency is correct. When everything is okay we can update the order status to "Paid", add the buyer's memo and modify the update time.
That's it. Once the customer made her payment you can check the order on the admin page. When you click on the "Order" menu on the admin page you can see all paid orders. From there it's up to you as the store owner to decide what to do with those orders.
Creating Your Paypal Account If you want to test the shopping cart script on your own server you should create your own developer account. Here are the steps to setup a developer account with paypal : 1. 2. 3. Go to https://developer.paypal.com ,click on on "Sign Up Now" and complete the registration Paypal will send you a verification email. Click the link in the email to verify your email address Login to PayPal Developer Central
4. 5.
6. 7. 8. 9. 10.
Create a sandbox account by clicking on the "Sandbox" tab Click on "Create Account". Anew window will popup. On the popup screen choose "Personal Account", select a country from the list then hit "Continue". This is just for testing so you can enter false email and use false data to fill in the form. Don't forget to enter the characters in the security image (it's case insensitive) On the next screen just hit the Complete the 'extra ...' stuff Check your paypal inbox Open the welcome email and click the verification link Open the 'extra ...' email and click on the link then enter the number
You will still need to create a "normal" Paypal account not only the developer account before testing. If not all payment status will be "Pending" instead of "Completed". If you don't already have it yet here are the steps to create and configure your paypal account : 1. 2. 3. 4. 5. Create a business account with paypal ( the personal account is not enough ) and add a credit card so your account is verified. If you don't add a credit card then IPN will always say that the payment status is "Pending" Login Go to My Account > Profile > Instant Payment Notification Preferences Click on the "Edit" button Check the checkbox and enter the url where we want to receive the IPN notifications ( in our case it's http://www.phpwebcommerce.com/plaincart/include/paypal/ipn.php) then hit "Save"
Since our script already pass the hidden input called notify_url, which is the url of the script that process IPN notifications, this step is actually not required anymore. I just put this for the sake of completeness.
I did try to make checkout process as simple as possible but somehow i feel it gets quite complex now. If you found something weird / not clear please contact me. Howevert if you're question is about paypal it would be a lot better to ask the questions in the paypal forum especially the IPN section. This forum is packed with paypal experts.
4.
By the way, someone asked me about the license for the code so here it is : The code is free to use, modify, or enhance. If you want to show you appreciation by placing a link to this site then i'll be very very very grateful :-). If you don't want to link to this site it's allright because the code was meant to be free anyway. Below is the list of the shopping cart files. I only list the php files excluding the css and javascript files.
source/cart.php source/admin/product/modify.php source/admin/product/detail.php source/admin/product/index.php source/admin/product/processProduct.php source/admin/product/list.php source/admin/product/add.php source/admin/processLogin.php source/admin/user/modify.php source/admin/user/processUser.php source/admin/user/changePass.php source/admin/user/index.php source/admin/user/list.php source/admin/user/add.php source/admin/main.php source/admin/category/modify.php source/admin/category/processCategory.php source/admin/category/index.php source/admin/category/list.php source/admin/category/add.php source/admin/index.php source/admin/library/functions.php source/admin/login.php source/admin/include/header.php
source/admin/include/template.php source/admin/include/footer.php source/admin/config/main-lama.php source/admin/config/processConfig.php source/admin/config/main.php source/admin/config/index.php source/admin/order/processOrder.php source/admin/order/detail.php source/admin/order/index.php source/admin/order/list.php source/checkout.php source/error.php source/success.php source/index.php source/library/errorMessage.php source/library/common.php source/library/config.php source/library/database.php source/library/product-functions.php source/library/checkout-functions.php source/library/category-functions.php source/library/cart-functions.php source/plaincart.sql source/db/db.sql source/include/productList.php source/include/header.php source/include/footer.php source/include/top.php source/include/categoryList.php source/include/paypal/process.php source/include/paypal/payment-old.php source/include/paypal/orderform.php source/include/paypal/error.php source/include/paypal/paypal.inc.php source/include/paypal/success.php source/include/paypal/Backup 1 of ipn.php source/include/paypal/testpost.php source/include/paypal/ipn/ipn_error.php source/include/paypal/ipn/ipn_success.php source/include/paypal/ipn/Copy of ipn.php source/include/paypal/ipn/Backup 1 of ipn.php source/include/paypal/ipn/Backup 1 of Copy of ipn.php source/include/paypal/ipn/ipn.php source/include/paypal/ipn.php source/include/paypal/cancelled.php source/include/paypal/payment.php source/include/paypal/includes/config.inc.php source/include/paypal/includes/global_config.inc.php source/include/Copy of shippingAndPaymentInfo.php source/include/productDetail.php source/include/thankyou.php source/include/leftNav.php source/include/checkoutConfirmation.php source/include/shippingAndPaymentInfo.php source/include/miniCart.php
2.
3.
4.
5.
Conversion Chronicles Plenty articles about increasing your online shop conversion. Site Build It! All in one solution for creating websites that get traffic.You can use SBI to create content sites that warm up your potential customers before sending them to your online shop PHP MySQL Tutorial My other site. I'm sure you know what this site has to offer from reading the name :-)
6.
7.
PHP Homepage
Of course, I have to list this site here. This is where you can to download the PHP bundle and documentations too It also has a lot of other important resources such as links to various PHP projects. You can also subscribe to PHP mailing list from this website. There mailing list are divided to cover specific issues, so you may want to choose the most appropriate to your needs.
MySQL Homepage
Here you can download the latest MySQL release, get the MySQL news update. The mailing list is also a great resources for anyone who want to build dynamic websites using MySQL, so don't to join.
Zend
PHP is powered by the Zend engine. This website is the official homepage of the company who built the engine. Here you can get the Zend Optimizer. It's a very useful tool which can give your PHP scripts a 40-100% increase in speed. It also has PHP programming contest, a directory of free PHP scripts and applications, articles and tutorials. Zend also offer a PHP Certification. They said it can "Differentiate yourself from competitors when looking for a new job or at your annual salary review". Maybe I'll try to get that certification and see if I can get a better salary :-). By the way, you can get a free chapter of the Zend PHP Certification Guide in Zend homepage. It's really an interesting reading.
PHPBuilder
Great articles and tutorials in PHP programming. Anyone from a beginner to an expert can really learn from this site.
Scriptlance
One of the best place to find freelance programming jobs. Last time i check there are over 300 open project related to PHP. It' really a good place to find freelance projects
DevShed
Lots of resources for open source stuff. Not all articles here is related to PHP or MySQL. Actually you can get a whole breed of web development issues here.
Eclipse.org
My favourite PHP editor. It's heavy ( use lots of memory ) but still great. Built in support for CVS ( Concurrent Versioning System ) makes it really easy to manage projects with multiple developers. By the way if you want to use Eclipse don't forget to get the PHP plugin from sourceforge.net or from phpeclipse.de
PHPPatterns.com
Learn how to apply Design Pattern in PHP. If you already learn Object Oriented Programming (OOP) in PHP this site can help you applying the concept of OOP better. There's a funny article here about how far you can go in applying patterns. It's about writing a Hello World script in the super hard way.
Vision.To Design
Web Applications, PHP/MySQL Advanced Solutions, Web Design
dbQwikSite
It's a software that generates PHP code against your MySQL database so you an speed up your coding.
ScriptSearch.com
You can find free scripts, source code, books and code examples in here.
PHPFreaks.com
PHP and MySQL Resources. They have a linux forum too