Categories
HTML MVC PHP

Picture Uploads in PHP MVC Applications

Basic picture file uploads are handled on the client-side by a form and on the server-side by a script moving a cached temporary file to a permanent location while recording the appropriate path information to database. In this post, we will elaborate on each of these portions.

The HTML Form

For forms to upload files correctly, 2 requirements must be met: a named file input must be included in the form and the encoding type must be appropriately. We show below an example proper form:

  1. <form action='' method='post' enctype='multipart/form-data'>
  2.   <div class='form-group'>
  3.     <label>Picture:
  4.       <input type='file' name='newPicture' class='form-control' />
  5.     </label>
  6.   </div>
  7.   <div class='form-group'>
  8.     <label>Description:
  9.       <textarea name='description' class='form-control'></textarea>
  10.     </label>
  11.   </div>
  12.   <input type='submit' name='action' value='Add picture' class='btn btn-primary' />
  13.   <a href='/product/index' class='btn btn-secondary'>Cancel</a>
  14. </form>

First, we see on line 1 that the enctype attribute for the form is set to multipart/form-data. This encodes the submitted form data with different parts for different data, much like an email with attachments included. If the form enctype attribute to this value, the file is simply not received by the server application.

Second, we see on line 4 an input with the type attribute set to the value file. This input also has the attribute name set to newPicture so that we may refer to it by this name in the server script.

The submit URL for this form will be the same as its calling URL.

The PHP Script

Now, a server-side script must receive the form data, check the data and save the file as appropriate:

  1. public function addPicture($product_id){
  2.   if(isset($_FILES['newPicture']) 
  3.     && $_FILES['newPicture']['error'] == UPLOAD_ERR_OK){
  4.     $info = getimagesize($_FILES['newPicture']['tmp_name']);
  5.     $allowedTypes = [IMAGETYPE_JPEG=>'.jpg',
  6.                      IMAGETYPE_PNG=>'.png',
  7.                      IMAGETYPE_GIF=>'.gif'];//accept jpg, png, gif
  8.     if($info === false){ // no go
  9.       $this->view('product/addPicture', ['error'=>'Bad file format']);
  10.     }else if(!array_key_exists($info[2], $allowedTypes)){ // no go
  11.       $this->view('product/addPicture', 
  12.         ['error'=>'Not an accepted file type']);
  13.     }else{
  14.       //save the picture in the images folder
  15.       $path = getcwd().DIRECTORY_SEPARATOR.'images'.DIRECTORY_SEPARATOR;
  16.       $filename = uniqid().$allowedTypes[$info[2]];
  17.       move_uploaded_file($_FILES['newPicture']['tmp_name'], $path.$filename);
  18.  
  19.       $newPicture = $this->model('Picture');
  20.       $newPicture->product_id = $product_id;
  21.       $newPicture->filename = $filename;
  22.       $newPicture->description = $_POST['description'];
  23.       $newPicture->create();
  24.       header('location:/product/index');
  25.     }
  26.   }else{
  27.     $this->view('product/addPicture');
  28.   }
  29. }

This method is meant to accept files associated to products in a product catalog. Therefore, on line 1, we need to pass in the product_id foreign key value for the picture record to refer to the appropriate product record.

On lines 2 and 3, we verify that the picture is indeed sent (line 2) to the receiving script without any error (line 3). If not, the script branches to line 27 and presents the submission form.

On lines 4 and 8, we extract image file information (line 4) that will allow us to validate (on line 8) that the received file is indeed an image. If not, an error message is sent to the user on line 9.

At line 10, the image information (from line 4) is used again against an image type whitelist (produced on lines 5-7). If the file is not of the proper type, an error message is sent to the user, on lines 11-12.

Finally, if all checks are good, the script continues on to lines 15-24. Lines 15, 16, 17 handle saving the file to the filesystem as follows:

  1. Define the saving path in the OS, based on the current working directory. The file will be saved in the images folder under the server document root.
  2. Define the file name to a random string with the extension from the whitelist defined on lines 5-7.
  3. Move (save) the file from its temporary location to the images folder.

Lines 19-24 record the data as follows:

  1. Get an instance of the Picture class.
  2. Populate the product_id foreign key from the method parameter.
  3. Populate the filename, since other computers don’t have access to the full path structure. We will use this filename later to display these pictures.
  4. Populate the description from the form data.
  5. Invoke Picture::create() to save the Picture record to the database.
  6. Redirect the client side to the Product index.

Viewing the Pictures

In such an application, we simply call up all the pictures in a product detail user story with a controller method as follows:

  1. public function detail($product_id){
  2.   $theProduct = $this->model('Product')->find($product_id);
  3.   $thePictures = $this->model('Picture')->getForProduct($product_id);
  4.   $theProduct->pictures = $thePictures;
  5.   $this->view('product/detail', $theProduct);
  6. }

By setting the pictures to be part of the product, we are able to pass all the data as a single object to then output the picture URLs as in the following view code:

  1. <?php
  2. foreach($data->pictures as $picture){
  3.   echo "<img src='/images/$picture->filename' style='max-width:100px;' />";
  4.   echo "<a href='/product/deletePicture/$picture->picture_id' class='btn btn-danger'>Delete picture</a>";
  5. }
  6. ?>

On line 3, we output an image element that will refer to the faved file in the images folder under the Web server document root. On line 4, we also include a delete link.

Deleting the Picture

Mistakes happen and things change, so it makes sense to be able to delete pictures from our Web application. The hyperlink form the above view calls the deletePicture method from our product controller, defined as follows:

  1. public function deletePicture($picture_id){
  2.   $thePicture = $this->model('Picture')->find($picture_id);
  3.   unlink(getcwd().DIRECTORY_SEPARATOR.'images'.DIRECTORY_SEPARATOR.$thePicture->filename);
  4.   $thePicture->delete();
  5.   header('location:/product/detail/'.$thePicture->product_id);
  6. }

The process is simply to find the Picture record using its primary key value, delete the associated file with the unlink instruction, and delete the picture record before redirecting back the appropriate page (here we go back to the product details.

The Picture Model and Database

To support all these operations, we need a Picture model similar to the following:

  1.  
  2. class Picture extends Model{
  3.   var $product_id;
  4.   var $filename;
  5.   var $description;
  6.  
  7.   public function getForProduct($product_id){
  8.     $SQL = 'SELECT * FROM Picture where product_id = :product_id';
  9.     $stmt = self::$_connection->prepare($SQL);
  10.     $stmt->execute(['product_id'=>$product_id]);
  11.     $stmt->setFetchMode(PDO::FETCH_CLASS, 'Picture');
  12.     return $stmt->fetchAll();
  13.   }
  14.  
  15.   public function create(){
  16.     $SQL = 'INSERT INTO Picture(product_id,filename,description) VALUE(:product_id,:filename,:description)';
  17.     $stmt = self::$_connection->prepare($SQL);
  18.     $stmt->execute(['product_id'=>$this->product_id,'filename'=>$this->filename,'description'=>$this->description]);
  19.     return $stmt->rowCount();
  20.   }
  21.  
  22.   public function find($picture_id){
  23.     $SQL = 'SELECT * FROM Picture WHERE picture_id = :picture_id';
  24.     $stmt = self::$_connection->prepare($SQL);
  25.     $stmt->execute(['picture_id'=>$picture_id]);
  26.     $stmt->setFetchMode(PDO::FETCH_CLASS, 'Picture');
  27.     return $stmt->fetch();
  28.   }
  29.  
  30.   public function update(){
  31.     $SQL = 'UPDATE Picture SET filename = :filename,description = :description WHERE picture_id = :picture_id';
  32.     $stmt = self::$_connection->prepare($SQL);
  33.     $stmt->execute(['filename'=>$this->filename,'description'=>$this->description,'picture_id'=>$this->picture_id]);
  34.     return $stmt->rowCount();
  35.   }
  36.  
  37.   public function delete(){
  38.     $SQL = 'DELETE FROM Picture WHERE picture_id = :picture_id';
  39.     $stmt = self::$_connection->prepare($SQL);
  40.     $stmt->execute(['picture_id'=>$this->picture_id]);
  41.     return $stmt->rowCount();
  42.   }
  43. }

This model performs operations on a Picture table with the following fields:

  • picture_id: the automatically incremented integer primary key
  • product_id: the foreign key to the product table
  • filename: a string type to hold the short filename
  • description: a text type field to hold long descriptions.

Conclusion

These general guidelines are also applicable to records that are child records of a master-detail relationship. You could change the upload types by modifying the upload whitelist and replacing getimagesize with other validation mechanisms.

Categories
MVC PHP

Filtering Access to Controller Classes and Methods (before PHP 8)

A feature of good routing is the possibility to enforce policies that get specified for Controller classes and methods. For this purpose, frameworks such as ASP.NET use annotations on classes and methods. We wish to replicate this in PHP, but PHP does not support annotations.

phpDoc Comments

phpDoc comments are the official means to document class and function API in PHP. These comments are delimited by /** and */ character sequences (note the opening tag has 2* characters. The usual contents should include method parameters, return values, summaries, etc. We aim to add our own annotation to this phpDoc which we will in turn use to filter user access.

Consider a simple controller class with phpDoc comments as below:

  1. <?php
  2. /**
  3. HomeController will handle operations related to Item entities.
  4. @accessFilter{LoginFilter}
  5. */
  6. class HomeController extends Controller{
  7. /**
  8. Provide a listing of the Item entities
  9. */
  10.   public function index(){
  11.     //...
  12.   }
  13.  
  14.   public function create(){
  15.     //...
  16.   }
  17. /** 
  18. @accessFilter{itemOwner}
  19. */
  20.   public function detail($item_id){
  21.     //...
  22.   }
  23. /**
  24. @accessFilter{itemOwner}
  25. */
  26.   public function edit($item_id){
  27.     //...
  28.   }
  29. /**
  30. @accessFilter{itemOwner}
  31. */
  32.   public function delete($item_id){
  33.     //...
  34.   }
  35. }
  36. ?>

In the above, we wish to apply the LoginFilter access policy to the entire class and the itemOwner policy only to Detail, Edit, and Delete methods.

ReflectionClass

For our purposes, phpDoc comments would serve no purpose without the means to efficiently extract them programatically. For this purpose, we use the ReflectionClass class, which provides an interface to all information about a selected class and its methods. Below, we extract the phpDoc comments of the class and each one of the methods:

  1. <?php
  2. $reflection = new ReflectionClass('HomeController');
  3. $classDoc = $reflection->getDocComment();
  4. $indexComment = $reflection->getMethod('index')->getDocComment();
  5. $createComment = $reflection->getMethod('create')->getDocComment();
  6. $detailComment = $reflection->getMethod('detail')->getDocComment();
  7. $editComment = $reflection->getMethod('edit')->getDocComment();
  8. $deleteComment = $reflection->getMethod('delete')->getDocComment();
  9. ?>

More specifically for our purposes, we need to extract the phpDoc comments for the specific controller and method resolved in the routing algorithm. (If you have been reading this site, I place this functionality in the App class constructor.)

Below is an example of how, given a class, method, and parameters, we should extract, parse, and resolve the filters.

  1. private static function redirectFilters($class,$method, $params){
  2.   $reflection = new ReflectionClass($class);
  3.  
  4.   $classDocComment = $reflection-&gt;getDocComment();
  5.   $methodDocComment = $reflection-&gt;getMethod($method)-&gt;getDocComment();
  6.  
  7.   //parse and extract the filters
  8.   $classFilters = self::getFiltersFromAnnotations($classDocComment);
  9.   $methodFilters = self::getFiltersFromAnnotations($methodDocComment);
  10.  
  11.   $filters = array_values(array_filter(array_merge($classFilters,$methodFilters)));
  12.  
  13.   $redirect = self::runFilters($filters, $params);
  14.   return $redirect;
  15. }

First, we initialize the ReflectionClass instance for this controller class and then extract the phpDoc comments for the class and method at hand. Then, we parse the phpDoc comment to retrieve any filter annotations and assemble these from the class and method in a single array. Then we run the filters and return the first redirection URL obtained, false otherwise. So the right time to call this method is right before calling the method on the controller class, as follows:

  1. if($redirectUrl = self::redirectFilters($this-&gt;controller, $this-&gt;method, $this-&gt;params)){
  2.   header("location:$redirectUrl");
  3.   return;
  4. }
  5.  
  6. call_user_func_array([$this-&gt;controller, $this-&gt;method], $this-&gt;params);

For any of this to work, of course, we must actually parse the phpDoc comments to extract the filter information, run the filters, and especially have filters to run in our code.

Parsing the phpDoc Comment

We must extract information from the phpDoc comments which resemble the following:

/**
Summary.
@accessFilter{filter1,filter2, filter3}
*/

For this purpose, we propose a method as follows:

  1. private static function getFiltersFromAnnotations($docComment){
  2.   preg_match('/@accessFilter:{(?<content>.+)}/i', $docComment, $content);
  3.   $content = (isset($content['content'])?$content['content']:'');
  4.   $content = explode(',',str_replace(' ', '', $content));
  5.   return $content;//this is an array
  6. }

On line 2, we extract all the accessFilter contents as elements in the $content array, with the key ‘content‘.

On line 3, we set $content to the value associated to the ‘content‘ key in $content, if any, or an empty string otherwise.

On line 4, we convert the string to an array of strings, one element per filter, without space characters.

Running the Filters

First and foremost, we must have filters to run. It is proposed to group these either in your routing class or a separate Filter class, as follows:

  1. <?php
  2. <?php
  3. class Filter extends Controller{
  4.   public static function itemOwner($params){
  5.     $theItem = self::model('Item')->find($params[0]);
  6.     if($theItem->user_id != $_SESSION['user_id']){
  7.       return '/home/index';
  8.     }else{
  9.       return false;
  10.     }
  11.   }
  12.   public static function LoginFilter($params){
  13.     if($_SESSION['user_id'] == null){
  14.       return '/login/index';
  15.     }else{
  16.       return false;
  17.     }		
  18.   }
  19. }
  20. ?>

Each of the above methods will check conditions based on the session variables, parameters, or core functionality. For example, LoginFilter verifies if a user is logged in and returns a redirection URL otherwise, or false if the user need not be redirected. The function itemOwner compares the user id of the user logged in with the user id in the record to verify if the current user is not attempting to modify someone else’s data.

The strategy to run the filters is to run them one by one until a redirection is determined or all have been run. We then return the redirection to the calling method which will send it to the routing algorithm.

  1. private static function runfilters($filters,$params){
  2.   $redirect = false;
  3.   $max = count($filters);
  4.   $i = 0;
  5.   while(!$redirect && $i < $max){
  6.     if(method_exists('Filter', $filters[$i])){
  7.       $redirect = Filter::{$filters[$i]}($params);
  8.     }else{
  9.       throw new Exception("No policy named $filters[$i]");
  10.     }
  11.     $i++;
  12.   }
  13.   return $redirect;
  14. }

Conclusion

All the pieces are in place. We can now implement redirection policies and apply them to classes and methods with a simple phpDoc comment. Note that there should only be ONE phpDoc comment per class and per method, which integrates all relevant annotations, including ours: @accessFilter. Otherwise, the ReflectionClass method getDocComment will only be able to return the one before and closest to the class or method declaration.

Categories
HTML

Improving your Views

So far, we have user very minimalistic views to prove that our application works. However, if we ever want to roll out a product that will be used by anyone, it has to provide a feeling of being well-built. And if it looks bad, it does not feel well-built.

Let’s start using Bootstrap to make our views look better.

Using Bootstrap

We will start by proposing a base template for views. When you create a new view, it should automatically contain this material in order to provide the wanted look and feel:

  1. <!doctype html>
  2. <html lang="en">
  3.   <head>
  4.     <!-- Required meta tags -->
  5.     <meta charset="utf-8">
  6.     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  7.  
  8.     <!-- Bootstrap CSS -->
  9.     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
  10.  
  11.     <title>TITLE</title>
  12.   </head>
  13.   <body>
  14.     <div class='container'>
  15.       <h1>HEADING</h1>
  16.  
  17.       <!-- Optional JavaScript -->
  18.       <!-- jQuery first, then Popper.js, then Bootstrap JS -->
  19.       <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
  20.       <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
  21.       <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
  22.     </div>
  23.   </body>
  24. </html>

Buttons, submit inputs and hyperlinks

All we need to do now in integrate the proper classes on the proper elements to make them look better. The first category is that of buttons (see https://getbootstrap.com/docs/4.0/components/buttons/):

  1. <button type="button" class="btn btn-primary">Primary</button>
  2. <button type="button" class="btn btn-secondary">Secondary</button>
  3. <button type="button" class="btn btn-success">Success</button>
  4. <button type="button" class="btn btn-danger">Danger</button>
  5. <button type="button" class="btn btn-warning">Warning</button>
  6. <button type="button" class="btn btn-info">Info</button>
  7. <button type="button" class="btn btn-light">Light</button>
  8. <button type="button" class="btn btn-dark">Dark</button>
  9.  
  10. <button type="button" class="btn btn-link">Link</button>

The class='btn btn-BUTTONTYPE' attribute definition is what will give the button appearance to any submit input, button, or hyperlink that you want. Therefore, the following all have a button appearance:

  1. <a href="CSTutoring.ca" class="btn btn-primary">CSTutoring.ca</a>
  2. <button type="button" class="btn btn-secondary">Secondary</button>
  3. <input type="submit" class="btn btn-success" value="Submit" />

Form Inputs

For forms to look adequate for a serious application, there are few modifications to make (see https://getbootstrap.com/docs/4.0/components/forms/):

  1. <div class="form-group">
  2.     <label for="exampleInputEmail1">Email address</label>
  3.     <input type="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="Enter email">
  4.     <small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
  5.   </div>

Two changes are essential: 1) apply class="form-group" to a div element containing the label and input elements and 2) apply class="form-control" to the input elements (except type submit that you will format as a button, as above).

Tables

Bootstrap can really help with the appearance of tables (see https://getbootstrap.com/docs/4.4/content/tables/). My preferred type is the striped table as below:

  1. <table class="table table-striped">
  2. ...
  3. </table>

Custom CSS and JS

In the PHP MVC application structure proposed and used in this series of posts, it is possible to use custom CSS and JS, referred through script and link elements of your views. You can simply create css and js folders under the Web server document root. The current .htaccess configuration is made to allow access to all the contained resources.

  • htdocs: contains index.php and the folders below
    • app: contains all models, views and controllers as well as core files.
    • css: add your custom CSS
    • js: add your custom JavaScript

Conclusion

For data-centric applications, it doesn’t take that much to make the application look acceptable. If anything, at least take the time to apply a Bootstrap theme on your application.

Categories
PHP

Arrays, Functions, Parameters and Return Values

Objectives

To learn to use several PHP functions useful for Web application development.

To learn to write and use your own functions – a prelude to writing your own classes.

Using Some Basic PHP Functions

We examine several other useful functions including some basic numeric PHP functions, e.g.,

abs() Function

The absolute value function takes a single numerical argument and returns its absolute value. For example,

  1. $x=abs(- 5);
  2. $y=abs(42);
  3.  
  4. print "x=$x y=$y";

Will output x=5 y=42

sqrt()

The square root function takes a single numerical argument and returns its square root. For example,

  1. $x= sqrt(25);
  2. $y= sqrt(24);
  3.  
  4. print "x=$x y=$y";

Will output x=5 y=4.898979485566

round()

The round function with 1 numerical argument returns the number rounded to the nearest integer. E.g.,

  1. $x=round( - 5.456);
  2. $y=round(3.7342);
  3. print "x=$x y=$y";

Will output x=-5 y=4

A 2nd argument can define the number of digits after the decimal point to round to. E.g.,

  1. $x=round( - 5.456,2);
  2. $y=round(3.7342,3);
  3. print "x=$x y=$y";

would output x=-5.46 y=3.734

is_numeric()

is_numeric () is useful for determining whether a variable contains a valid number or a numeric string. It returns true in this case and false otherwise, e.g.,

  1. if (is_numeric($input)) {
  2.   print "Valid number:$input";
  3. } else {
  4.   print "Not a valid number:$input";
  5. }

If $input == "6", the output would be Valid number:6

If $input == "Happy" the output would be Not a valid number:Happy

Testing for data types

The following functions test for different data types and return true if the variable contains data of the specified type.

  • is_ array(…),
  • is_ bool(…),
  • is_ callable(…),
  • is_ double(…),
  • is_ float(…),
  • is_ int(…),
  • is_ integer(…),
  • is_ long(…),
  • is_ null(…),
  • is_ object(…),
  • is_ real(…),
  • is_ resource(…),
  • is_ scalar(…),
  • is_ string(…).

The function string gettype(…) can be used to return the data type as a string.

rand() and srand()

The function rand() returns (pseudo)random numbers. The function srand() is used to seed the random function. Using a different seed each time the random number generator is used the first time in a program is a good way of getting a different sequence of random numbers for each program run. If the same seed is used for each run if the program, then the same sequence of numbers will be output by the random number generator, hence the term pseudo-random number generator.

Use srand() and microtime() to seed rand() and ensure it returns a random number, for example,

  1. srand ((double) microtime () * 10000000);
  2. $dice = rand(1, 6);
  3. print "Your random dice toss is $dice";

The random number generated in this case can be a 1, 2, 3, 4, 5, or 6.

print, echo, and quotation marks

You don’t need to use parentheses with print(), much like echo.

Double quotes are used to delimit strings that will be parsed for variables to substitute. Therefore,

  1. $x = 10;
  2. print ("Mom, please send $x dollars.");

will output Mom, please send 10 dollars.

Single quotes delimit string literals that will not be parsed. Therefore,

  1. $x = 10;
  2. print ('Mom, please send $x dollars.');

will output Mom, please send $x dollars.

You also need double quotation marks to generate special escaped characters like \n, \r, etc.

  1. $str = "Hi, Mom! \n"; //has an end of line
  2. $str2 = 'Hi, Mom! \n'; //has the characters \n
  3. echo nl2br($str); //will output an HTML <br> at the end of the string
  4. echo nl2br($str2); //will not output an HTML <br>

The nl2br function converts newline characters to the HTML <br> element. In the case on line 4, it does not output this element, because $str2 does not contain a newline character.

Generating HTML elements

Using single or double quotation statements can be useful when generating HTML tags print ‘<font color=”blue”>’;

This above is easier to understand and actually runs slightly faster than using all double quotation marks and the backslash ( \ ) character : print “<font color= \”blue \”>”;

Function syntax

Use the following syntax to write functions. Include parentheses at the end of the function name to hold the parameter list.

  1. function_name () {
  2.   …
  3. }

The following function takes 2 arguments:

  1. function OutputTableRow($c1, $c2) {
  2.   print "<tr><td>$c1</td><td>$c2</td></tr>";
  3. }

Consider the following code …

  1. <html>
  2. <head>
  3.   <title> Simple Table Function</title>
  4. </head>
  5. <body>
  6.   <font color="blue" size=4>
  7.   <table border=1>
  8.   <?php
  9.     function OutputTableRow( $col1, $col2 ) {
  10.       print "<tr><td>$col1</td><td>$col2</td></tr>";
  11.     }
  12.     for ( $i=1; $i<=4; $i++ ) {
  13.       $message1="Row $i Col 2";
  14.       $message2="Row $i Col 2";
  15.       OutputTableRow($message1, $message2);
  16.     }
  17.   ?>
  18.   </table>
  19. </body>
  20. </html>

Returning Values

Your functions can return data to the calling script. Your functions can return the results of a computation, the status of a process, etc. You can use the PHP return statement to return a value to the calling script statement:

  1. return $result;

This variable’s value will be returned to the calling script. For example

  1. function getMax ( $num1, $num2 ) {
  2.   // PURPOSE: returns largest of 2 numbers
  3.   // ARGUMENTS: $num1 - 1st number, $num2 - 2nd number
  4.   return ($num1 &gt; $num2?$num1:$num2);
  5. }

Using External Script Files

Sometime you will want to use scripts from external files, functions and classes from libraries. PHP supports 4 related functions:

  1. require("header.php");
  2. include("trailer.php");
  3. require_once("header.php");
  4. include_once("trailer.php");

The require() function produces a fatal error if it can’t insert the specified file. The include() function produces a warning if it can’t insert the file. Ensure inclusion/requiring is done only once by using the require_once or include_once respectively instead.

These search for the file named within the string parameter and insert its PHP, HTML, or JavaScript code into the current file.

For example, might use the following as footer.php.

<hr><p>123 Elm, Montreal, Qc, H0H0H0 - 514-555-5555</p>

Can include using: <?php include("footer.php"); ?>

Sequential Arrays

Use the array() function or [] to create an array.

  1. $students = array('Johnson', 'Jones', 'Jackson', 'Jefferson');
  2. $grades = [66, 75, 85, 80];

Another way to create an array You can also create an array by making individual value assignments into the array variable name. E.g.,

  1. $students[] = 'Johnson';
  2. $students[] = 'Jones';
  3. $students[] = 'Jackson';
  4. $students[] = 'Jefferson';

To reference individual array items, use an array name and index pair.

  1. $sports[0] = 'baseball';
  1. $names = array('Alice', 'Bob', 'Carol', 'Dennis');
  2. echo "$names[0], $names[1], $names[2], $names[3]";

By default sequential arrays start with index 0, e.g., the fourth element has index 3. Avoid referencing an item past the end of your array (for example, using $names[20] in an array that contains only four items).

Array indices can be whole numbers or a variable.

  1. $i =3;
  2. $classes = array('Math', 'History', 'Science', 'Pottery');
  3. $oneclass = $classes[$i - 1];
  4. print "$classes[$i] $oneclass $classes[1] $classes[0]";

This code outputs Pottery Science History Math

Changing arrays values: You can change values in an array as follows:

  1. $scores = array(75, 65, 85, 90);
  2. $scores[3] = 95;
  3. $average = ($scores[0] + $scores[1] + $scores[2] + $scores[3]) / 4;
  4. print "average=$average";

The output of the above PHP segment is average=80.

Explicitly-Set Indices

Below, we assign the value of 65 to the item with index 2, 85 to index 1.

  1. $scores = array(1=&gt;75, 2=&gt;65, 3=&gt;85);
  2. $scores[] = 100;
  3. print "$scores[1] $scores[2] $scores[3] $scores[4]";

Assign the value of 85 to the item with index 3. Add item with value 100 to the end of the array. The above outputs 75 65 85 100.

Loops and Sequential Arrays

Looping statements can be used to iterate through arrays

  1. $courses = array ('Perl', 'PHP', ' C','Java ', 'Pascal', 'Cobol', 'Visual Basic');
  2. for ($i=0; $i < count($courses); $i++) {
  3.   echo "$courses[$i] ";
  4. }

The above repeats 7 times, with $i equal to 0, 1, 2, 3, 4, 5, and 6. The above outputs: Perl PHP C Java Pascal Cobol Visual Basic .

The foreach loop

PHP supports the foreach statement as another way to iterate through arrays.

  1. $courses = array('Perl', 'PHP', 'C', ' Java','Pascal ', 'Cobol', 'Visual Basic');
  2. foreach ($courses as $item){
  3.   print ("$item ");
  4. }

The above outputs Perl PHP C Java Pascal Cobol Visual Basic .

Sorting data

For example the following code segment outputs
1 11 55 91 99 119 911 .

  1. $courses = array (91, 55, 11, 1, 99, 911, 119);
  2. sort($courses);
  3. foreach ($courses as $item) {
  4.   print "$item ";
  5. }

Arrays and User Input

Consider an example script that enables end-user to select multiple items from a checklist. Suppose the form inputs are generated as follows:

  1. <form action='' method='post'>
  2. <?php
  3. $topics = ['animals', 'cars', 'cooking', 'furniture'];
  4. foreach($topics as $topic){
  5.   echo "<input type='checkbox' name='pref[]' value='$topic' />$topic";
  6. }
  7. ?>
  8. ...
  9. </form>

Suppose you are receiving the previous form as follows:

  1. $pref = $_POST["prefer"];

If the user selected the second and fourth checkbox items on the previous form, then $pref[] would be an array of two items:

  • $pref[0], would have a value of cars, and
  • $pref[1] would have a value of furniture.

Adding and Deleting Items

  • array_shift() removes an item from the beginning of an array.
  • array_pop() removes an item from the end of an array.
  • array_unshift() adds an item to the beginning of an array.
  • array_push() adds an item to the end of an array.

Useful Array Functions

Use max() and min() to find the largest and smallest number in an array.

  1. $grades = array (99, 100, 55, 91, 65, 22, 16);
  2. $big=max($grades);
  3. $small=min($grades);
  4. print "max=$big min=$small";

The above would output: max=100 min=16.

Use array_sum () to return a sum of all numerical values. For example,

  1. $grades = array (25, 100, 50, 'N/A');
  2. $total= array_sum ($grades);
  3. print "Total=$total";

The above would output: Total=175

PHP will try to convert character to numerical values when it can. For example,

  1. $grades = array ('2 nights', '3days', 50, '1 more day');
  2. $total=array_sum($grades);
  3. print "total=$total";

Instead of generating an error message, this code outputs total=56.

Associative Arrays

PHP also supports arrays with string – value indices called associative arrays. For example, the following code creates an associative array with three items.

  1. $instructor['Science'] = 'Smith';
  2. $instructor['Math'] = 'Jones';
  3. $instructor['English'] = 'Jackson';

Use the array() function or [] along with the => operator to create an associative array, e.g.,

  1. $months = array( 'Jan'=&gt;31, 'Feb'=&gt;28, 'Mar'=&gt;31, 'Apr'=&gt;30, 'May'=&gt;31, 'Jun'=&gt;30, 'Jul'=&gt;31, 'Aug'=&gt;31, 'Sep'=&gt;30, 'Oct'=&gt;31, 'Nov'=&gt;30, 'Dec'=&gt;31 );

Use syntax similar to sequential arrays to access items: Enclose the index in square brackets.

  1. $days = $months['Mar'];

Note that you can’t fetch indices by using data values, as in the following example:

  1. $mon = $months[28]; //no can do

This syntax is incorrect because associative arrays can fetch data values only by using indices (not the other way around). This is the case for all arrays…

Multidimensional lists

Some data is best represented using a list of list or a multidimensional list, e.g.,

  1. $inventory=array (
  2.   'AC1000'=&gt;array('Part'=&gt;'Hammer', 'Count'=&gt;122, 'Price'=&gt;12.50),
  3.   'AC1001'=&gt;array('Part'=&gt;'Wrench', 'Count'=&gt;5, 'Price'=&gt;5.00),
  4.   'AC1002'=&gt;array('Part'=&gt;'Hand Saw', 'Count'=&gt;10, 'Price'=&gt;10.00),
  5.   'AC1003'=&gt;array('Part'=&gt;'Screw Driver', 'Count'=&gt;222, 'Price'=&gt;3.00)
  6. );

$inventory[‘AC1000’][‘Part’] has the value Hammer, $inventory[‘AC1001’][‘Count’] has the value 5, and $inventory[‘AC1002’][‘Price’] has the value 10.00.

Categories
MVC PHP

Intro to PDO

PDO – PHP Data Objects – is a database access layer providing a uniform method of access to multiple database management systems. PDO does not account for database-specific syntax, but can allow for the process of switching databases and platforms to be fairly painless, simply by switching a connection string.

PDO - db abstraction layer

Database Support

The extension can support any database for which a PDO driver has been written. To find out which drivers you have, run the following command:

  1. print_r(PDO::getAvailableDrivers());

Connecting

Different databases may have slightly different connection methods. Below, the method to connect to MySQL databases is shown:

  1. $host = 'localhost';
  2. $dbname = 'application';
  3. $user = 'app_account';
  4. $pass = 'app_account_pass';
  5. try {   # MySQL with PDO_MYSQL
  6.   $DBH = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);
  7. }
  8. catch(PDOException $e) {
  9.     echo $e->getMessage();
  10. }

Take note of the try/catch block – you should always wrap your PDO operations in a try/catch, and use the exception mechanism. $DBH stands for ‘database handle’.

You can close any connection by setting the handle to null.

  1. # close the connection
  2. $DBH = null;

You can get more information on database-specific options and/or connection strings for other databases from PHP.net.

Exceptions and PDO

PDO can use exceptions to handle errors, which means anything you do with PDO should be wrapped in a try/catch block. You can force PDO into one of three error modes by setting the error mode attribute on your newly created database handle. Here’s the syntax:

  1. $DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
  2. $DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
  3. $DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );

No matter what error mode you set, an error connecting will always produce an exception, and creating a connection should always be contained in a try/catch block.

PDO::ERRMODE_SILENT

This is the default error mode. If you leave it in this mode, you’ll have to check for errors in the way you’re probably used to if you used the mysql or mysqli extensions. The other two methods are more ideal for DRY programming.

PDO::ERRMODE_WARNING

This mode will issue a standard PHP warning, and allow the program to continue execution. It’s useful for debugging.

PDO::ERRMODE_EXCEPTION

This is the mode you should want in most situations. It fires an exception, allowing you to handle errors gracefully and hide data that might help someone exploit your system. Here’s an example of taking advantage of exceptions:

  1. # connect to the database
  2. try {
  3.   $DBH = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);
  4.   $DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
  5.   $DBH->prepare('DELECT name FROM people');
  6.   # Typed DELECT instead of SELECT!
  7. }
  8. catch(PDOException $e) {
  9.   echo "Error!";
  10.   file_put_contents('PDOErrors.txt', $e->getMessage(), FILE_APPEND);
  11. }

There’s an intentional error in the select statement; this will cause an exception. The exception sends the details of the error to a log file, and displays a friendly (or not so friendly) message to the user.

Insert and Update

Inserting new data, or updating existing data is one of the more common database operations. Using PDO, this is normally a two-step process. Everything covered in this section applies equally to both UPDATE and INSERT operations.

2 to 3 step insert and update

Here’s an example of the most basic type of insert:

  1. # STH means "Statement Handle"
  2. $sql = "INSERT INTO person(first_name) VALUES('Alice')";
  3. $stmt = $DBH->prepare($sql);
  4. $stmt->execute();

You could also accomplish the same operation by using the exec() method, with one less call. In most situations, you’re going to use the longer method so you can take advantage of prepared statements. Even if you’re only going to use it once, using prepared statements will help protect you from SQL injection attacks.

Prepared Statements

Using prepared statements will help protect you from SQL injection. A prepared statement is an SQL statement that can be executed multiple times just by changing the data and running the statement. It has the added advantage of automatically making the data used in the placeholders safe from SQL injection attacks, as part of the preparation. You use a prepared statement by including placeholders in your SQL. Here are three examples: one without placeholders, one with unnamed placeholders, and one with named placeholders.

  1. # no placeholders - vulnerable to SQL Injection!
  2. $STH = $DBH->prepare("INSERT INTO person(first_name, addr, city) VALUES($name, $addr, $city)");
  3. # unnamed placeholders
  4. $STH = $DBH->prepare("INSERT INTO person(first_name, addr, city) VALUES(?, ?, ?)");
  5. # named placeholders
  6. $STH = $DBH->prepare("INSERT INTO person(first_name, addr, city) VALUES(:name, :addr, :city)");

You want to avoid the first method; it’s here for comparison. The choice of using named or unnamed placeholders will affect how you set data for those statements.

Unnamed Placeholders

Hint: read this only to understand how trrible this technique actually is. For a better technique see the following sections.

  1. # unnamed placeholders
  2. $stmt = $DBH->prepare("INSERT INTO person(first_name, addr, city) VALUES(?, ?, ?)");
  3.  
  4. # assign variables to each place holder, indexed 1-3
  5. $stmt->bindParam(1, $first_name);
  6. $stmt->bindParam(2, $address);
  7. $stmt->bindParam(3, $city);
  8.  
  9. # insert one row
  10. $first_name = "Alice"
  11. $address = "123 Elm";
  12. $city = "Montreal";
  13. $stmt->execute();
  14.  
  15. # insert another row with different values
  16. $first_name = "Bob"
  17. $address = "124 Elm";
  18. $city = "Montreal";
  19. $stmt->execute();

There are two steps here. First, we assign variables to the various placeholders (lines 2-4). Then, we assign values to those placeholders and execute the statement. To send another set of data, just change the values of those variables and execute the statement again.

Does this seem a bit unwieldy for statements with a lot of parameters? It is. However, if your data is stored in an array, there’s an easy shortcut:

  1. # the data we want to insert
  2. $data = ['Cathy', '125 Elm', 'Montreal'];
  3. $stmt = $DBH->prepare("INSERT INTO person(first_name, addr, city) VALUES(?,?,?);
  4. $stmt->execute($data);

The data in the array applies to the placeholders in order. $data[0] goes into the first placeholder, $data[1] the second, etc. However, if your array indexes are not in order, this won’t work properly, and you’ll need to re-index the array.

Named Placeholders

You could probably guess the syntax, but here’s an example:

  1. # the first argument is the named placeholder name - notice named
  2. # placeholders always start with a colon.
  3. $stmt->bindParam(':name', $name);

You can use a shortcut here as well, but it works with associative arrays. Here’s an example:

  1. # the data we want to insert
  2. $data = ['first_name'=>'Cathy', 'address'=>'125 Elm', 'city'=>'Montreal']; 
  3. # the shortcut!
  4. $stmt = $DBH->prepare("INSERT INTO person(first_name, address, city) value (:first_name, :address, :city)");
  5. $stmt->execute($data);

The keys of your array do not need to start with a colon, but otherwise need to match the named placeholders. If you have an array of arrays you can iterate over them, and simply call the execute with each array of data.

Another nice feature of named placeholders is the ability to insert objects directly into your database, assuming the properties match the named fields. Here’s an example object, and how you’d perform your insert:

  1. # a simple object
  2. class person {
  3.   public $first_name;
  4.   public $address;
  5.   public $city;
  6.  
  7.   function __construct($fname,$addy,$city){
  8.     $this->first_name = $fname;
  9.     $this->address = $addy;
  10.     $this->city = $city;
  11.   }
  12.   # etc ...
  13. }
  14.  
  15. $newPerson = new person('Johnny','125 Elm','Montreal');
  16. # here's the fun part:
  17. $stmt = $DBH->prepare("INSERT INTO person(first_name, address, city) VALUE(:first_name, :address, :city)");
  18. $stmt->execute((array)$newPerson);

By casting the object to an array in the execute, the attribute names are mapped to keys and the values are set from each attribute.

Selecting Data

Fetch data into arrays or objects

Data is obtained via the ->fetch(), a method of your statement handle. Before calling fetch, it’s best to tell PDO how you’d like the data to be fetched. You have the following options:

  • PDO::FETCH_ASSOC: returns an array indexed by column name
  • PDO::FETCH_BOTH (default): returns an array indexed by both column name and number
  • PDO::FETCH_BOUND: Assigns the values of your columns to the variables set with the ->bindColumn() method
  • PDO::FETCH_CLASS: Assigns the values of your columns to properties of the named class. It will create the properties if matching properties do not exist
  • PDO::FETCH_INTO: Updates an existing instance of the named class
  • PDO::FETCH_LAZY: Combines PDO::FETCH_BOTH/PDO::FETCH_OBJ, creating the object variable names as they are used
  • PDO::FETCH_NUM: returns an array indexed by column number
  • PDO::FETCH_OBJ: returns an anonymous object with property names that correspond to the column names

In reality, there are three which will cover most situations: FETCH_ASSOC, FETCH_CLASS, and FETCH_OBJ. In order to set the fetch method, the following syntax is used:

  1. $stmt->setFetchMode(PDO::FETCH_ASSOC);

You can also set the fetch type directly within the ->fetch() method call.

FETCH_ASSOC

This fetch type creates an associative array, indexed by column name. This should be quite familiar to anyone who has used the mysql/mysqli extensions. Here’s an example of selecting data with this method:

  1. # can use the query() method when no input in the statement
  2. $tmt = $DBH->query('SELECT first_name, address, city from person');
  3. # setting the fetch mode
  4. $stmt->setFetchMode(PDO::FETCH_ASSOC);
  5. while($rec = $stmt->fetch()) {
  6.   echo $rec['first_name'] . "<br>";
  7.   echo $row['address'] . "<br>";
  8.   echo $row['city'] . "<br>";
  9. }

The while loop will continue to go through the result set one row at a time until complete.

FETCH_OBJ

This fetch type creates an object of std class for each row of fetched data. Here’s an example:

  1. # creating the statement
  2. $stmt = $DBH->query('SELECT first_name, address, city from person');
  3. # setting the fetch mode 
  4. $stmt->setFetchMode(PDO::FETCH_OBJ);
  5. # showing the results
  6. while($row = $stmt->fetch()) {
  7.   echo $row->first_name . "<br>";
  8.   echo $row->address . "<br>";
  9.   echo $row->city . "<br>";
  10. }

FETCH_CLASS

Important note: when using this fetch mode, the properties of your object are set BEFORE the constructor is called. Therefore, you can use the constructor to modify and hide the data as required for your application.

This fetch method allows you to fetch data directly into a class of your choosing. When you use FETCH_CLASS, the properties of your object are set before the constructor is called. Read that again, it’s important. If properties matching the column names don’t exist, those properties will be created (as public) for you.

This means if your data needs any transformation after it comes out of the database, it can be done automatically by your object as each object is created.

As an example, imagine a situation where the address needs to be partially obscured for each record. We could do this by operating on that property in the constructor. Here’s an example:

  1. class obscure_person {
  2.   public $first_name;
  3.   public $address;
  4.   public $city;
  5.   public $notes;
  6.   function __construct($notes = ''){
  7.     $this->address = preg_replace('/[a-z0-9]/i', '*', $this->address);
  8.     $this->notes = $notes;
  9.   }
  10. }

As data is fetched into this class, the address has all its lowercase a-z letters and digits 0-9 replaced by the character *. Now, using the class and having that data transformation occur is completely transparent:

  1. $stmt = $DBH->query('SELECT first_name, address, city from person');
  2. $stmt->setFetchMode(PDO::FETCH_CLASS, 'obscure_person');
  3. while($obj = $STH->fetch()) {
  4.   echo "Obscure $obj->first_name lives at $obj->address";
  5. }

If the address was ‘123 Elm,’ you’d see ‘*** ***’ as your output. Of course, there may be situations where you want the constructor called before the data is assigned. PDO can also accommodate this by adding PDO::FETCH_PROPS_LATE as follows:

  1. $stmt->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, 'obscure_person');

Now, when you repeat the previous example with this fetch mode the address will NOT be obscured, since the constructor was called before the properties were assigned.

Finally, if you really must, you can pass arguments to the constructor when fetching data into objects with PDO:

  1. $stmt->setFetchMode(PDO::FETCH_CLASS, 'secret_person', ['data']);

If you must pass different data to the constructor for each object, you can set the fetch mode inside the fetch method:

  1. $i = 0;
  2. while($rowObj = $stmt->fetch(PDO::FETCH_CLASS, 'obscure_person', [$i])) {
  3.   // operations
  4.   $i++;
  5. }

Some Other Helpful Methods

While this isn’t meant to cover everything in PDO (it’s a huge extension!) there are a few more methods you’ll want to know in order to do basic things with PDO.

  1. $DBH->lastInsertId();

The lastInsertId() method of PDO (not PDOStatement) will return the last inserted primary key value for this connection.

  1. $DBH->exec('DELETE FROM person WHERE 1');
  2. $DBH->exec("SET time_zone = '-8:00'");

The exec() method is used for operations that cannot return data other than the number of affected rows.

  1. $safe = $DBH->quote($unsafe);

The quote() method quotes strings so they are safe to use in queries. This is your fallback if you’re not using prepared statements.

  1. $rows_affected = $stmt->rowCount();

The rowCount() method returns an integer indicating the number of rows affected by an operation.

Categories
HTML MVC PHP

Deeper in Models

In this post, we will explore more database transactions in model classes with the previous Item class example. We will also create views that can support these operations.

SQL DML (Data Modification Language) are these operations that we must implement in our model classes. They comprise all CRUD (Create, Read, Update, Delete) operations, i.e., respectively INSERT, SELECT, UPDATE, and DELETE.

Create

We add a create method to the Item model class as follows:

public function create(){
  $SQL = 'INSERT INTO Item(name) VALUES (:name)';
  $stmt = self::$_connection->prepare($SQL);
  $stmt->execute(['name'=>$this->name]);
  return self::$_connection->lastInsertId();
}

In the above, we first define the SQL command we will use, with a named placeholder, :name. We then get PDO to prepare the statement; the use of prepared statements has many advantages including reusability and SQL-injection prevention (this happens later).

Next, we run the prepared statement with the execute method. The parameter is an array ([...]) containing a dictionary of values for this statement, i.e., if there is a :name placeholder in the SQL, then there must be a name key-value pair in this dictionary. The execute method will take all this data and sanitize it, preventing any special character that could be used to perform SQL injection. In other words, if you are using prepared statements in all your models such that all data is passed into the statements through the execute method, then you have been diligent in preventing SQL injection.

The final step in the create method is to return the primary key value assigned to the created record. This value is fetched by PDO’s lastInsertId() method.

We now focus on the view used to create this record. We create a view, /app/view/home/create.php that will be used for data entry by the user, as follows:

<html>
  <head><title>Create an Item</title></head>
  <body>
    <h1>Create an Item</h1>
    <form action='' method='post'>
      <label>Name:<input type='text' name='name' /></label><br />
      <input type='submit name='action' value='Create' /> <a href='/home/index'>Cancel</a>
    </form>
  </body>
</html> 

The form is submitted to the same URL from which it was called. This way, we can write the instructions to call the view and to add the record in the same controller method in HomeController:

public function create(){
  if(isset($_POST['action'])){
    $newItem = $this->model('Item');
    $newItem->name = $_POST['name'];
    $newItem->create();
    header('location:/Home/index');
  }else{
    $this->view('home/create');
  }
}

The create method is written in two branches, the first if the submit button named action is pressed. We instantiate an Item object and initialize its name attribute (its only attribute) before calling the create() method to store it. Finally, we send the browser headers to redirect to the /Home/index URL for the application, which will display the list of items. The user can also navigate back to the list of items by blicking Cancel.

Read

We now focus on a detail-view feature for the records. Bear in mind that this is especially useful in cases where records hold many fields. We first add a find(…) method to the model class:

public function find($item_id){
  $SQL = 'SELECT * FROM Item WHERE item_id = :item_id';
  $stmt = self::$_connection->prepare($SQL);
  $stmt->execute(['item_id'=>$item_id]);
  $stmt->setFetchMode(PDO::FETCH_CLASS, 'Item');
  return $stmt->fetch();
}

In contrast with the get() method we wrote in An Introduction to Models, the SQL includes a WHERE clause to choose the record based on the primary key value and this value is passed in the call to the execute() method. Finally, the is returned after a call to fetch() such that the single matching record is returned as an object, without being placed in an array.

To display the record, we build a detail view, /app/views/home/detail.php which should include all record fields in the display:

<html>
  <head><title>Item Details</title></head>
  <body>
    <h1>Item Details</h1>
    <dl>
      <dt>Name:</dt>
      <dd><?=$data->name ?></dd>
    </dl>
<a href='/home/index'>Back to list</a>
  </body>
</html> 

Above, we are assuming the single record is passed to the view and that the name attribute is displayed by the <?=expression ?> tags which are shorthand for <?php echo expression; ?>.

We complete this part with method detail in HomeController as follows:

public function detail($item_id){
  $theItem = $this->model('Item')->find($item_id);
  $this->view('home/detail', $theItem);
}

On the first line, we invoke the method to find the wanted record and store it in $theItem, which is then passed to the newly-created view on the following line.

Update

Another important part of a data-centric Web application is the ability to modify data. In a MySQL database, this is done through UPDATE SQL statements, as in the following update method added to the Item model.

public function update(){
  $SQL = 'UPDATE Item 
          SET name = :name 
          WHERE item_id = :item_id';
  $stmt = self::$_connection->prepare($SQL);
  $stmt->execute(['name'=>$this->name,
                  'item_id'=>$this->item_id]);
  return $stmt->rowCount();
}

First we write a proper SQL UPDATE statement with named placeholders where the data is meant to go. Then we prepare the statement and execute it with the needed data, from the object. Finally, we return the rowCount as feedback about how many rows were modified.

The view to support this process must allow data entry from the user AND have the data entry fields prepopulated with the existing record field values. We add the view /app/views/home/edit.php as follows:

<html>
  <head><title>Edit an item</title></head>
  <body>
    <h1>Edit an item</h1>
    <form action='' method='post'>
      <label>Name:<input type='text' name='name'
           value='<?= $data->name ?>' /></label><br />
      <input type='submit name='action' value='Save' /> <a href='/home/index'>Cancel</a>
    </form>
  </body>
</html> 

For the view to work properly, it must receive the record as its $data parameter. We put all this together in the HomeController edit method, as follows:

public function edit($item_id){
  $theItem = $this->model('Item')->find($item_id);
  if(isset($_POST['action'])){
    $theItem->name = $_POST['name'];
    $theItem->update();
    header('location:/Home/index');
  }else{
    $this->view('home/edit', $theItem);
  }
}

This method is very similar to create, in that it handles displaying a form, processes the submitted data, and redirects back to the list of all items. The distinctions are that edit operates on an existing record and thus must find and present it in the view prior to calling for updates with the user input. Naturally, if a user clicks the Cancel hyperlink, they navigate back to the index method, so this case need not be addressed here.

Delete

To complete the basic data management for the Item table, we must have the ability to delete records. We will implement the Item model class delete method as follows:

public function delete(){
  $SQL = 'DELETE FROM Item WHERE item_id = :item_id';
  $stmt = self::$_connection->prepare($SQL);
  $stmt->execute(['item_id'=>$this->item_id]);
  return $stmt->rowCount();
}

It is easy to also implement this method to take the item_id as a parameter and forego using the actual object attributes.

The pattern to process this request is familiar by now, but in a nutshell: define the DELETE statement with an :item_id placeholder; prepare the statement; execute the statement with the data to replace the placeholders; and return the number of rows affected by the change.

To make this process go smoothly, there are two possibilities: with or without a deletion confirmation view. We write a deletion confirmation view /app/views/home/delete.php as follows:

<html>
  <head><title>Delete an item</title></head>
  <body>
    <h1>Delete an item</h1>
    <form action='' method='post'>
      <label>Name:<input type='text' name='name'
        value='<?= $data->name ?>' disabled />
        </label><br />
      <input type='submit name='action' value='Delete' /> <a href='/home/index'>Cancel</a>
    </form>
  </body>
</html> 

So the user will click on the Delete button to confirm deletion and the Cancel link to cancel the deletion.

To complete the deletion implementation, we add the delete method to the HomeController as follows:

public function delete($item_id){
  $theItem = $this->model('Item')->find($item_id);
  if(isset($_POST['action'])){
    $theItem->delete();
    header('location:/Home/index');
  }else{
    $this->view('home/delete', $theItem);
  }
}

In the above, we only handle the cases when the method is called with or without the action button being clicked. These cases happen upon submitting the form and navigating to delete, respectively.

Tying it All Together

The only aspect missing from this application is the set of hyperlinks that allow navigation. Since all views have links back to /home/index and since all methods redirect to /home/index as well, the links to go to access create, detail, update, and delete methods must be implemented in the view for the index method of the HomeController class; we modify it as follows:

<html>
<head><title>Item List</title></head>
<body>
<h1>List of items</h1>
<a href='/home/create/'>Add an item</a>
<table>
<tr><th>Name</th><th>Actions</th></tr>
<?php
  foreach($data['items'] as $item){
    echo "<tr><td>$item->name</td><td>
      <a href='/home/detail/$item->item_id'>Details</a>
      <a href='/home/edit/$item->item_id'>Modify</a>
      <a href='/home/delete/$item->item_id'>Delete</a>
      </td></tr>";
  }
?>
</table>
</body>
</html>

Notice the hyperlinks all lead to URLs which will be used to invoke controller methods with their parameters. The controller methods will, in turn, call the appropriate views and process the responses to finally redirect as appropriate.