Overview
By the end of this chapter, you will be able to perform filesystem-related operations (read, write, copy, move, and remove); read big files line by line and read CSV files one record at a time; download files via the browser using PHP; connect to MySQL RDBMS using PHP; create a database and a table, and insert records into a MySQL database using PHP; query, update, and delete data from MySQL DB using PHP; and secure MySQL queries using prepared statements in PHP.
In the previous chapter, we saw how to deal with user input using PHP superglobals and applied sanitization and validation in order to secure the application. We also learned how to keep the user's session on the server and built a small application. In that application, we used the session to store data, which vanished with every session destroyed (or logout).
In this chapter, we will learn how we can store and read persistent data using PHP. Specifically, we will learn how to handle file I/O (open, read, and write) and disk operations (change the current directory, create a new file/directory, remove a file or directory, and so on). This is useful when you would like to use the filesystem for precious application logs, to generate all kinds of reports, to handle uploaded user images, and so on. We will also learn how to connect to a MySQL database and how to query the data, insert new records, and update or delete data from the database. This is helpful when you want to store data in a structured way, which can then be easily accessed by many other applications; for instance, user-specific data, such as a first name, a last name, an email address, and password hashes. And not only this – most probably, your application will perform data manipulation, on the data which will be stored somewhere to have it ready to read on request. This kind of data might represent elements of the business domain and could include product lists, prices, discount coupons, orders, subscriptions, and suchlike. We will deal with security in this chapter as well. Hence, we'll learn how we can protect our database against potentially malicious user input.
Filesystem operations are some of the most important in programming. We can enumerate session data storage in PHP; user-uploaded files, generated report files, cached data, logs – all of them utilize the filesystem. Of course, there are many other alternatives for persistent storage, but knowing how to operate the filesystem in a language is especially important due to its availability. It is basically present anywhere and can be used immediately.
Working with the filesystem, sometimes, you might want to read or write into a file that is stored in a known location relative to the script file location. For example, for a script that is created in the /app/demo/ directory that wants to read files from source/ relative to its location (in other words, /app/demo/source/), it would be better to know the script location.
This is different to the current working directory, because you may run the script from other locations as well. For example, if the current working directory is /root, you can run the script providing one of the following is present: the relative path, php ../app/demo/the-script.php, or the absolute path, php /app/demo/the-script.php. In this case, the current working directory is /root, while the script directory is /app/demo.
This leads to the next point. PHP offers some “magic constants”; values of which change across the scripts depending on where are they used. The list of magic constants is as follows:
In our case, we would want to make use of the __DIR__ constant in the script. The directory the script would have to look into would be $lookupDir = __DIR__ . '/source';.
Dealing with files in PHP is one of the easiest things to do. PHP has several functions to handle file operations for creating, reading, and updating/editing. No additional installation is needed to use PHP filesystem functions.
One of the simplest functions to use for reading a file is file_get_contents(). This function can be used to fetch all the content of a file and put it into a variable, for example. The syntax is as follows:
file_get_contents (string $filename [, bool $use_include_path = FALSE [, resource $context [, int $offset = 0 [, int $maxlen ]]]])
The file_get_contents() function reads the file content into memory before giving any output, until the entire file has been read. This is a drawback that makes this function unsuitable for use when the input file size is not known. In the case of large files, let's say in excess of 1 GB, the PHP process would very quickly fill the allocated RAM memory, and this would make the script crash. Therefore, this function is only suitable for use when the expected file size is smaller than the memory_limit configuration entry in PHP.
Let's say you are required to develop a script that will be able to import a short list of users from a CSV format file into a current application.
First, let's prepare the environment:
<?php echo file_get_contents(__DIR__ . '/sample/users_list.csv');
We are invoking the file_get_contents() function, specifying the file path, and what we are receiving is the full file content. For the file path, we are using the __DIR__ magic constant, which gets replaced with the file directory path at compile time.
Save the preceding PHP script in a file called file_get_contents.php in the parent directory of the sample directory.
You will get the CSV file output, as above.
As discussed previously, the file_get_contents() function is not suitable for use on large files, since the entire file content is first read into memory, before any output, which would make the script very inefficient in terms of resource usage, as well as in terms of performance.
In the following exercise, we will explore some functions that will allow us to parse large files, keeping the system memory safe. This means we will use a technique that will allow us to read chunks of the file content at a time, which can be achieved using a group of PHP built-in functions, and a data stream PHP resource. A resource in PHP is a reference to the external resource; in our case, it will be a reference to a data stream resource (for example, a system file, or a URL).
fopen() is one of PHP's built-in functions, used to create stream resources in PHP. To achieve greater flexibility with regard to working with files (or any other data stream), we will use the fopen() function. The fopen() function accepts two required arguments, $filename being the first argument, and the access mode being the second one. The access mode describes the stream resource access type (read, write, read and write) and resolves to a set of instructions while creating the stream. It can have one of the following values:
You will notice the “file pointer” concept in the preceding table. You can think about this simple yet powerful concept in the same way as the cursor in a text file. So, for example, if we deal with the stream resource of a file with the Learning PHP fundamentals content, having the file pointer on position nine means it is located right before the word PHP. Reading the stream from that position until the end would result in the PHP fundamentals output.
The fopen() function returns a file pointer resource or false if the operation fails.
To read from the data stream, we will use the fread() function. This function requires two parameters, a resource variable being the first, and the length of bytes to read. It returns the read string or Boolean as false in the event of failure.
Other functions that can be used to read from stream resources are fgets() and fgetcsv(), to name a couple. fgets() returns a line from the file pointer; it requires the stream resource as the first parameter and accepts the optional read length (bytes) as the second parameter. fgetcsv() is similar to fgets() – it returns a line of data as an array containing the read CSV fields, except this line is parsed data as CSV (meaning more than one line of string data might be read, since one CSV field can contain multiline data). The fgetcsv() function accepts several parameters, but the required stream resource (the first parameter) is often enough to do a good job of parsing and returning CSV line data.
While reading from a stream, we might want to know when the end-of-file is hit. We can use the feof() function for this, which will test for the file pointer's presence at the end of the file (EOF). This function returns true if the file pointer is at EOF or errors occurred. It returns false otherwise.
Note
feof() returns false for invalid streams as well, so it is recommended to test your stream resource before invoking feof().
Let's suppose you are asked to optimize your user's import script in order to work with large data files of the magnitude of tens of gigabytes:
<?php
$filePath = __DIR__ . '/sample/users_list.csv';
$fileResource = fopen($filePath, 'r');
if ($fileResource === false) {
exit(sprintf('Cannot read [%s] file.', $filePath));
}
Note
To fine-tune this value, you should test it with a specific size range of files, depending on the usage.
We also define the iterations variable, to learn about the number of cycles when the file was read using the specified read length. Note that defining the $iterations variable is not necessary for production-grade code. We are including it here purely for educational purposes:
$readLength = 64;
$iterations = 0;
while (!feof($fileResource)) {
$iterations++;
$chunk = fread($fileResource, $readLength);
echo $chunk;
}
fclose($fileResource);
echo sprintf(“ %d iteration(s)”, $iterations);
Since the file contains 65 characters and the chunk size was set to 64, the file was read twice. This means that, at the first iteration, fread() filled the memory with 64 bytes of data that was then returned and the occupied memory was freed; at the second iteration, fread() filled the memory with 1 byte (the remaining file content) before it returned this and freed the memory. The advantages of this approach are that we can operate with small pieces of content at a time, at each read iteration, using a small amount of memory resources, rather than loading the whole file in memory and then iterating and processing content line by line.
In previous examples, we saw the difference between the two approaches to reading a file, but here, you will evaluate metrics to benchmark each of the file reading methods.
We will use the same scripts but will add a number of measurements.
We will make use of the memory_get_peak_usage() function to retrieve the peak memory usage at some point, as the name suggests. This function accepts one optional argument, set to false by default when its value is not specified; you should set it to true when you want the allocated memory to be reported (which we will do in the following exercises), rather than the actual memory usage.
In the following exercises, we will make use of the DIRECTORY_SEPARATOR constant, which exists in PHP implicitly, and which is set with the directory separator as follows:
In this exercise, we will evaluate metrics to benchmark each of the file reading methods:
Note
dd is a command-line utility for Unix and Unix-like operating systems that exists in any of these distributions.
dd if=/dev/zero of=sample/test-256-mb.txt count=1024 bs=262144
This file will most likely terminate the script that uses file_get_contents() to read it, since most PHP installations, by default, do not allow a memory limit of more than 128 MB per process. This limit is stored in the php.ini configuration file by default, under the memory_limit parameter, as previously indicated. Hence, we will create another file, 10 MB in size, using dd if=/dev/zero of=sample/test-10-mb.txt count=1024 bs=10240.
<?php file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . $argv[1]);
echo sprintf(“-- memory %.2fMB -- ”, memory_get_peak_usage(true) / 1024 / 1024);
Here, we are making use of the first command-line argument ($argv[1]), which will be the file path to read, relative to the script path. We are adding the memory peak metric as well, using the memory_get_peak_usage() function.
time php file_get_contents-memory.php sample/test-10-mb.txt
You should essentially get the following output:
--
memory 12.01MB
--
real 0m 0.03s
user 0m 0.02s
sys 0m 0.01s
Note
We have used the time Linux command here, which will run the command and print the resource usage.
The memory value of 12.01 MB in this example output is reported by the memory_get_peak_usage() function and it shows us that this is the RAM memory amount necessary for a PHP script to read a 10 MB file.
time php file_get_contents-memory.php sample/test-256-mb.txt.
In the output, we will see an error message like this:
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 268443680 bytes) in /app/file_get_contents-memory.php on line 1
As expected, trying to read a 256 MB file into memory fails because the limit of 128 MB per process is exceeded.
<?php
$filePath = __DIR__ . DIRECTORY_SEPARATOR . $argv[1];
$fileResource = fopen($filePath, 'r');
if ($fileResource === false) {
exit(sprintf('Cannot read [%s] file.', $filePath));
}
$readLength = $argv[2] ?? 4096;
$iterations = 0;
while (!feof($fileResource)) {
$iterations++;
fread($fileResource, $readLength);
}
fclose($fileResource);
echo sprintf(“-- %d iteration(s): memory %.2fMB -- ”, $iterations, memory_get_peak_usage(true) / 1024 / 1024);
What has changed from the previous file_get_contents-memory.php script is that we are reading chunks of data one at a time from the file, using the $readLength variable.
time php fread-memory.php sample/test-10-mb.txt
The output is as follows:
--
2561 iteration(s): memory 2.00MB
--
real 0m 0.05s
user 0m 0.02s
sys 0m 0.02s
As we can see, to read the entire 10 MB file, it took 2,561 read iterations of 4 KB (the second script argument is missing, and the default 4,096 bytes are set for the $readLength variable). The total duration of the script was 0.05 seconds, compared to 0.03 seconds when using file_get_contents(). The main difference to note is the memory usage – 2 MB, which is the minimum the PHP script allocates per process, compared to 12.01 MB when using the file_get_contents() function.
time php fread-memory.php sample/test-10-mb.txt 1048576
The output is now as follows:
--
11 iteration(s): memory 4.00MB
--
real 0m 0.03s
user 0m 0.02s
sys 0m 0.01s
Now, the entire 10 MB file read used only 11 iterations, with a peak of 4 MB of RAM memory. This time, the script took 0.03 seconds, as in the case of using the file_get_contents() function.
time php fread-memory.php sample/test-256-mb.txt
The output is as follows:
--
65537 iteration(s): memory 2.00MB
--
real 0m 0.30s
user 0m 0.16s
sys 0m 0.13s
In this case, the read length is 4 KB, and the complete file read required 65,537 iterations, using a peak of 2 MB of memory. The script took 0.3 seconds to read the entire file, which is not bad, but could be improved by increasing the read length to a bigger value; and this is what we will do in the next step.
time php fread-memory.php sample/test-256-mb.txt 1048576
The output is now this:
--
257 iteration(s): memory 4.00MB
--
real 0m 0.08s
user 0m 0.02s
sys 0m 0.05s
As expected, the time needed to read the entire 256 MB file decreased (from 0.3 seconds to 0.08 seconds), since the read length is higher (1 MB versus 4 KB, resulting in peak memory usage of 4 MB versus 2 MB), and the number of iterations required decreased to 257.
Now, having a look at this data, we can come up with our own ideas as to what is happening behind the scenes. In the case of file_get_contents(), a peak of 12.01 MB memory is used reading the 10 MB file; that's because the whole file was loaded into memory using this approach. The 256 MB file caused the script shutdown because the limit of 128 MB was hit.
On the other hand, it seems the fread approach did pretty well, both in terms of duration and memory usage. Reading the 10 MB file in chunks of 4 KB, the script uses 2 MB of memory, compared to 12 MB in the case of file_get_contents, while the read time is significantly bigger (0.05 for fread() versus 0.03 for file_get_contents()). Reading the same file though, but in chunks of 1 MB, we get similar results in terms of performance, but we still use much less memory than in the case of file_get_contents (4 MB versus 12 MB).
Now, what happens when we increase the scale a bit? Reading the 256 MB file was not possible with file_get_contents() on account of exhausted memory. But look at the second approach – not only is the file read entirely but also, only 2 MB of memory is used for this process! It takes about 0.3 seconds to read, which is not very satisfactory, but let's see what happens when the read length is increased and, therefore, the number of iterations is decreased. We get much better results now – a read time of 0.08 seconds and a memory peak of 4 MB.
As you can see, the convenient way – using file_get_contents() – is more suitable for small or very small files, whereas dealing with large files requires you to use different approaches, such as fread(), which reads chunks of data; fgets(), which gets an entire line at a time from the file pointer; and fgetcsv(), which is similar to fgets() but, in addition, parses the CSV string line into an array with data.
As indicated earlier, there are more ways to perform optimized reading from big files. In the following exercise, you will learn how to use PHP to read a file line by line. This helps especially when one entry record corresponds to one line, as in access or error logs, for example, so that reading the file allows one data record to be processed at a time.
In this exercise, we will open a file and read it line by line:
<?php
$filePath = __DIR__ . '/sample/users_list.csv';
$fileResource = fopen($filePath, 'r');
if ($fileResource === false) {
exit(sprintf('Cannot read [%s] file.', $filePath));
}
$lineNumber = 0;
while (!feof($fileResource)) {
$lineNumber++;
$line = fgets($fileResource);
echo sprintf(“Line %d: %s”, $lineNumber, $line);
}
fclose($fileResource);
echo PHP_EOL;
Line 1: John,Smith,2019-03-31T10:20:30Z
Line 2: Alice,Smith,2019-02-28T12:13:14Z
Line 3:
As you will notice, we have a line without content – that is actually an empty line in a CSV file. Please pay attention when dealing with file lines when trying to process data; check for a non-empty line at least before proceeding with processing.
The previous example shows a handy way to read one line at a time from a file. It turns out in our case that it's about a CSV file, a very simple one, with a comma as a delimiter, and that's pretty much it. But what if you have to deal with a complicated CSV document? Luckily, PHP provides a built-in function for that, called fgetcsv(). Using it, we can get one record at a time; that's right, one record, not one line, as the record can be spread over several lines, containing enclosed data (for example, multiline data wrapped between quotes).
In this exercise, we will read the data from CSV files:
<?php
$filePath = __DIR__ . '/sample/users_list_enclosed.csv';
$fileResource = fopen($filePath, 'r');
if ($fileResource === false) {
exit(sprintf('Cannot read [%s] file.', $filePath));
}
$recordNumber = 0;
while (!feof($fileResource)) {
$recordNumber++;
$line = fgetcsv($fileResource);
echo sprintf(“Line %d: %s”, $recordNumber, print_r($line, true));
}
fclose($fileResource);
echo PHP_EOL;
John,Smith,2019-03-31T10:20:30Z,”4452 Norma Lane
Alexandria
71302 Louisiana”
Alice,Smith,2019-02-28T12:13:14Z,”4452 Norma Lane
Alexandria
71302 Louisiana”
As you will notice, the fgetcsv() function does a very good job, parsing the CSV entries for us correctly. It does not matter whether the CSV content has a custom delimiter, enclosure, or escape character; all these parameters can be passed as function arguments to fgetcsv() to make the parser understand the format and perform the appropriate parsing
We saw how we can make the script read the files using a variety of means in order to allow us to do something with that content. But there is also downloading, when we need the file to be read by the script and sent back to the user, as a response to the HTTP request, and we don't want the PHP process to overload the memory by doing this, something along the lines of reading in chunks and sending the user small pieces at a time. Fortunately, there is a function for that, which is called readfile(). This function reads the file and writes it directly to the output buffer. The readfile() function requires only the file path to read from. The other optional arguments are a Boolean, which tells the function to search for the file in the include_path of PHP, and a context stream resource as a third argument.
A context stream is a set of options for a specific wrapper (a piece of code that builds other code) that modify or enhance the behavior of a stream. For example, when we want to read a remote file, using FTP, we pass the file path as the first argument of the readfile() function, and a valid FTP context stream variable as a third argument. We will not use context streams in the following exercises.
In this exercise, we will download a file and save it to the specified destination using PHP:
<?php
$filePath = 'sample/users_list.csv';
header('Content-Type: text/csv');
header('Content-Length: ' . filesize($filePath));
header(sprintf('Content-Disposition: attachment; filename=”%s”', basename($filePath)));
readfile($filePath);
Make sure you have started the built-in server in this directory (which is /app in my case) running php -S 127.0.0.1 in your Terminal, and that the file exists.
Note
One should check whether the file exists on disk or not and treat each case accordingly. When the file is missing, readfile() will output nothing and the browser might receive the output of the PHP script (output of download.php in our case).
Writing files with PHP is possible using a variety of methods, the majority involving the fwrite() and file_put_contents() built-in functions.
The fwrite() function accepts two required arguments, the first is the file pointer, and the second one is the string to write to the file. The function returns the number of bytes written or the Boolean false in the event of failure.
file_put_contents() is the equivalent of calling the fopen(), fwrite(), and fclose() sequence.
Note
When a file is written several times in a single PHP process, the fwrite() method is preferred, for performance reasons, since the stream resource is reused, and the file open and close operations (fopen() and fclose()) are avoided for each write as it happens with the file_put_contents() function. A good example of using fwrite() over file_put_contents() is the case of file loggers, when a PHP process might write several times in the same file during its lifetime.
The first required argument is the filename, and the second one is the data to write to the file. The data can be a string, a resource stream, or a single dimension array of strings, rows of which are written in sequence. The third argument is optional and accepts the flags for a write operation. This can be any combination of the following values:
When using the fwrite method, we may want to use the same data stream resource to read from; for example, to move the pointer at the beginning of the file after writing, or to read the last N bytes of data. In this case, we would use the fseek() function. This function sets the file pointer (remember the cursor analogy before?) to a specific position. The function signature is as follows:
fseek(resource $handle, int $offset [, int $whence = SEEK_SET ]) : int
The new position, measured in bytes, is obtained by adding an offset to the position specified by $whence.
$whence values can be:
In the following exercise, we will perform write operations in files using both of the fwrite() and file_put_contents() functions described previously:
<?php
$fileFwrite = 'sample/write-with-fwrite.txt';
$fp = fopen($fileFwrite, 'w+');
$written = fwrite($fp, 'File written with fwrite().' . PHP_EOL);
First, we define the file path to write to, and then we open the file pointer using the fopen() function.
Note
Always make sure to have the directory structure created before trying to open or put content into a file. Following our example, you should make sure the sample/ directory exists in the current working directory.
if (false === $written) {
echo 'Error writing with fwrite.' . PHP_EOL;
} else {
echo sprintf(“> Successfully written %d bytes to [%s] with fwrite():”, $written, $fileFwrite) . PHP_EOL;
fseek($fp, 0);
echo fread($fp, filesize($fileFwrite)) . PHP_EOL;
}
If the write fails ($written is the Boolean false), then we print an error message and continue the script. Otherwise, we print the success message, indicating the number of bytes written. After that, in order to read from the file, we move the pointer at the beginning of the file, at position zero, using the fseek() function. Then, we just print the file content to test the written data.
$fileFpc = 'sample/write-with-fpc.txt';
$written = file_put_contents($fileFpc, 'File written with file_put_contents().' . PHP_EOL);
if (false === $written) {
echo 'Error writing with fwrite.' . PHP_EOL;
} else {
echo sprintf(“> Successfully written %d bytes to [%s] with file_put_contents():”, $written, $fileFwrite) . PHP_EOL;
echo file_get_contents($fileFpc) . PHP_EOL;
}
Note
The whole script can be referred at https://packt.live/2MCkeOJ.
In this exercise, we wrote string sequences in two different files using two different methods – file_put_contents() and fwrite().
Congratulations! You just managed to write files using PHP.
We have seen how it is possible to write fresh content in files, but often, you just want to add to an existing file – think about some sort of log, for example. In this exercise, you will learn how it is possible to append content to a file, using PHP:
$fp = fopen($fileFwrite, 'a+');
$written = file_put_contents($fileFpc, 'File written with file_put_contents().' . PHP_EOL, FILE_APPEND);
Running the script over and over again will print you the same success message, and, with each run, the number of sentences will increase in each file, due to the append instruction.
Appending content in files is very useful in the case of logging and generating content in files in order to perform further downloads, to name but a couple of use cases.
PHP offers generous support when it comes to handling filesystems. All of the functions can be explored at https://packt.live/2MAsLmw. In addition, we will cover some of the most widely used filesystem functions in PHP.
unlink() is the delete files function. It requires the file path as the first parameter and accepts an optional context stream. It returns TRUE if the file is deleted successfully, or FALSE otherwise.
Before deleting a file, it is good to check first whether the file path points to an actual file, and, to achieve this, we can use the is_file() function. This function requires only the file path as the first parameter. It returns TRUE if a file is located and is a regular file, otherwise FALSE.
When working with file content in PHP, it is highly likely that you will want to clean some older files. In this exercise, we will write code to delete a file using PHP:
<?php
$filepath = 'sample/to-delete.txt';
if (is_file($filepath)) {
if (unlink($filepath)) {
echo sprintf('The [%s] file was deleted.', $filepath) . PHP_EOL;
} else {
echo sprintf('The [%s] file cannot be deleted.', $filepath) . PHP_EOL;
}
} else {
sprintf('The [%s] file does not exist.', $filepath) . PHP_EOL;
}
In this script, we check whether the file exists and is a regular file, using the is_file() function. In the case of a regular file, next, we test the file deletion; that is, the output of the unlink() function that is responsible for this, and then print the appropriate message based on the output. If the file does not exist, a message providing a notification of this will be printed.
The [sample/to-delete.txt] file was deleted.
Running the script again will print the following:
The [sample/to-delete.txt] file does not exist.
This means the delete operation was indeed executed successfully.
In this exercise, when running the script for the first time, all the conditions were met in order to run file deletion, and the file was indeed deleted. When running the script for the second time, the script cannot find the file for the specified path, so the script returns the file does not exist message immediately, prior to exiting.
On occasion, you may need to move files to a new location, for example, to the archive. This might be the case with a database data dump or log files, to name but two. PHP provides a function for moving functionality, called rename(), which requires the actual file path as a first argument, and the target file path as a second argument. This function returns TRUE if successful and FALSE in the event of failure, and can be used for both files and directories.
Sometimes, the target directory might not yet exist and, in these cases, it is supposed to be created with the script. There is a function for creating directories, called mkdir(), which accepts the following arguments: the directory path to create, the mode (which is 0777, by default, meaning full permissions for any user), a recursive directory creation instruction, and the context resource.
In this exercise, you will move a file to your local server, using PHP. Let's say you are assigned the task of creating a script that will move generated log files to an “archive location,” on a daily basis:
<?php
$filePath = 'sample/to-move.txt';
$targetDirectory = 'sample/archive/2019';
if (!is_file($filePath)) {
echo sprintf('The [%s] file does not exist.', $filePath) . PHP_EOL;
return;
}
if (!is_dir($targetDirectory)) {
echo sprintf('The target directory [%s] does not exist. Will create... ', $targetDirectory);
if (!mkdir($targetDirectory, 0777, true)) {
echo sprintf('The target directory [%s] cannot be created.', $targetDirectory) . PHP_EOL;
return;
}
echo 'Done.' . PHP_EOL;
}
$targetFilePath = $targetDirectory . DIRECTORY_SEPARATOR . basename($filePath);
if (rename($filePath, $targetFilePath)) {
echo sprintf('The [%s] file was moved in [%s].', basename($filePath), $targetDirectory) . PHP_EOL;
} else {
echo sprintf('The [%s] file cannot be moved in [%s].', basename($filePath), $targetDirectory) . PHP_EOL;
}
Note
The complete script file can be referred at : https://packt.live/35wmDmK.
The target directory [sample/archive/2019] does not exist. Will create... Done.
The [to-move.txt] file was moved in [sample/archive/2019].
Checking the file tree, you will notice that the file has indeed moved:
In addition to this, when running the script for the second time, you should get the following output:
The [sample/to-move.txt] file does not exist.
In this exercise, you succeeded in moving a file from one location to another, using PHP with its built-in filesystem functions, validating the input as well, so as to make sure that you were not attempting to move a non-existent file.
Copying files is yet another straightforward task for which PHP offers support. The copy() function accepts two required arguments – the source file path and the destination path, and an optional one – the stream context. Using the copy() function is very useful in scenarios such as choosing your profile picture from a list of available pictures on the server (in this case, you want to leave the picture list intact, so you will only want to create a copy of the selected picture), or restoring files copied from a backup (again, you want to leave the original files intact, so copy() is again appropriate in this case).
Note
Using the copy() function, if the destination file exists already, it will be overwritten.
You are required to write a script that will copy specific files to a backup location. The copied files should have the .bak extension prepended:
<?php
$sourceFilePath = 'sample/to-copy.txt';
$targetFilePath = 'sample/to-copy.txt.bak';
if (!is_file($sourceFilePath)) {
echo sprintf('The [%s] file does not exist.', $sourceFilePath) . PHP_EOL;
return;
}
First, we define the source and target file paths, and then check whether the source file exists. If the source file does not exist, an error message is printed and the execution of the script stops.
if (copy($sourceFilePath, $targetFilePath)) {
echo sprintf('The [%s] file was copied as [%s].', $sourceFilePath, $targetFilePath) . PHP_EOL;
} else {
echo sprintf('The [%s] file cannot be copied as [%s].', $sourceFilePath, $targetFilePath) . PHP_EOL;
}
Note
The complete script can be referred at https://packt.live/2plXtXu.
As you can see, copying files with PHP turns out to be a pretty straightforward process.
In this exercise, you learned how to deal with files using PHP, starting with file create and write, and continuing with append, rewrite and delete, and copy and move, and then reading large files line by line and sending files to download.
In the previous section, we saw how we can use PHP to manipulate and store data in files. But when an application relies on structured data, it gets pretty complicated using the filesystem, especially when the application grows, and so does your data. Imagine a social media website, with tons of relationships between the data, including post comments, interests, friendships, groups, and a plethora of other linked data. Also, as your application grows, scalability is an important factor. This is when you want to use a database, to be able to query the data in different ways – ordered, filtered, partial data, combined data (joined), and, at the same time, in a very performant way. A database management system (DBMS) is used for performing operations on database data (create, read, update, and delete). Also, since different types of data are related to other data types in a database, you may want accuracy, consistency, and reliability for your data storage. In this case, you would prefer a relational DBMS.
MySQL is a Relational Database Management System (RDBMS) and is the most commonly used with PHP. It is very fast, reliable, easy to use (it uses Structured Query Language (SQL) queries), and it's free to use. It suits a wide range of applications, from small to large. It's very powerful, fast, secure, and scalable.
A MySQL database stores data in tables, just like any other relational database. A table is composed of related data, organized in rows (records) and columns (record fields).
PHP supports a wide variety of databases, such as MySQL, PostgreSQL, SQLite, MongoDB, MSSQL, and others, but, in this chapter, we will work with MySQL, as it's by far the most widely used database management system with PHP.
Often, graphical user interface (GUI or “desktop applications”) clients are very useful when it comes to performing miscellaneous operations in a database, such as verifying data, altering tables or columns, exporting or importing data, and migrating a database.
For MySQL, three clients are recommended:
In addition, for screenshots, I'll use Workbench to test the data in MySQL Server, but any of these tools could be used.
To use MySQL Server with PHP, some extensions need to be installed. Usually, an extension is a component that exposes an Application Programming Interface (API) to the user, and which is used to perform specific tasks; in our case, a database-specific extension will be used to connect to MySQL, query update, and delete data, among other operations. In PHP, the two most commonly used extensions when working with MySQL are the mysqli and PDO extensions. These are very similar in terms of functionality and syntax, and, unless you need a specific feature from one of the extensions, choosing an extension to work with should not cause any difficulties. Just pick one.
Since PDO appears to be the most widely used option, we will pick this extension for further exercises.
PHP Data Objects (PDO) is a lightweight and lean interface for accessing databases with PHP.
To continue, make sure you have MySQL installed, as described in the preface. Furthermore, consider the MySQL server listening on 127.0.0.1, port 3306, with the username set to php-user and the password set as php-pass. Also, make sure you have the PDO extension and the pdo_mysql driver installed to facilitate the establishment of connections and send queries to the MySQL Server.
Note
The pdo_mysql driver is an extension that provides an interface to the aforementioned PDO extension. This driver is a component that makes communication with the MySQL Server possible, translating instructions between the two parties.
Checking for an enabled PHP extension in the Terminal is possible by running php -m to list all installed and enabled extensions or php -m | grep -i pdo to list only those entries that match the pdo string fragment. The latter should output these two entries:
Note
grep is a Unix function that searches for text in files or in string input, and returns the matching lines in output by default. The | (pipe) token is used to forward the previous command's output (php -m) to the next command, as input.
In order to proceed further, let's create a new directory where we will write the database-related exercises (for example, database).
Connections to MySQL are initiated by instantiating the PDO object. This accepts the database source (DSN) as the first argument, and optionally, the username, password, and PDO options, if required.
The syntax is as follows:
PDO::__construct(string $dsn [, string $username [, string $password [, array $options ]]])
Parameters:
The DSN allows the following elements:
By way of good practice, it is recommended to set the connection character set to utf8mb4; that will save you from further difficulties if you have to store and fetch UTF-8 characters using this connection (and you will have to, at some point).
One of the methods of the PDO class is getAttribute(), which returns a database connection attribute, such as server information and the connection status. The PDO::getAttribute() method requires and accepts only one parameter, the integer type; that is, one of the PDO::ATTR_* constants. For a complete list of PDO attributes and other constants, visit the official documentation page at https://www.php.net/manual/en/pdo.constants.php.
In this exercise, you will connect to a MySQL server using PDO.
<?php
$dsn = “mysql:host=127.0.0.1;port=3306;charset=utf8mb4”;
$options = [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
// PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
Note
We will learn about exceptions and error handling in the next chapter.
$pdo = new PDO($dsn, “php-user”, “php-pass”, $options);
echo sprintf(
“Connected to MySQL server v%s, on %s”,
$pdo->getAttribute(PDO::ATTR_SERVER_VERSION),
$pdo->getAttribute(PDO::ATTR_CONNECTION_STATUS)
) . PHP_EOL;
Connected to MySQL server v5.7.23, on 127.0.0.1 via TCP/IP
In the event of a connection failure, the output will look like this:
PHP Fatal error: Uncaught PDOException: SQLSTATE[HY000] [1045] Access denied for user 'php-user'@'127.0.0.1' (using password: YES) in /app/connect.php:8
Stack trace:
#0 /app/connect.php(8): PDO->__construct('mysql:host=127....', 'php-user', 'wrongpwd', Array)
#1 {main}
thrown in /app/connect.php on line 8
In the event of a connection failure, it would be better to treat the error and fall back gracefully to a nice-looking error page, providing a user-friendly error message. In this case, though, we will leave the script as it is now because PHP exceptions will be covered in the next chapter.
Here, you made a connection to MySQL, with a username and password, using PDO, and you set some options as well, for the PDO object. You also printed the server version and connection status, from the PDO connection attributes.
Now that we have learned how to establish a connection with a MySQL Server, let's move forward and see how we can create a database.
To do this, we will have to run SQL queries; this is where we get to use the PDO methods.
We will invoke the PDO::exec() method to send the SQL queries to MySQL Server. It requires and accepts only one parameter: the SQL query string, and returns the Boolean false in the event of an error, or the number of affected rows in the event of success.
Warning: Since this function can return a Boolean false and also 0 (zero), which evaluates to false, make sure you use the === or !== operator when testing the result, so as to avoid false positives when checking for errors.
In the event of a query failure (PDO::exec() returns false), we may invoke the PDO::errorInfo() method to get the error codes and the error message. This method returns a numeric array containing the following data:
The query to run in order to create a new database has the following syntax:
CREATE SCHEMA db_name, where db_name should be replaced with the name of the database you want to create.
Note
The CREATE SCHEMA string is an SQL statement. It can be executed in a SQL server using any SQL client. The syntax and more information can be found on the official documentation page at https://packt.live/32ewQSK.
In this exercise, we will create a database and run queries:
<?php
$dsn = “mysql:host=127.0.0.1;port=3306;charset=utf8mb4”;
$options = [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
$pdo = new PDO($dsn, “php-user”, “php-pass”, $options);
return $pdo;
This is similar to what we did in the previous exercise, except that instead of printing the connection information, we return the PDO instance. In this file, we do not specify a database name, since we have not yet created one.
<?php
/** @var PDO $pdo */
$pdo = require 'connection-no-db.php';
Then, we write our SQL query under the $sql variable, which will create a database with the name demo:
$dbname = 'demo';
$sql = “CREATE SCHEMA $dbname”;
if ($pdo->exec($sql) !== false) {
echo “The database '$dbname' was successfully created.” . PHP_EOL;
} else {
list(, , $driverErrMsg) = $pdo->errorInfo();
echo “Error creating the database: $driverErrMsg” . PHP_EOL;
}
Running the code successively, you will get the following error message:
In this exercise, you learned how we can create a database and how to test for the successful execution of the SQL statement, CREATE SCHEMA.
Let's now see how we can create a table that will actually hold the data in an organized way. We will use the CREATE TABLE SQL statement to achieve this. The syntax of this statement is more complex and also involves table column definitions.
Standard CREATE TABLE syntax is as follows:
CREATE TABLE [IF NOT EXISTS] tbl_name
(
col_name data_type [NOT NULL | NULL] [DEFAULT default_value] [AUTO_INCREMENT] [UNIQUE [KEY]] [[PRIMARY] KEY]
...
)
The parameters are as follows:
A sample CREATE TABLE query can be as follows:
CREATE TABLE users
(
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(254) NOT NULL UNIQUE,
signup_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
)
In this statement, we point to the table name – users, with three columns, as follows:
Warning
Be aware that the “current datetime” will be the value set using the MySQL Server time zone offset, which may differ from the application server. For example, when you deploy your application on a server from a data center that is located in a different time zone to yours, it is possible that the system time zone of the remote server is set to the local time zone offset. You may want to make sure that your server's settings do not apply time offset – using the UTC time zone, or you may want to use a timestamp value instead of a human-readable date.
You can find the full syntax and more information at https://packt.live/2MAGloG.
In this exercise, we will learn how to select a database with PDO, and how to create a table using the PDO instance:
<?php
/** @var PDO $pdo */
$pdo = require 'connection-no-db.php';
$createStmt = “CREATE TABLE users
(
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(254) NOT NULL UNIQUE,
signup_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
)”;
After executing the statement, in the event of failure, the error message will be printed and execution or the script will stop. Otherwise, a success message will be printed to output:
if ($pdo->exec($createStmt) === false) {
list(, , $driverErrMsg) = $pdo->errorInfo();
echo “Error creating the users table: $driverErrMsg” . PHP_EOL;
return;
}
echo “The users table was successfully created.”;
We get an error message, indicating that no database is selected. What we understand from this statement is that a MySQL server can store several databases, and, when executing a statement, we should indicate the database we want to run it into. To achieve this, we should either include the database name inside the SQL statements (for example, CREATE TABLE demo.users ...) or specify the database name inside DSN, before creating the connection to MySQL Server.
$dsn = “mysql:host=mysql-host;port=3306;dbname=demo;charset=utf8mb4”;
Note
We will require this connection.php file further, in all exercises, to reuse the code instead of typing this block of code in every file where we use the database connection.
$pdo = require 'connection.php';
Great! You successfully created the first table in the demo database.
In this exercise, you learned how to select a database at connection time, and how to create a table in a SQL database. Notice that the queries begin with an action (CREATE) followed by the object type (schema/database or table), followed by the object definition where required. Also, you probably noticed that the column names are followed by the date type declaration (integer, string, date, and so on) and then by additional constraints (NOT NULL, PRIMARY KEY, UNIQUE, and so on).
As you can see, SQL statements are pretty descriptive and easy to learn and remember. So, let's advance with more exciting examples!
Since we already know how to create a table in a MySQL database, let's add some data to it.
Before inserting data into a table, we must craft the script in such a way that the data to be inserted will match the table's column definition. This means we will not be able to store strings in a column defined with an integer data type. In such cases, MySQL Server will reject the query and will respond with an error. Also bear in mind, since most of the data will come from user input, that you should always validate it before sending it to a database server, and, at the same time, escape it properly, so as to avoid another security issue, called SQL injection, covered later in the chapter.
Standard INSERT statement syntax is as follows:
INSERT INTO tbl_name
(col_name [, col_name] ...)
VALUES (value_list) [, (value_list)] ...
Where value_list is:
value [, value] ...
Note
The number of values specified in value_list should match the col_name count. The complete syntax of the INSERT statement can be found on the official documentation page at https://packt.live/32fXkmP.
An example INSERT query may appear as follows:
INSERT INTO employees (email, first_name, last_name)
VALUES ('[email protected]','John','Smith'),
('[email protected]','Jane','Smith')
In this case, two rows will be inserted in the employees table, setting the values from VALUES to the corresponding position column from the column list; for example, [email protected] is assigned to the email column, and the John value is assigned to the first_name column.
In this exercise, we will become familiar with the INSERT statement, learning how we can add data to a table:
<?php
/** @var PDO $pdo */
$pdo = require 'connection.php';
$insertStmt = “INSERT INTO users (email) VALUES ('[email protected]')”;
if ($pdo->exec($insertStmt) === false) {
list(, , $driverErrMsg) = $pdo->errorInfo();
echo “Error inserting into the users table: $driverErrMsg” . PHP_EOL;
return;
}
echo “Successfully inserted into users table the record with id “ . $pdo->lastInsertId() . PHP_EOL;
This proves that the previous script execution succeeded, and that the UNIQUE constraint in the email column is working as expected.
As expected, we have a single row, with id = 1, [email protected] for the email column, and the signup time set by MySQL Server at the time of the row insertion.
Congratulations on adding in the initial data to a database table! It was pretty easy. Now, knowing that we should work with user input, we must ensure that the script will run the queries in complete safety, avoiding SQL injection, which may lead to data leaks and system compromise.
So, what is SQL injection anyway? SQL injection is one of the most common vulnerabilities in the wild web nowadays. It is a technique used to steal data, gain control of users' accounts, or destroy a database, and is performed by sending malicious query chunks via HTML form inputs.
To better understand this, here is a simple example of how you can drop a table using the SQL injection technique, given a query that accepts user input without sanitizing and/or validating it:
$rawInput = $_POST['email'];
$query = “INSERT INTO users (email) VALUES ($rawInput)”;
When the email input value is “”); DROP TABLE users; /**, then the query will become:
INSERT INTO users (email) VALUES (“”); DROP TABLE users; /**)
What happens is easy to understand; the INSERT statement is executed, adding an empty value to the email column, and then the query to drop the table is executed, making the users table vanish, while the /**) part is ignored, since /** marks the beginning of a comment in a SQL query.
In order to prevent SQL injection, we should escape the input data. PDO offers an alternative –so-called prepared statements (the PDOStatement class). These statements are templates and look like regular SQL queries, with the difference that, instead of values, they contain placeholders, which will be replaced with escaped values at execution time. The placeholders' mapping is done using the PDOStatement::bindParam() method, or by providing the mapping at execution time, as an argument of the PDOStatement::execute() method.
There are two types of placeholders:
Query example:
INSERT INTO users (email) VALUES (?);
Query example:
INSERT INTO users (email) VALUES (:email);
The use of prepared statements offers major benefits:
Note
By default, PDO will emulate prepared statements as support for databases that don't have this feature and, if you want to benefit from genuine prepared statements in MySQL Server, you should set PDO::ATTR_EMULATE_PREPARES to false in the connection options.
Emulating prepared statements means that the query will not be sent to the server and checked when PDO::prepare() is invoked. Instead, PDO will escape the bind parameters from PDO::execute(), and will make the placeholders' replacements on its own. Then, the raw SQL query is sent to the database server, meaning that, this way, you do not benefit from performance optimizations that the database could carry out when using prepared statements that are then executed many times.
To obtain a prepared statement, you must invoke the PDO::prepare() method, providing the statement as a first argument. The output is an instance of the PDOStatement class (the prepared statement), which is then used to bind parameters' values and execute the statement.
PDO::bindParam() is used to bind prepared statements' parameters, and has the following syntax:
PDOStatement::bindParam(mixed $parameter, mixed &$variable [, int $data_type = PDO::PARAM_STR [, int $length [, mixed $driver_options ]]])
Accepted input parameters:
The PDO::bindParam() method returns true if successful, otherwise false.
To execute the prepared statement, use the PDO::execute() method. The syntax is the following:
PDOStatement::execute([array $input_parameters])
The only accepted parameter is an optional $input_parameters array with values for the statement placeholders. All values of the array are treated as PDO::PARAM_STR.
This method returns true if successful, otherwise false.
The following is a sample query using a prepared statement with positional placeholders:
$stmt = $pdo->prepare(“INSERT INTO users (email) VALUES (?)”);
$stmt->bindParam(1, $email);
$email = '[email protected]';
$stmt->execute();
$email = '[email protected]';
$stmt->execute();
Or it can be written as follows:
$stmt = $pdo->prepare(“INSERT INTO users (email) VALUES (?)”);
$stmt->execute(['[email protected]']);
$stmt->execute(['[email protected]']);
The following is a sample query using a prepared statement with named placeholders:
stmt = $pdo->prepare(“INSERT INTO users (email) VALUES (:email)”);
$stmt->bindParam(':email', $email);
$email = '[email protected]';
$stmt->execute();
$email = '[email protected]';
$stmt->execute();
Or it could be written as follows:
$stmt = $pdo->prepare(“INSERT INTO users (email) VALUES (:email)”);
$stmt->bindParam(':email', $email);
$stmt->execute([':email' => '[email protected]']);
$stmt->execute([':email' => '[email protected]']);
Notice that the $email variable is assigned to the :email placeholder only once, while its data changes twice, each change being followed by the execution of the statement. Each statement will send the current value of the $email variable, at that point of execution, this being possible as a result of using the variable reference in the PDO::bindParam() method, rather than passing the variable by value.
In this exercise, you will create a script that inserts new user emails from user input, using prepared statements:
<?php
/** @var PDO $pdo */
$pdo = require 'connection.php';
$insertStmt = $pdo->prepare(“INSERT INTO users (email) VALUES (:email)”);
if ($insertStmt->execute([':email' => $argv[1] ?? null]) === false) {
list(, , $driverErrMsg) = $insertStmt->errorInfo();
echo “Error inserting into the users table: $driverErrMsg” . PHP_EOL;
return;
}
echo “Successfully inserted into users table” . PHP_EOL;
This is an expected error, because we have already added this email before, and the UNIQUE keyword ensures that no other entries will be added that have the same email address. For a table definition, please refer to Exercise 7.14, Creating the Table.
Let's check the records using Workbench:
It looks good. You have successfully run a prepared statement with PDO. You will notice that the ID of [email protected] is not 2, but 5. This is because the prepared statements that ran before, even the failed ones, increased the AUTO_INCREMENT value.
php insert-prepared.php '””); DROP TABLE users; /**'
The output is similar to this:
Let's check the results using Workbench:
They look good. We are protected against SQL injection, but ended up with corrupt data, since the input was not validated nor sanitized before the query ran. Please refer to the Sanitizing and Validating the User Input section of Chapter 6, Using HTTP.
So far, you have learned how to create a database and a table, and also how to insert data into tables, in a secure manner. Now, it's time to fetch and display some data using PHP.
To accomplish this, we use the SELECT statement, which has the following minimal syntax:
SELECT column1 [, column2 …] FROM table
The preceding query would return all the records from the table since no limitation is set. It is therefore recommended (if not mandatory in some cases) to use the LIMIT clause in one of its forms:
A part of the LIMIT clause, the SELECT statement is rich in clauses that can be used to filter, join, group, or sort data. You can check the SELECT statement syntax of the official documentation page at https://dev.mysql.com/doc/refman/5.7/en/select.html.
A very simple SELECT statement looks like this:
SELECT * FROM employees LIMIT 10;
This statement queries the first 10 records from the employees table.
Note
Using an asterisk, *, instead of column names in SELECT statements will make MySQL perform an additional lookup query to retrieve the column list of the queried table, and replace the * in the original query with this list of columns. This has a performance impact on SQL queries, which is not significant for low-traffic applications; yet it is considered good practice to specify the column list instead of *, irrespective of the project size or the estimated traffic load.
Now, let's examine, step by step, how we can get the data we want from a MySQL database, using various examples.
In this exercise, you will learn how you can query data from a MySQL database in the most simplistic way, getting a slice of records in the result set, filtering the data, and ordering the data by a specific column:
<?php
/** @var PDO $pdo */
$pdo = require 'connection.php';
$statement = “SELECT * FROM users”;
$result = $pdo->query($statement);
if ($result === false) {
list(, , $driverErrMsg) = $pdo->errorInfo();
echo “Error querying the users table: $driverErrMsg” . PHP_EOL;
return;
}
echo “All records” . PHP_EOL;
while ($record = $result->fetch()) {
echo implode(“ ”, $record) . PHP_EOL;
}
$result = $pdo->query(“SELECT * FROM users LIMIT 2”);
echo PHP_EOL . “Use LIMIT 2” . PHP_EOL;
while ($record = $result->fetch()) {
echo implode(“ ”, $record) . PHP_EOL;
}
$result = $pdo->query(“SELECT * FROM users WHERE id > 3”);
echo PHP_EOL . “Use WHERE id > 3” . PHP_EOL;
while ($record = $result->fetch()) {
echo implode(“ ”, $record) . PHP_EOL;
}
$result = $pdo->query(“SELECT * FROM users ORDER BY id DESC”);
echo PHP_EOL . “Use ORDER BY id DESC” . PHP_EOL;
while ($record = $result->fetch()) {
echo implode(“ ”, $record) . PHP_EOL;
}
Note
The final file can be referred at https://packt.live/31daUWP.
Congratulations! You successfully fetched data from the MySQL database in different ways: sorting, filtering, and slicing the entire data in the table.
By now, we have got a glimpse into the power of a database. This is just the beginning.
To update records in MySQL, the UPDATE statement is used. This is usually used together with the WHILE clause to filter the rows to which the update is applied.
Warning
Not using WHERE in an UPDATE statement will cause the update to apply to all records in the table.
The PDOStatement::rowCount() method returns the number of rows affected by the last INSERT, UPDATE, or DELETE statement executed by the corresponding PDOStatement object.
In this exercise, you will learn how to perform an update to a MySQL database users table, setting the email [email protected] for a record with incorrect data in the email column (ID 6 in our case):
<?php
/** @var PDO $pdo */
$pdo = require 'connection.php';
$updateId = $argv[1] ?? 0;
$updateEmail = $argv[2] ?? '';
$updateStmt = $pdo->prepare(“UPDATE users SET email = :email WHERE id = :id”);
if ($updateStmt->execute([':id' => $updateId, ':email' => $updateEmail]) === false) {
list(, , $driverErrMsg) = $updateStmt->errorInfo();
echo “Error running the query: $driverErrMsg” . PHP_EOL;
return;
}
echo sprintf(“The query ran successfully. %d row(s) were affected.”, $updateStmt->rowCount()) . PHP_EOL;
The email for the record with the id 6 was changed to the value provided. It looks great! Note that if you have another id for the record with incorrect data in the email field, then you should use that id in step 2 when running the command.
php update.php 16 [email protected];
Expect the following output:
We end up with no row being affected by this query, and the logic seems pretty straightforward: the UPDATE statement filters the rows to update, using the conditions from the WHERE clause; in our case, filtering by id=16 resulted in no rows qualifying for an update.
Note
Trying to update a record column value with the same, identical value will result in no count for the affected row aggregation; in other words, PDOStatement::rowCount() will return 0 (zero).
To delete records from MySQL, we should use the DELETE statement. This is often (if not always) used together with the WHERE clause to indicate matching records to delete.
Warning
Failure to provide the WHERE clause in a DELETE statement will cause all records to be deleted from the table.
Usually, in the WHERE clause of a DELETE statement, the id columns are used. This is the case when a precisely indicated row is deleted. But the WHERE clause can be used to its full potential in DELETE statements as well. Let's say we want to delete records using a partial match for string columns. To achieve this, we will use the LIKE operator, which is simple, yet powerful, pattern matching. With this operator, we can use two wildcards:
For example, LIKE php_ will match the php7 column value but will not match php or php70.
On the other hand, LIKE “php7%” will match php7, php70, but will not match php.
To know how many records were deleted, we will use the PDOStatement::rowCount() method mentioned before.
In this exercise, you will learn how to delete records from MySQL using a partial match in the WHERE clause:
<?php
/** @var PDO $pdo */
$pdo = require 'connection.php';
$partialMatch = $argv[1] ?? '';
$deleteStmt = $pdo->prepare(“DELETE FROM users WHERE email LIKE :partialMatch”);
if ($deleteStmt->execute([':partialMatch' => “%$partialMatch%”]) === false) {
list(, , $driverErrMsg) = $deleteStmt->errorInfo();
echo “Error deleting from the users table: $driverErrMsg” . PHP_EOL;
return;
}
if($rowCount = $deleteStmt->rowCount()){
echo sprintf(“Successfully deleted %d records matching '%s' from users table.”, $rowCount, $partialMatch) . PHP_EOL;
} else {
echo sprintf(“No records matching '%s' were found in users table.”, $partialMatch) . PHP_EOL;
}
Note
The full script can be referred at https://packt.live/2MCeswE.
All the records matching smith have gone.
You successfully completed the deletion of records from the database table by matching them using the LIKE operator. For a complete list of operators, refer at https://packt.live/2OHMB0B.
The singleton pattern is a software design pattern that limits the instantiation of a class to a single instance. The idea of this pattern is to make the class itself responsible for its instantiation, which can be achieved by hiding the constructor method (for example, changing its visibility to private) and by defining a public static method that returns the sole instance of the class.
This is useful when precisely one object (the first instance) is needed to perform actions across the application. For a database connection class, this is particularly useful since it does not only limit multiple instantiations of the class but also avoids repetitive connection and disconnection operations with the MySQL Server, making the first established connection available across the application for the lifetime of a single request-response cycle.
To test (or demonstrate) the singleton implementation in PHP, a simple script file would be sufficient:
DatabaseSingleton.php
1 <?php
2
3 class DatabaseSingleton
4 {
5 private function __construct()
6 {
7 //$this->pdo = new PDO(...);
8 }
9
10 public static function instance()
11 {
12 static $instance;
13 if (is_null($instance)) {
14 $instance = new static;
15 }
16 return $instance;
17 }
18 }
Running the preceding script would always return the following:
Note
When comparing objects using the identity operator (===), object variables are identical if, and only if, they refer to the same instance of the same class.
So far in this chapter, you have learned how to use a database, starting with the connection, creating a database and tables, before moving on to adding, querying, updating, and deleting records, and then to securing queries by using prepared statements and anonymous or named placeholders. Undoubtedly, MySQL has much more to offer—it merits an entire book, but the essentials were all briefly covered here.
You are required to build a website where users can create an account and then log in to manage a private list of contacts. The website will make use of databases to store user login data, as well as to store each user's contacts.
Along with the database functionality that you have learned in this chapter, you will be required to use functionality from previous chapters in order to build the website (for example, conditionals from Chapter 3, Control Statements; functions from Chapter 4, Functions; OOP from Chapter 5, Object-Oriented Programming; and form validation from Chapter 6, Using HTTP). You may need to refer to previous chapters for a reminder of how to implement the required functionality.
The required pages are as follows:
Layout and Briefing
The layout is as shown:
There is a horizontal navigation bar at the top of the page, featuring the website title on the left, and the Login button on the right. After a successful login, the Login button will be replaced by the username, which will link to the Profile page, the Contacts page link, and the Logout link.
The content is a message with two call-to-action links: Sign up and Login.
The Login page will look as follows:
The login is based on the username and password, so the content is a simple login form, with Username and Password fields, and a Login button. The last sentence is a Sign up call-to-action link.
After logging in, the user is redirected to the Profile page.
The Sign up page will look as follows:
The content is the Sign up form, with the following inputs:
The username is required to be at least three characters long, and just alphanumeric. The password should be at least six characters long and should be verified by a second password input at signup. Any form error should be displayed under the input where the data came from, for example:
The registered accounts should also retain the signup date. After signing up, the user is redirected to the Profile page:
The Profile page will look as follows:
This will contain a greeting, the profile data, and the session login time. While the username and signup date are stored in the database, the session login time can be stored in the current session.
The Contacts page will look as follows:
The content is split into two: the contacts list and the contact add/edit form:
The contacts list will list the contact records, each record having the Edit and Delete links. If the list is empty, then display the appropriate message instead of rendering the empty table.
The contact form will have the following field names:
It should look similar to the following:
Error messages for invalid data should be placed under the inputs from which the data emanated.
Accessing the Contacts page, the form is ready to use to create new contacts. Once the Edit button of a contact is pressed, then the contact info will be filled in the form; submitting the form updates the existing contact.
When an authenticated user accesses the Home page, Login page, or Sign up page, they will be redirected to the Profile page.
The default page title is Contacts list.
Now, where should you start? While, in most cases, frameworks are used to simplify the “getting started” process of each project, and since we will cover the frameworks in a later chapter, let's stick with our bootstrap example. Therefore, let's have the previous activity as a starting point for this one (please refer to the activity in Chapter 6, Using HTTP). Since the code of the current activity will change, you may want to create a copy of the code from the previous activity.
That having been said, I'll give you some guidelines here and there.
Steps to perform:
Let's see what is needed for the new requirements, compared to the previous activity:
Here are the steps to perform the activity:
Note
The solution for this activity can be found on page 534.
In this chapter, you learned how to handle files with PHP, which includes creating, writing, reading, and other filesystem-related operations. You also performed some basic, yet powerful, operations against a MySQL database server, creating a database structure and inserting, modifying, and deleting data. Although it might look a bit complex or overwhelming at the beginning, remember: it's like riding a bike – once practiced enough, until you get comfortable with it, you will never forget it (and it will actually get you from point A to point B way faster). In the next chapter, we will cover the concept of error handling, which is essential to identify potential problems in an application, and prevent important details leaking out to your users in the form of nasty error messages.
3.147.104.248