This section is included to assist the students to perform the activities present in the book. It includes detailed steps that are to be performed by the students to complete and achieve the objectives of the book.
Solution
<?php
$name = $_GET['movieName'];
$star = $_GET['movieStar'];
$year = $_GET['movieYear'];
?>
movies.php
8 <head>
9 <meta charset="UTF-8">
10 <meta name="viewport" content="width=device-width, initial-scale=1.0">
11 <meta http-equiv="X-UA-Compatible" content="ie=edge">
12 <title><?php echo $name; ?></title>
13 </head>
14 <body>
15 <div>
16 <h1>Information about <?php echo $name; ?></h1>
17 <p>
28 Based on the input, here is the information so far:
19 <br>
20 <?php echo $star . ' starred in the movie ' . $name .' which was released in year ' . $year; ?>
21 </p>
22 </div>
23 </body>
php -S localhost:8085
You should see a screen like the following:
http://localhost:8085/movies.php?movieName=Avengers&movieStar=IronMan&movieYear=2019
You can change the values to anything you like to see how they will be displayed in the browser.
You should see a screen like the following:
Note
Ensure that the port you have specified is not being used by any other application on your system.
Depending on the last few exercises, you should now be aware of how this code is working. Let's go through the query string and code.
The query string this time is movieName=Avengers&movieStar=IronMan&movieYear=2019. This means that the $_GET variable in PHP will have access to three different variables now, which are movieName, movieStar, and movieYear.
In the first three lines of code, we are extracting values for movieName, movieStar, and movieYear and assigning them to the $name, $star, and $year variables, respectively.
In the head section of HTML, we have a title. Inside it, we have used the echo statement to print the movie name, which will appear in the browser. Moving further down, we have an h1 element where we are printing the name again. After the h1 element is a p element, where we are creating a sentence dynamically. We have used the variables and the dot operator (.) to append different strings and variables to create a full sentence.
Solution
<?php
$name = 'Joe';
$weightKg = 80;
$heightCm = 180;
$heightMeters = $heightCm/100;
$heightSquared = $heightMeters * $heightMeters;
$bmi = $weightKg / ($heightSquared);
echo "<p>Hello $name, your BMI is $bmi</p>";
php -S localhost:8085
Now, in a browser, go to http://localhost:8085/tracker.php.
You will see the following output:
In this activity, we've looked at assigning data to variables and performing calculations (divisions and multiplications). Then, we printed the end result to the screen.
Solution
The steps to complete the activity are as follows:
<?php
$directors = [
"Steven Spielberg" => ["The Terminal", "Minority Report", "Catch Me If You Can", "Lincoln", "Bridge of Spies"],
"Christopher Nolan" => ["Dunkirk", "Interstellar", "The Dark Knight Rises", "Inception", "Memento"],
"Martin Scorsese" => ["Silence", "Hugo", "Shutter Island", "The Departed", "Gangs of New York"],
"Spike Lee" => ["Do the Right Thing", "Malcolm X", "Summer of Sam", "25th Hour", "Inside Man"],
"Lynne Ramsey" => ["Ratcatcher", "Swimmer", "Morvern Callar", "We Need To Talk About Kevin", "You Were Never Really Here"]
];
Here, we have an associative array, $directors, which contains five directors' names and each director is used as a key for the array. Also, each director's key has been assigned another associative array that contains five movie names.
foreach ($directors as $director => $movies) {
echo "$director's movies: " . PHP_EOL;
foreach ($movies as $movie) {
echo " > $movie " . PHP_EOL;
}
}
In the preceding example, we have a simple looping through a nested array. Since a foreach loop is a good choice to iterate through associative arrays, we have utilized foreach in both the inner and outer loop to print a formatted director's name along with the movies they directed on each new line.
php activity-movies.php
The preceding command outputs the following:
The nested foreach loops do their job and iterate through the nested array to print the available movie names against the directors' names.
php activity_movies.php 3 2
Here, the script name itself is an argument for a php command, hence, the first, second, and third arguments are activity-movies.php, 3, and 2 respectively. The second argument should control the number of directors to iterate and the third argument should control the number of movies to iterate.
Command-line arguments can be obtained using the $argv system variable, so we will be using $argv[1] and $argv[2] for the second and third arguments. Note that $argv[0] is the script name in this case.
<?php
$directorsLimit = $argv[1] ?? 5;
$moviesLimit = $argv[2] ?? 5;
$directorsCounter = 1;
foreach ($directors as $director => $movies) {
if ($directorsCounter > $directorsLimit) {
break;
}
echo "$director's movies: " . PHP_EOL;
$moviesCounter = 1;
foreach ($movies as $movie) {
if ($moviesCounter > $moviesLimit) {
break;
}
echo " > $movie " . PHP_EOL;
$moviesCounter++;
}
$directorsCounter++;
}
Here, we have added $directorsCounter before the outer loop and $moviesCounter before the inner loop. Both of them start counting from 1 and immediately inside the loops we have checked whether the directors or movies exceed the limits given in $directorsLimit and $moviesLimit respectively. If any of the counters become greater than their limit, we terminate the iteration using the break command.
At the beginning of each loop, we have used a condition expression in the if control to check that the counter doesn't exceed the limit, and at the very end of each loop, the corresponding counter gets incremented.
Note
The final file can be referred at: https://packt.live/35QfYnp.
php activity_movies.php 2 1
The preceding command should print one movie from each of the two directors, as follows:
php activity-movies.php 2
The output is as follows:
Congratulations! You have used control statements and looping techniques to create a dynamic script that works based on command-line arguments. Control structures are used to control the execution of a program, hence we can leverage such structures to make decisions about things such as which branch of code to execute, to perform repetitive executions, to control the flow of iterations, and so on.
Solution
<?php
declare(strict_types=1);
activity.php
13 function factorial(int $number): float
14 {
15 $factorial = $number;
16 while ($number > 2) {
17 $number--;
18 $factorial *= $number;
19 }
20 return $factorial;
21 }
Let me explain what the function does. First of all, it takes an integer argument; we can be sure that it will always be an integer because we added a type hint and declared that we are using strict types. There are several ways in which you may have implemented the function, so don't let my solution put you off.
My take on it is that the first number in the calculation will have to be the input number – we store it in $factorial, which is the variable we will use to hold the result. Then, it is multiplied by $number - 1. This goes on until $number === 2;. The while condition runs for the last time when $number has become 3; it will then be decremented by 1 and multiplied with the $factorial variable. By the end, $factorial contains the result and is returned from the function.
Instead of $number--; using the post decrement operator, --, we could have written $number = $number -1;. Some people consider the latter to be a better practice because it is more explicit. I sometimes prefer to use the handy shortcuts that PHP has to offer. Because $number-- is on its own line as a single statement, we could have also written --$number. In this case, there is no difference.
The difference between the two operators is that with --$number, $number will be decremented before the statement runs, and with $number--, it will be decremented after the statement has been evaluated. In this case, there is no consequence of that difference.
/**
* Return the sum of its inputs. Give as many inputs as you like.
*
* @return float
*/
function sum(): float
{
return array_sum(func_get_args());
}
While we could have just looped over func_get_args(); and added all the numbers together to get the sum, there is already a built-in function in PHP that does just that. So, why not use it? That is what array_sum does: it adds up all the numbers in the input array you give it. The return keyword makes the function return the result.
If you wanted to validate each parameter to check whether it was numeric (using is_numeric), then looping over the arguments would have been better because you would do the check in the same iteration as the addition and throw an exception when the argument wasn't numeric.
activity.php
41 function prime(int $number): bool
42 {
43 // everything equal or smaller than 2 is not a prime number
44 if (2 >= $number) {
45 return false;
46 }
47 for ($i = 2; $i <= sqrt($number); $i++) {
48 if ($number % $i === 0) {
49 return false;
50 }
51 }
52 return true;
53 }
The prime function is definitely the most challenging of them all. The naive implementation would just try to determine the modulo of the $number input by all values that are smaller: when the modulo is 0, then it is not a prime number. However, it has already been proven that you only have to check all the numbers up to the square root of the input. In fact, you could check even fewer numbers, but we have not gone as far as that.
Now we know 1 is not a prime number so, if the number that is passed through is 1 then we return false early. This also rules out 0 and negative numbers. Prime numbers are positive by definition. Then, starting with 2, up until the square root of the $number input, we increment $i by 1 and check whether the modulo of the division of $number by $i is 0. If it is, $number is not a prime number and we again return false early. The modulo operator is written as % (the percentage symbol). In other words, when the $number modulo $i equals 0, $number is divisible by $i, and since $i is not equal to 1 and not equal to $number, $number is not a prime number.
activity.php
59 function performOperation(string $operation)
60 {
61 switch ($operation) {
62 case 'factorial':
63 // get the second parameter, it must be an int.
64 // we will cast it to int to be sure
65 $number = (int) func_get_arg(1);
66 return factorial($number);
67 case 'sum':
68 // get all parameters
69 $params = func_get_args();
70 // remove the first parameter, because it is the operation
71 array_shift($params);
72 return call_user_func_array('sum', $params);
73 case 'prime':
74 $number = (int) func_get_arg(1);
75 return prime($number);
76 }
77 }
This function just switches between the three other functions based on the $operation case you give it as its first argument. Since one of the functions it delegates its work to accepts a varying amount of arguments, performOperation also has to accept a varying number of arguments.
You could also choose an implementation where you let performOperation have a second parameter, $number, which can then be passed exactly as it is to both factorial and prime. In that case, you only query func_get_args in the case of the sum operation. The approach you choose is not only a matter of taste, but also of performance. It is faster not to use func_get_args(), so the alternative approach would definitely be the fastest.
echo performOperation("factorial", 3) . PHP_EOL;
echo performOperation('sum', 2, 2, 2) . PHP_EOL;
echo (performOperation('prime', 3)) ? "The number you entered was prime." . PHP_EOL : "The number you entered was not prime." . PHP_EOL;
Here is the output:
Solution
The steps to complete the activity are as follows:
<?php
namespace Student;
class Student
{
public $name;
public $title = 'student';
function __construct(string $name)
{
$this->name = $name;
}
}
<?php
namespace Professor;
class Professor
{
public $name;
public $title = 'Prof.';
private $students = array();
function __construct(string $name, array $students)
{
$this->name = $name;
}
}
<?php
namespace Professor;
use StudentStudent;
Here, after the Professor namespace declaration, we have imported the Student class via its Student namespace.
Add the following filtration in the Professor constructor for $students:
function __construct(string $name, array $students)
{
$this->name = $name;
foreach ($students as $student) {
if ($student instanceof Student) {
$this->students[] = $student;
}
}
}
Here, we have iterated through $students using a foreach loop and, inside, checked whether $student is an instance of the Student class, then added it to the $this->students array. So, only valid students can be added to the professor's student list.
public function setTitle(string $title)
{
$this->title = $title;
}
This one should be used to set the professor's title. If a professor is a Ph.D., then we set the title as Dr..
public function printStudents()
{
echo "$this->title $this->name's students (" .count($this- >students). "): " . PHP_EOL;
$serial = 1;
foreach ($this->students as $student) {
echo " $serial. $student->name " . PHP_EOL;
$serial++;
}
}
Here, we have printed the professor's title, name, and the number of students. Again, we have used a foreach loop to iterate through the professor's private property, $students, and inside the loop we have printed each student's name. Also, for the sake of maintaining a serial order of the students, we have used the $serial variable starting from 1, which increments by one after each iteration in order to add a number before each student's name while printing.
<?php
spl_autoload_register();
Here, we haven't registered any class loader methods in the spl_autoload_register() function; rather, we have kept it as the default to load the classes according to their namespaces.
$professor = new ProfessorProfessor('Charles Kingsfield', array(
new StudentStudent('Elwin Ransom'),
new StudentStudent('Maurice Phipps'),
new StudentStudent('James Dunworthy'),
new StudentStudent('Alecto Carrow')
));
Here, we have added a random amount of Student instances in an array and passed them to the Professor constructor. When we instantiate the Professor class as new ProfessorProfessor(), this namespaced class name tells the auto loader to load the Professor class from the Professor directory. This same namespaced class' loading technique is applied to the Student class as well. The new StudentStudent() namespace tells the autoloader to expect the Student class in the Student directory.
$professor->setTitle('Dr.');
$professor->printStudents();
Finally, the activity-classes.php looks like:
<?php
spl_autoload_register();
$professor = new ProfessorProfessor('Charles Kingsfield', array(
new StudentStudent('Elwin Ransom'),
new StudentStudent('Maurice Phipps'),
new StudentStudent('James Dunworthy'),
new StudentStudent('Alecto Carrow')
));
$professor->setTitle('Dr.');
$professor->printStudents();
php activity-classes.php
The output should look like the following:
We have successfully obtained a list of a professor's students using OOP techniques. In this activity, we have practiced class attributes, access modifiers, methods, class declaration, class namespacing, object instantiation, autoloading namespaced classes, type hints in parameters, and object filtration using instanceof, and so on.
Solution
Login.php
37 private function getUserData(string $username): ?array
38 {
39 $users = [
40 'vip' => [
41 'level' => 'VIP',
42 'password' => '$2y$10$JmCj4KVnBizmy6WS3I/bXuYM/yEI3dRg/IYkGdqHrBlOu4FKOliMa' // "vip" password hash
43 ],
$username = 'admin';
$passwordHash = '$2y$10$Y09UvSz2tQCw/454Mcuzzuo8ARAjzAGGf8OPGeBloO7j47Fb2v. lu'; // "admin" password hash
$formError = [];
$userData = $this->getUserData($formUsername);
if (!$userData) {
$formError = ['username' => sprintf('The username [%s] was not found.', $formUsername)];
} elseif (!password_verify($formPassword, $userData['password'])) {
$formError = ['password' => 'The provided password is invalid.'];
} else {
$_SESSION['username'] = $formUsername;
$_SESSION['userdata'] = $userData;
$this->requestRedirect('/profile');
return '';
}
Note
For convenience, generate password hash with command line using php -r "echo password_hash('admin', PASSWORD_BCRYPT);" command
<div class="text-center mb-4">
<h1 class="h3 mb-3 mt-5 font-weight-normal">Authenticate</h1>
</div>
The src/templates/profile.php file will have to be rebuilt from scratch. First, let's add the greetings and logout button part. While browsing Bootstrap's framework documentation, we came across alerts component, and we saw we could use this component for our current purpose:
<div class="row">
<div class="my-5 alert alert-secondary w-100">
<h3>Welcome, <?= $username ?>!</h3>
<p class="mb-0"><a href="/logout">Logout</a></p>
</div>
</div>
<div class="row">
<div class="col-sm-6">...</div>
<div class="col-sm-6">...</div>
</div>
Note
To learn more about grid system in Bootstrap, please follow this link: https://packt.live/31zF72E.
profile.php
15 <div class="form-label-group mb-3">
16 <label for="name">Name:</label>
17 <input type="text" name="name" id="name"
18 class="form-control <?= isset($formErrors['name']) ? 'is-invalid' : ''; ?>"
19 value="<?= htmlentities($_POST['name'] ?? ''); ?>">
20 <?php if (isset($formErrors['name'])) {
21 echo sprintf('<div class="invalid-feedback">%s</div>', htmlentities($formErrors['name']));
22 } ?>
23 </div>
<?php if (isset($formErrors['form'])) { ?>
<div class="alert alert-danger"><?= $formErrors['form']; ?></div>
<?php } ?>
<input type="hidden" name="csrf-token" value="<?= $formCsrfToken ?>">
<button type="submit" name="do" value="get-support" class="btn btn-lg btn-primary">Send</button>
<?php foreach ($sentForms as $item) { ?>
<div class="card mb-2">
<div class="card-body">
<h5 class="card-text"><?= htmlentities($item['form'] ['message']) ?></h5>
<h6 class="card-subtitle mb-2 text-muted">
<strong>Added:</strong> <?= htmlentities($item['timeAdded']) ?></h6>
<h6 class="card-subtitle mb-2 text-muted">
<strong>Reply-to:</strong> <?= sprintf('%s <%s>', htmlentities($item['form']['name']), htmlentities($item['form']['email'])) ?>
</h6>
</div>
</div>
<?php } ?>
Note
The complete code in profile.php can be referred at: https://packt.live/2pvh0or.
$formErrors = $this->processContactForm($_POST);
Note
It is a good practice to reload the page (perform a redirect to the same page, which will result in a GET HTTP request) after a successful operation due to a POST request, in order to avoid subsequent submissions when the page is reloaded in the browser by the user.
The code is as follows:
if (!count($formErrors)) {
$this->requestRefresh();
return '';
}
return (new ComponentsTemplate('profile'))->render([
'username' => $_SESSION['username'],
'formErrors' => $formErrors ?? null,
'sentForms' => $_SESSION['sentForms'] ?? [],
'formCsrfToken' => $this->getCsrfToken(),
]);
private function getCsrfToken(): string
{
if (!isset($_SESSION['csrf-token'])) {
$_SESSION['csrf-token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf-token'];
}
list($form, $errors) = $this->validateForm($data);
$_SESSION['sentForms'][] = [
'dateAdded' => date('Y-m-d'),
'timeAdded' => date(DATE_COOKIE),
'form' => $form,
];
if (!isset($data['csrf-token']) || $data['csrf-token'] !== $this->getCsrfToken()) {
$errors['form'] = 'Invalid token, please refresh the page and try again.';
}
if (($_SESSION['userdata']['level'] === 'STANDARD')
&& $this->hasSentFormToday($_SESSION['sentForms'] ?? [])
) {
$errors['form'] = 'You are only allowed to send one form per day.';
}
$name = trim($data['name'] ?? '');
if (empty($name)) {
$errors['name'] = 'The name cannot be empty.';
}
if (empty($data['email'] ?? '')) {
$errors['email'] = 'The email cannot be empty.';
} elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
$errors['email'] = 'The email address is invalid.';
}
$message = trim($data['message'] ?? '');
if (!$message) {
$errors['message'] = 'The message cannot be empty.';
}
if (strlen($message) <= 40) {
$errors['message'] = 'The message is too short.';
}
$form = [
'name' => $name,
'email' => $data['email'],
'message' => $message,
];
return [$form, $errors];
private function hasSentFormToday(array $sentForms): bool
{
$today = date('Y-m-d');
foreach ($sentForms as $sentForm) {
if ($sentForm['dateAdded'] === $today) {
return true;
}
}
return false;
}
private function requestRefresh()
{
$this->requestRedirect($_SERVER['REQUEST_URI']);
}
Note
The final code in the handler Profile.php can be referred at: https://packt.live/2VREaRY.
We are redirected to the Profile page and we can see the HTML elements we have worked on so far.
You could try this data:
Name: Luigi
Email: [email protected]
Message: I would like to be able to upload a profile picture. Do you consider adding this feature?
Name: Mario
Email: [email protected]
Message: I would like to be able to upload a profile picture. Do you consider adding this feature?
It looks fine, as expected.
Name: Mario
Email: [email protected]
Message: Can I filter my order history by the payment method used to make the purchase?
As you can see, we succeeded in adding another entry, as expected.
Solution
Let's discuss the new or changed items, from the most uncoupled ones to the most complex ones.
A good start here is the User model class since this class will be invoked on every page for authenticated users; let's put this file inside the src/models/ directory:
<?php
declare(strict_types=1);
namespace Models;
use DateTime;
class User
{
/** @var int */
private $id;
/** @var string */
private $username;
/** @var string */
private $password;
/** @var DateTime */
private $signupTime;
User.php
21 public function __construct(array $input)
22 {
23 $this->id = (int)($input['id'] ?? 0);
24 $this->username = (string)($input['username'] ?? '');
25 $this->password = (string)($input['password'] ?? '');
26 $this->signupTime = new DateTime($input['signup_time'] ?? 'now', new DateTimeZone('UTC'));
27 }
28
29 public function getId(): int
30 {
31 return $this->id;
32 }
public function passwordMatches(string $formPassword): bool
{
return password_verify($formPassword, $this->password);
}
}
This class aims to be a representation of a database record from the users table. The constructor function will ensure that each field will get data of its own type. The following methods are simple getters, and the last method, Users::passwordMatches(), is a convenient way to validate the input passwords at login.
Since the User entity is strongly related to the authentication mechanism, let's see what the Auth component would look like.
<?php declare(strict_types=1);
namespace Components;
use DateTime;
use ModelsUser;
class Auth
{
public static function userIsAuthenticated(): bool
{
return isset($_SESSION['userid']);
}
public static function getLastLogin(): DateTime
{
return DateTime::createFromFormat('U', (string)($_SESSION['loginTime'] ?? ''));
}
public static function getUser(): ?User
{
if (self::userIsAuthenticated()) {
return Database::getUserById((int)$_SESSION['userid']);
}
return null;
}
public static function authenticate(int $id)
{
$_SESSION['userid'] = $id;
$_SESSION['loginTime'] = time();
}
public static function logout()
{
if (session_status() === PHP_SESSION_ACTIVE) {
session_regenerate_id(true);
session_destroy();
}
}
}
<?php declare(strict_types=1);
namespace Components;
use ModelsUser;
use PDO;
use PDOStatement;
class Database
{
public $pdo;
private function __construct()
{
$dsn = "mysql:host=mysql-host;port=3306;dbname=app;charset=utf 8mb4";
$options = [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
$this->pdo = new PDO($dsn, "php-user", "php-pass", $options);
}
public static function instance()
{
static $instance;
if (is_null($instance)) {
$instance = new static();
}
return $instance;
}
public function addUser(string $username, string $password): PDOStatement
{
$stmt = $this->pdo->prepare("INSERT INTO users ('username', 'password') values (:user, :pass)");
$stmt->execute([
':user' => $username,
':pass' => password_hash($password, PASSWORD_BCRYPT),
]);
return $stmt;
}
Note
It is advised to return the PDOStatement instance in this case, instead of Boolean true/false values, which indicate whether the operation succeeded, because the former can give more info in the event of a failed operation (for example, PDOStatement::errorInfo()).
Database.php
41 public function getUserByUsername(string $formUsername): ?User
42 {
43 $stmt = $this->pdo->prepare("SELECT * FROM users WHERE username = :username");
44 if ($stmt->execute([':username' => $formUsername]) && ($data = $stmt->fetch(PDO::FETCH_ASSOC))) {
45 return new User($data);
46 }
47 return null;
48 }
Notice the if (stmt->execute() && ($data = $stmt->fetch(PDO::FETCH_ASSOC))) { /* ... */ } expression. This is a combined expression that executes the evaluation-assignment-evaluation type of operations, and is identical to the following:
if (stmt->execute()) { // evaluation
$data = $stmt->fetch(PDO::FETCH_ASSOC); // assignment
if ($data) { // evaluation
/* ... */
}
}
While the latter block might look more readable, especially for beginner developers, the former expression might look cleaner, especially for seasoned developers. Both approaches are valid and, in the end, it's a matter of subjective preference.
public function getOwnContacts(int $uid): PDOStatement
{
$stmt = $this->pdo->prepare("SELECT * FROM contacts WHERE user_id = :uid");
$stmt->bindParam(':uid', $uid, PDO::PARAM_INT);
$stmt->execute();
return $stmt;
}
public function getOwnContactById(int $ownerId, int $contactId): ?array
{
$stmt = $this->pdo->prepare("SELECT * FROM contacts WHERE id = :cid and user_id = :uid");
$stmt->bindParam(':cid', $contactId, PDO::PARAM_INT);
$stmt->bindParam(':uid', $ownerId, PDO::PARAM_INT);
if ($stmt->execute() && ($data = $stmt->fetch(PDO::FETCH_ASSOC)))
{
return $data;
}
return null;
}
Database.php
79 public function addContact(
80 int $ownerId,
81 string $name,
82 string $email,
83 string $phone,
84 string $address
85 ): PDOStatement
86 {
87 $stmt = $this->pdo->prepare("INSERT INTO contacts (user_id, 'name', phone, email, address) " .
88 "VALUES (:uid, :name, :phone, :email, :address)");
Database.php
98 public function updateContact(
99 int $contactId,
100 int $ownerId,
111 string $name,
112 string $email,
113 string $phone,
114 string $address
115 ): PDOStatement
public function deleteOwnContactById(int $ownerId, int $contactId): PDOStatement
{
$stmt = $this->pdo->prepare("DELETE FROM contacts WHERE id = :cid and user_id = :uid");
$stmt->bindParam(':cid', $contactId, PDO::PARAM_INT);
$stmt->bindParam(':uid', $ownerId, PDO::PARAM_INT);
$stmt->execute();
return $stmt;
}
Router.php
1 <?php declare(strict_types=1);
2
3 namespace Components;
4
5 use HandlersContacts;
6 use HandlersSignup;
7 use HandlersLogin;
8 use HandlersLogout;
9 use HandlersProfile;
10 use HandlersSignup;
Router.php
21 case '/profile':
22 return new Profile();
23 case '/login':
24 return new Login();
25 case '/logout':
26 return new Logout();
27 case '/':
28 return new class extends Handler
29 {
30 public function __invoke(): string
31 {
32 if (Auth::userIsAuthenticated()) {
33 $this->requestRedirect('/profile');
34 }
<?php declare(strict_types=1);
namespace Handlers;
use ComponentsAuth;
use ComponentsDatabase;
use ComponentsTemplate;
class Contacts extends Handler
{
public function handle(): string
{
if (!Auth::userIsAuthenticated()) {
return (new Login)->handle();
}
$user = Auth::getUser();
$formError = [];
$formData = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$formError = $this->processForm();
if (!$formError) {
$this->requestRedirect('/contacts');
return '';
}
$formData = $_POST;
}
if (!empty($_GET['edit'])) {
$formData = Database::instance()->getOwnContactById ($user->getId(), (int)$_GET['edit']);
}
if (!empty($_GET['delete'])) {
Database::instance()->deleteOwnContactById($user->getId(), (int)$_GET['delete']);
$this->requestRedirect('/contacts');
return '';
}
return (new Template('contacts'))->render([
'user' => $user,
'contacts' => Database::instance()->getOwnContacts ($user->getId()),
'formError' => $formError,
'formData' => $formData,
]);
Contacts.php
46 private function processForm(): array
47 {
48 $formErrors = [];
49 if (empty($_POST['name'])) {
50 $formErrors['name'] = 'The name is mandatory.';
51 } elseif (strlen($_POST['name']) < 2) {
52 $formErrors['name'] = 'At least two characters are required for name.';
53 }
54 if (!filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL)) {
55 $formErrors['email'] = 'The email is invalid.';
56 }
if (!$formErrors) {
if (!empty($_POST['id']) && ($contactId = (int)$_POST['id'])) {
Database::instance()->updateContact($contactId, Auth::getUser()->getId(), $_POST['name'], $_POST['email'], $_POST['phone'] ?? '', $_POST['address'] ?? '');
} else {
Database::instance()->addContact(Auth::getUser()->getId(), $_POST['name'], $_POST['email'], $_POST['phone'] ?? '', $_POST['address'] ?? '');
}
}
return $formErrors;
}
Signup.php
1 <?php
2 declare(strict_types=1);
3
4 namespace Handlers;
5
6 use ComponentsAuth;
7 use ComponentsDatabase;
8 use ComponentsTemplate;
Signup.php
32 private function handleSignup(): ?array
33 {
34 $formError = null;
35 $formUsername = trim($_POST['username'] ?? '');
36 $formPassword = trim($_POST['password'] ?? '');
37 $formPasswordVerify = $_POST['passwordVerify'] ?? '';
38 if (!$formUsername || strlen($formUsername) < 3) {
39 $formError = ['username' => 'Please enter an username of at least 3 characters.'];
40 } elseif (!ctype_alnum($formUsername)) {
41 $formError = ['username' => 'The username should contain only numbers and letters.'];
42 } elseif (!$formPassword) {
43 $formError = ['password' => 'Please enter a password of at least 6 characters.'];
44 } elseif ($formPassword !== $formPasswordVerify) {
45 $formError = ['passwordVerify' => 'The passwords doesn't match.'];
46 } else {
47 $stmt = Database::instance() ->addUser(strtolower($formUsername), $formPassword);
<?php
declare(strict_types=1);
namespace Handlers;
use ComponentsAuth;
use ComponentsTemplate;
class Profile extends Handler
{
public function handle(): string
{
if (!Auth::userIsAuthenticated()) {
return (new Login)->handle();
}
return (new Template('profile'))->render();
}
}
<?php
declare(strict_types=1);
namespace Handlers;
use ComponentsAuth;
class Logout extends Handler
{
public function handle(): string
{
Auth::logout();
$this->requestRedirect('/');
return '';
}
}
Login.php
1 <?php
2 declare(strict_types=1);
3
4 namespace Handlers;
5
6 use ComponentsAuth;
7 use ComponentsDatabase;
8 use ComponentsTemplate;
9
10 class Login extends Handler
11 {
12 public function handle(): string
13 {
14 if (Auth::userIsAuthenticated()) {
15 $this->requestRedirect('/profile');
16 return '';
17 }
index.php
1 <?php
2 declare(strict_types=1);
3
4 use ComponentsRouter;
5 use ComponentsTemplate;
6
7 const WWW_PATH = __DIR__;
8
9 require_once __DIR__ . '/../src/components/Auth.php';
10 require_once __DIR__ . '/../src/components/Database.php';
11 require_once __DIR__ . '/../src/components/Template.php';
12 require_once __DIR__ . '/../src/components/Router.php';
13 require_once __DIR__ . '/../src/handlers/Handler.php';
14 require_once __DIR__ . '/../src/handlers/Login.php';
15 require_once __DIR__ . '/../src/handlers/Logout.php';
Now to the templates – let's see what has changed.
main.php
1 <?php use ComponentsAuth; ?>
2 <!doctype html>
3 <html lang="en">
4 <head>
5 <meta charset="utf-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
8 <title><?= ($title ?? '(no title)') ?></title>
<div class="jumbotron">
<h1 class="display-4">Hello!</h1>
<p class="lead"><a href="/signup">Sign up</a> to start creating your contacts list.</p>
<p class="lead">Already have an account? <a href="/login">Login here</a>.</p>
</div>
login-form.php
1 <?php
2 /** @var array $formError */
3 /** @var string $formUsername */
4 ?>
5 <div class="d-flex justify-content-center">
6 <form action="/login" method="post" style="width: 100%; max-width: 420px;">
7 <div class="text-center mb-4">
8 <h1 class="h3 mb-3 mt-5 font-weight-normal">Authenticate</h1>
9 </div>
signup-form.php
1 <?php
2 /** @var array $formError */
3 /** @var string $formUsername */
4 ?>
5 <div class="d-flex justify-content-center">
6 <form action="/signup" method="post" style="width: 100%; max-width: 420px;">
7 <div class="text-center mb-4">
8 <h1 class="h3 mb-3 mt-5 font-weight-normal">Sign up</h1>
9 </div>
profile.php
1 <?php
2
3 use ComponentsAuth;
4
5 $user = Auth::getUser();
6 ?>
7
8 <section class="my-5">
9 <h3>Welcome, <?= $user->getUsername() ?>!</h3>
10 </section>
contacts.php
1 <?php
2 /** @var PDOStatement $contacts */
3 /** @var array $formError */
4 /** @var array $formData */
5 ?>
6 <section class="my-5">
7 <h3>Contacts</h3>
8 </section>
33 <div class="col-12 col-lg-4">
34 <h4 class="mb-3">Add contact:</h4>
35 <form method="post">
36 <div class="form-row">
37 <div class="form-group col-6">
38 <label for="contactName">Name</label>
39 <input type="text" class="form-control <?= isset($formError['name']) ? 'is-invalid' : ''; ?>"
40 id="contactName" placeholder="Enter name" name="name"
41 value="<?= htmlentities($formData['name'] ?? '') ?>">
Thus, we have created a contact management system based on the concepts covered so far in the chapter.
Solution
$exceptionHandler = function (Throwable $e) {
static $fh;
if (is_null($fh)) {
$fh = fopen(__DIR__ . '/app.log', 'a');
if (!$fh) {
echo 'Unable to access the log file.', PHP_EOL;
exit(1);
}
}
$message = sprintf('%s [%d]: %s', get_class($e), $e->getCode(), $e->getMessage());
$msgLength = mb_strlen($message);
$line = str_repeat('-', $msgLength);
$logMessage = sprintf(
"%s %s > File: %s > Line: %d > Trace: %s %s ",
$line,
$message,
$e->getFile(),
$e->getLine(),
$e->getTraceAsString(),
$line
);
fwrite($fh, $logMessage);
};
$errorHandler = function (int $code, string $message, string $file, int $line) use ($exceptionHandler) {
$exception = new ErrorException($message, $code, $code, $file, $line);
$exceptionHandler($exception);
if (in_array($code, [E_ERROR, E_RECOVERABLE_ERROR, E_USER_ERROR])) {
exit(1);
}
};
set_error_handler($errorHandler);
set_exception_handler($exceptionHandler);
class NotANumber extends Exception {}
class DecimalNumber extends Exception {}
class NumberIsZeroOrNegative extends Exception {}
function printError(string $message): void
{
echo '(!) ', $message, PHP_EOL;
}
function calculateFactorial($number): int
{
if (!is_numeric($number)) {
throw new NotANumber(sprintf('%s is not a number.', $number));
}
$number = $number * 1;
if (is_float($number)) {
throw new DecimalNumber(sprintf('%s is decimal; integer is expected.', $number));
}
if ($number < 1) {
throw new NumberIsZeroOrNegative(sprintf('Given %d while higher than zero is expected.', $number));
}
We use is_numeric() to check whether the input is an integer or a numeric string and throw a NotANumber exception if the validation fails. Then, we validate whether the input is a decimal number since we only want to allow integers. To achieve this, we have to "convert" the potential string numeral to one of integers or float types, and therefore we multiply the number with the numeric 1 so that PHP will convert the input automatically for us. Another way of checking whether we are dealing with decimals is to look for decimal separators in the input, using the built-in strpos() function. In the case of a decimal value, we throw a DecimalNumber exception. Then, if the input number is lower than 1, we throw a NumberIsZeroOrNegative exception. At this step, validation ends, and we can proceed with the computation.
$factorial = 1;
for ($i = 2; $i <= $number; $i++) {
$factorial *= $i;
}
return $factorial;
}
A for loop is used to multiplicate the $factorial variable through its iterations until $i reaches the $number input value provided.
Note
We use the $factorial *= $i; notation, which is equivalent to the more verbose one—$factorial = $factorial * $i;
$arguments = array_slice($argv, 1);
if (!count($arguments)) {
printError('At least one number is required.');
} else {
foreach ($arguments as $argument) {
try {
$factorial = calculateFactorial($argument);
echo $argument, '! = ', $factorial, PHP_EOL;
The calculateFactorial() function is wrapped in a try block since we are expecting an exception to be thrown, which we want to catch eventually. Remember that we have to display an output value for each input argument, so, in the event of errors for one argument, we want to be able to continue to advance the script to the next argument.
} catch (NotANumber | DecimalNumber | NumberIsZeroOrNegative $e) {
printError(sprintf('[%s]: %s', get_class($e), $e->getMessage()));
} catch (Throwable $e) {
printError("Unexpected error occured for [$argument] input number.");
$exceptionHandler($e);
}
}
}
php factorial.php;
The output is as follows:
Since no arguments were passed to the script, the appropriate error message is printed on the screen.
In this case, a list of arguments was provided, starting with 1 and ending in four. As expected, for each argument, a new line is printed, containing either the response or the error. An interesting line here is the one for the argument 21, for which we got an Unexpected error message, without giving many details. We should look in the log file to see some relevant data:
The complaint here concerns a float type being returned by the calculateFactorial() function, while int is expected. That's because the resulting factorial number for 21 (51090942171709440000) is higher than the maximum integer the PHP engine can handle (php -r 'echo PHP_INT_MAX;' would output 9223372036854775807), and so is converted to a float type and is presented in scientific notation (5.1090942171709E+19). Since the calculateFactorial() function has declared int as a return type, the returned float type value has caused a TypeError, and now we may decide to apply an extra condition to input arguments, limiting the maximum number to 20, throwing a custom exception when the number is higher, or to check the type of factorial in calculateFactorial() before the value is returned, and throw a custom exception as well.
In this activity, you managed to improve the user experience by printing pretty messages to user output, even for unexpected errors. Also, in the case of unexpected errors, the messages were logged to a log file so that the developer could check on them and, based on that data, reproduce the issue, and then come up with a fix or an improved solution for the script.
Solution
composer require ramsey/uuid
The output is as follows:
ls -lart vendor
The output is as follows:
Example.php
1 <?php
2
3 namespace Packt;
4
5 use MonologLogger;
6 use RamseyUuidUuid;
7
8 class Example
9 {
10 protected $logger;
11 public function __construct(Logger $logger)
12 {
13 $this->logger = $logger;
14 }
<?php
require 'vendor/autoload.php';
use MonologLogger;
use MonologHandlerStreamHandler;
use PacktExample;
$logger = new Logger('application_log');
$logger->pushHandler(new StreamHandler('.logs/app.log', Logger::INFO));
$e = new Example($logger);
$e->doSomething();
$e->printUuid();
Solution
<?php
require 'vendor/autoload.php';
use GuzzleHttpClient;
$client = new Client(['base_uri'=>'http://httpbin.org/']);
try
{
$response=$client->request('POST', '/response-headers',[
'headers'=>[
'Accept'=>'application-json'
]
'query'=> [
'first'=>'John',
'last'=>'Doe'
]
]);
if ($response->getStatusCode()!==200){
throw new Exception("Status code was {$response->getStatusCode()}, not 200");
}
$responseObject=json_decode($response->getBody()->getContents());
echo "The web service responded with {$responseObject->first} {$responseObject->last}".PHP_EOL;
}
catch(Exception $ex)
{
echo "An error occurred: ".$ex->getMessage().PHP_EOL;
}