Create a PHP REST API : Write a RESTful API from Scratch using Plain, Object-Oriented PHP and MySQL

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
in this video you'll learn how to create a  complete restful api using plain object-oriented   php and mysql all the code developed in this video  is free to download and use and the link is in the   description to make api requests you'll need  an api client for example curl postman or http   in this video i'll be using httpi on the command  line we'll start with the api endpoints first   we'll create a new file called index.php in  here we'll start with the php opening tag   and for now let's just print out the value of the  request uri element from the server super global let's see what response we get from that page on  the command line using httpi we make a get request   to that page and we see the response status code  which is 200 the response headers and the response   body in the response body we see the output of  the vardump call and this is printing out the   path part of the url index.php if we make that  request again but remove the index.php part as   index.php is the default the request still works  and the value we get printed out is just the slash   so this server variable contains the part  of the url after the host name as the api   is going to be restful however we want a specific  endpoint url format a restful api consists of two   urls one for a collection of resources and one for  an individual one that contains the resource id   the result of calling the endpoint  depends on the combination of the url   and the request method used so in order to have  restful urls we need to do some url rewriting   as i'm using apache let's add a new file called  htaccess where we can specify server configuration   settings in here we'll turn the rewrite engine on  then add a rewrite rule with a pattern of a dot   that matches any url and we'll  substitute this with the index.php script   basically this says that any url will now call  the index.php script no matter what it is these   rules are only valid for apache and compatible  web servers and i'm deliberately keeping this   very simple for the purposes of this video but if  you need the rewrite rules for another web server   let me know in the comments let's give that a try  now if i make a request to any url the index.php   script is executed and we get the path part of  the url printed out as an aside with httpi the   default request method is get so if we're making  a get request we don't need to specify the method   also the http protocol is the default so  we don't need to specify that either now   we have access to this part of the url from our  code we can pass it to get the segments we need so instead of printing this out we'll  use the explode function to split it up   using the forward slash character we'll  assign this to a variable and print that out if we call the slash product endpoint then  we get an array with two elements the element   with an index of one containing the string  products if we add an id to that endpoint   then this is placed in an additional array element  we're just creating an api for products so if the   url is something else we'll respond with a 404  status code which you'll no doubt be familiar with   so instead of printing this out if  the first segment isn't products   we'll call the http response code function  setting the status code of 404 and we'll   exit the script as for the id this will be  the second segment or if it's not specified   we'll default it to null for now let's  just print that out to see what it contains   if we make a request to an end point that isn't  slash products we get the 404 not found response   if we make a request to the collection endpoint   we get a response with a status code of 200 which  is the default success response code and the id   is null if we add the id to  the url we get this printed out note that for the purposes of this  video i'm keeping the routing simple   with a simple rewrite rule and passing the  url manually like this in a more complex   project you'd probably use a third-party router  component like fastroot to provide the routing   next let's add a controller class to control the  response to this first let's add a new folder   to store the class files called source then a  new file in here called productcontroller.php in here we'll add the php opening tag and the  class definition next let's add a public method   to this called process request the response  depends on the request method and if there's a   resource id or not so let's add a string argument  for the method and a string argument for the id   to keep it simple we'll just echo out the response  from this method so we'll declare the return type   as void as we just saw when we made a request the  id can be null so we'll make this type declaration   nullable inside this method for now let's just  print out the values of these two arguments   back in the index script as we're using type  declarations let's enable strict types at   the top of the script then in order to use the  class we just added we need to require the file   that it's in instead of explicitly doing this  as we'll be adding more class files later on   let's add an auto loader to load class files  automatically we'll do this by calling the spl   auto load register function passing in  a callback function that will be called   whenever a previously unused class is referenced  passing in the name of that class as an argument   in the body of the function we'll require that  class in the source folder using the dir constant   to reference the directory of the current file the  class variable passed into the function contains   the name of the class you are trying to use so as  the file name matches the class name we can just   add the dot php suffix and we'll get the right  file name in a larger project you'd probably use   composer's autoloader but again for the purposes  of this video we're keeping it as simple as we can   then at the end of the script now we can  use the controller class we just added   so instead of printing out the id  variable we'll create a new object   of the product controller class then we  can call the process request method on this   passing in the request method which we can  get from the server super global then the id so now if we make a request to slash  products the controller class is loaded   automatically and we get the request method  printed out which is get and the id is null   if i add the id then we get that printed out too   if i change the request method for example  to post then this is what is printed out   back in the controller now we can actually process  the request instead of printing out this message   we're going to base the response on the request  method and the id if there's an id then it's a   request that affects a single resource and if  not it's a request that affects a collection of   resources instead of doing all the processing  in one method let's add separate methods for   each one of these a process resource request  method passing in the request method and the   id as arguments and a process collection request  method to which we'll just pass the request method   then back in the process request  method we can call these two methods let's start by processing the collection requests a get request to slash products should return a  list of products and a post request will create a   new product so in the process collection request  method let's check the request method with a   switch statement if it's get will return a list of  products the standard format of an api response is   javascript object notation or json so let's start  by just outputting an array containing a sample   element encoded as json then a break statement  to end the case block if we make a get request   to slash products then we see the array in the  response body however if we look at the content   type response header it's set to text html  which is the default as we're returning json   we need to set this to the json content  type instead of doing this in here as every   response we send back from the api will be in  json format we'll do it in the index script   so just after we set up the auto loader we'll call  the header function passing in a string to set the   content type header to application json and also  additionally specifying the character set as utf-8 so when we make the same request the  content type header is set to json   plus using httpi the response body now  contains additional color formatting   now we have this setup we can return some  actual data instead of this hard-coded value   i'm going to create a database  quickly by running this sql   this will create a database called productdb  and a table in that database called product   this table contains various  columns of different types   to demonstrate how these are handled in an api  client and how they're json encoded there's a   link to the source code that includes this sql  in the description of this video let's run that and there's the database and the product table while we're here let's just insert a  couple of example records into this table   we don't need to provide values for the id  column as being an auto increment column   values for this will be assigned automatically  if we browse the table there are the two records now we can connect to the database from  php first let's add a class to represent   the database connection so we'll add a new  file in the source folder called database.php   in here we'll add the php opening tag and the  class definition to connect to the database   we need details such as the hostname database name  username and password let's pass these values in   when we create an object of this class using  the constructor method we'll add arguments for   the database host database name username and  password if you need any other values like the   port for example then you can include them here  we'll use constructive property promotion to   automatically create private properties for each  of these and assign the values of these arguments   to these properties next let's add a method called  get connection which will return a pdo object   in the body of this method let's create a variable  for the dsn containing the hostname and database   name using the values of the properties we  created above then we'll create a new pdo   object passing in the dsn username and password  and we'll return this object from the method in the index page let's create an object  of this class passing in the database   host name username and password i'm using the  root account which by default has a blank password   this is fine when you're developing locally but  on a production machine the connection should   have its own username and password also to keep  it simple i'm going to hard code these values in   here but in reality what you'd probably do is  have these settings in a separate config file   to test the database code we just created let's  call the get connection method on this object if we make a get request to slash products then  we get the same response as earlier with the   json test data this means that the class has  been loaded automatically and we connect it to   the database successfully however let's see what  happens if i get one of the database connection   details wrong for example an incorrect password  now if we repeat the request we get an exception   this is as expected however by default  the exception details are formatted as   html we always want to return json  from the api even if there is an error   to fix this we could put a try catch block around  the get connection method call and output the   error details as json in the catch block instead  however let's add a generic exception handler that   will output any unhandled exception as json we can  do this with the set exception handler function   this takes a callback function that will be  executed when an unhandled exception occurs   the single argument to this function is an object  of the throwable class so let's add a new file in   the source folder called errorhandler.php  and in here we'll add the php opening tag   and the class definition then let's add a method  called handle exception with the single argument   of type throwball that represents the exception  that's been thrown and doesn't return anything in   the body of the method we'll output some json that  contains various data from the exception object we   can get the error code the error message the file  it occurred in and the line number in that file   in addition to outputting the exception details  in the response body let's also set the http   status code to 500 indicating that there is an  error on the server to use this class as the   generic exception handler in the index file we  call the set exception handler function passing   in a string that identifies the handle exception  method in the error handler class we just added   we do this as early as possible in this script  so that it's enabled as soon as possible   but so that the class is loaded automatically we  have to do it after we've created the autoloader   let's make the api request again and now  we get the error details encoded as json   along with the 500 internal  server error http status   now we've tested the error handler we can change  the password back to the correct one next we can   use this database connection to make a query  to the database we'll do this in a separate   class we'll start by adding a new file in  the source folder called productgateway.php this class uses the table gateway pattern  basically its methods serve as a gateway to the   product table in the database we'll add the php  opening tag and the class definition the methods   in this class depend on a database connection  so we'll pass an object of the database class   in using the constructor all the methods in this  class are going to use the database connection   so instead of using constructor property  promotion to simply store this value in a property   let's add a private property to the class to  store the pdo connection object then in the   body of the constructor method we'll call the get  connection method on the database object and store   this in the property then let's add a method  to get all the records from the product table   we'll add a new public method called get all and  this will return an array in the body of this   method we'll add a variable containing the sql  to select all the records from the product table   we'll keep it simple with no order by clause or  limit or anything like that just the sql to select   all of the records then we'll call the query  method on the connection object passing in the sql   and assigning the returned pdo statement object  to a variable we want to return an array of rows   so let's initialize a variable with an empty array  then we'll use a while loop to fetch each row in   turn as an associative array inside the loop  we'll append the row to the array and finally   in this method return the array of row data now we  can call this method to query the database which   we'll do here in the product controller class  however instead of creating an object of the   product gateway class in here we'll pass one in as  a dependency so let's add the constructor method   with a product gateway object argument and  we'll add a private visibility modifier to this   so that the value of this argument will  be assigned to a private property with the   same name then in the process collection request  method instead of outputting this hard-coded array   we'll call the get all method on the gateway  property to get this working back in the index   script we just need to stitch all this together  so instead of calling the get connection method   in here we'll create a new product gateway object  passing in the database as an argument then we   can pass this gateway object in as an argument  when we create the product controller object   let's give that a try now if we make the same  request again we get the json containing the   two records from the database that we inserted  earlier note however that the values for each   record are encoded as strings even though in the  database the id and size columns are integers   and the is available column is boolean this is  because pdo is converting these values to strings if the pdo stringify fetch's attribute is true   then all values will be converted to  strings so we need to set this to false   we also need to set the emulate prepares  attribute to false so that this works so in the database class when we create a  new pdo object we'll add a fourth argument   that's an array and we'll set both the emulate  prepares and stringify fetch's attributes to false   now if we make the api request again the  numbers have no longer been converted to strings   note however that the boolean value is also  a number one or zero this is to be expected   as internally the database server stores boolean  values as one for true and zero for false if   you want these to be boolean literals in the  json then they have to be converted manually so in the product gateway class inside the  while loop where we're getting each row from the   database before we append the row to the array  we'll cast the is available column to boolean now when we make the api request the is  available column is output as a boolean literal   true or false so when we make a get request  to the collection url slash products we get   a list of all the products next let's add the  functionality to create a new product which we   do by making a post request to the same url so  in the product controller class in the process   collection request method we'll add a new case to  the switch statement for when the request method   is post first we need to get the data from the  request with a post request for a regular web form   that you would submit from a browser the data from  that form would be available in the post array   so let's assign that array to a variable and  print it out to see what we get if we make a post   request to the collection endpoint with some data  in the request we get an empty array printed out   by default httpi sends data in the request encoded  as json as the api is responding with json encoded   data we'll require that data sent to the api is  encoded as json also instead of the post array   we get json data from the request  directly from the php input stream   we do this by using the file get contents function  passing in this string to identify that stream now if we make the same request we get a string  containing some json let's decode that json   using the json decode function passing in true as  the second argument to get an associative array   if we make the request again now we have an  associative array containing the data from the   request if we add another item of data this adds  a new element to that array note that with httpi   by default values are encoded as strings to send  a literal integer or boolean value for example   we need to add a colon before the equal sign now  the value for that field is encoded as an integer   note what happens though if we make a post  request but don't include any data in the request   we get null to make the api more robust  and handle this possibility we need to   check that this value isn't null the json decode  function will return null if the json is invalid   or if there isn't any data we could check for  this with an if statement but a simpler way to   do it is to just cast this value to an array  if the value is already an array then nothing   happens if it's null then it will be converted to  an empty array if we make an empty post request   again now we get an empty array instead of null  now we have an array of data from the request   we can insert a new record into the database  to do this first in the product gateway class   let's add a new public method called create with  an array argument for the data inside this method   we'll add a string containing the sql to insert  a new record into the product table specifying   the name size and is available columns with  placeholders for the values then we'll use   a prepared statement for this so we'll call  the prepare method on the connection property   passing in the sql then we can bind the values to  the placeholders first the name which is a string then the size which will default to zero if it  wasn't supplied binding its value as an integer   finally the is available column which will  default to false and we'll cast this to boolean   just in case the value was supplied as another  type and we'll bind this as a boolean value   then we'll execute the statement and we'll  return the id of the record that was inserted   which we can get by calling the last  insert id method this method returns   a string so we'll add the string return  type declaration to the function definition then back in the controller instead of  outputting the data from the request   we can call the create method on the gateway  property passing in the array of data this   returns the id of the newly created record so  we'll assign that to a variable for the response   to this request we'll output some json with  a message saying that the product was created   and its id finally we'll add a break  statement to end the case block let's give that a try if we make a post request to  the product's endpoint passing in just a product   name we get a reply saying the product with an  id of 3 was created if we include the size we get   a product with an id of 4 and if we include  the is available field we get product id 5. in the database we can see the three new  records that we just inserted using the api   where we didn't specify values for the size and  is available columns the values for these were   defaulted note that in the output when we  make this request we have a status code of   200. this is the default status code if we don't  change it however when inserting a new record the   status code to use is 2 0 201 so let's set  that using the htt response code function now if we make a successful request to create a  new product we get the 201 created status code   watch what happens though if we make a request  with no data at all we get a couple of errors   the first tells us we have an undefined array  element the second is a database error that   comes from trying to insert a record with  no value for the name column however note   that the first error isn't encoded as json even  though we configured a generic exception handler   what's actually happening here is that a  php error has occurred which isn't caught   by the custom exception handler to catch the  error we have to set a custom error handler   using the set error handler function the callback  function we passed to this has this signature with   the error number message and so on so first in the  error handler class let's add a new public method   called handle error which fits this signature  although we only need the first four arguments inside the method instead of repeating  what we have in the exception handler   we can just throw a new exception  of the error exception class   this exception class was designed to  represent errors as exceptions the   constructor takes various different arguments  for the various attributes of the exception   so in the body of the handle error method we'll  throw a new error exception passing in the error   message zero for the exception code as we don't  have this the error level file where it occurred   and the line number in that file then in the  index script before we set the exception handler   we'll call the set error handler function  passing in the callback function we just added now if we make a post request without  any data we get the error encoded as json   we also no longer get the other exception as  execution halted after this exception occurred   which is as expected now we can look at  the reason why this error is occurring   we're getting an undefined array key error  on line 38 of the product gateway class on that line we're trying to use  an array element that doesn't exist   this is because we didn't send any data with  the request so before we try and save the   data to the database we need to validate it  we'll do this in the product controller class   let's add a new private method called get  validation errors this will have one argument   which is the array of data to validate  and will return an array of error messages   in the body of the method first we'll initialize  an empty array then we'll validate the name field   we'll keep this simple and just  check to see if it's empty or not   if it is we'll append an error  message to the errors array next we'll validate the size this is optional so  first we'll check if a value has been supplied   if so we'll use the filter var function with  the integer validation filter to check that   it's an integer if it's not this function will  return false so we'll explicitly check for that   the reason we check it's specifically boolean  false and not just negate the result of the   function call is that it could be zero which  is still a valid value but this evaluates to   boolean false if it's not valid we'll append  a suitable message to the errors array finally   at the end of the method we'll return the array  then back in the process collection request method   before we call the create method we'll call the  method we just added passing in the data from   the request and assigning the return value  to a variable if this variable isn't empty   then we'll output the errors as json in  the response body and break out of this   case block we'll also return an http status  code of 422 which is unprocessable entity let's give that a try now if we make a  request with no data we get the error   in the body of the response and a 422 status code   if we add an invalid size to the data  then we get the error message for that too   if we send valid data then the record is  created and we get the 201 status code   so now we can make a get request to this  collection endpoint to get a list of all products   and a post request to create a new one there  are other http request methods we can use for   example delete if we make a delete request to this  endpoint we get an empty response body with a 200   status code we should respond differently if this  method isn't allowed for this endpoint so let's   add a default block to the switch statement and  in here we'll respond with a 405 status code which   is method not allowed when responding with this  status code the standard requires you to include   the methods that are allowed in an allow header  so we'll add the call to the header function   with an allow header specifying that get and post  are the only methods allowed on this endpoint now if we try an invalid method we get the  method not allowed response and the allow   header is included if we try a valid method such  as get we get a successful response as expected   that completes the processing for the collection  request next let's process requests where the   product id is included in the endpoint the  first thing we need to do for these requests   is to check that a record already exists in  the database for the given id in the product   gateway class let's add a new public method  called get with a string argument for the id   as above we'll use a prepared statement for this  so first we'll write the sql to select the record   from the product table using a placeholder  for the id then we'll prepare the statement   passing in the sql and bind the value  of the id argument as an integer next we'll execute it and fetch the record as  an associative array finally in this method   we'll return the data the fetch method will  return an array if the record was found   are false otherwise so we'll add the appropriate  return type declaration to the function definition then in the controller in the process resource  request method we can call the get method on the   gateway property passing in the id then for now  let's just output the return value encoded as json let's give that a try if we make a get request  to the slash products slash one endpoint   then we get the product record in  the response body in json format   note however that the is available column  is an integer instead of a boolean value in the get all method in the product gateway  class we manually converted this column to a   boolean value when we retrieve the data  let's do the same in the get method so   once we fetch the data if it isn't  false we'll cast its value to boolean now if we make the same request again  the is available column is boolean   this request worked because  a record with an id of 1   exists in the database if we make a request  with an id that doesn't exist we just get false back in the product controller  class let's check for this   and if the product wasn't found  we'll respond with a 404 status code   a suitable message in the response body and we'll  exit from the method with a return statement now if we make the same request  we get the 404 not found response next we can process the request  based on the http request method used   as before we'll do this with a switch  statement on the method argument   if the method is get we'll do what we're already  doing which is to output the product data as json   then we'll break from the case block we  don't need to specify the status code as 200   as this is the default but you can if you want to  so if we make a get request to the endpoint for   a product that exists we get a 200 response  with the product data in the response body next we'll process a request  to update an existing record   we'll do this with a patch request so let's add  a patch case to the switch statement in here   most of this will be similar to what we're doing  down here when we insert a new record so let's   copy this whole case block and paste it in the  patch block the first part will be the same where   we get the data from the request and validate  it however instead of calling the create method   on the gateway object we need a different method  to update an existing record so let's add a new   public method to the product gateway class called  update we'll pass in the current product details   and the new details from the request data and  we'll return an integer which will be the number   of rows in the database that were updated in here  we'll add the sql to update a record identifying   the record with a where clause for the id we'll  prepare the statement as before passing in the sql then we can bind values to the placeholders  starting with the name if a new value has been   passed in the request we'll use that otherwise  we'll just use the existing value if the value   hasn't changed the database will detect this and  not update anything we'll do the same for the size   and is available columns then we'll bind the id  value which we get from the current product values   and we'll execute the statement we'll  call the row count method to set the   return value from this method then back in the  controller instead of calling the create method   we call the update method we just added passing  in the current product as the first argument   this returns the number of rows affected so we'll  change the name of the variable we assign this to   accordingly for the response we'll use the  200 status code so we can remove this call   to the http response code function as this  is the default for the body of the response   we'll include the id in the message and  return the number of rows that were affected let's give that a try we'll send a patch request  to the product with an id of one updating the name   we get a successful response telling us one row  was affected if we make a get request to the same   endpoint then we see the name value has indeed  been updated if we make the same patch request   again then no rows were affected as no values  were changed we can update all of the columns and the validation works as before however if  we just want to update the size for example   then we get a validation error saying that  the name is required the name is only required   when we're creating a new record so we need  to do some conditional validation on this   in the get validation errors method we'll only  add this validation message for the name field   if we're creating a new record so let's add a new  boolean argument to the method to identify if it's   a new record or not which will default to true  then before we check if the name value is empty   we'll check if this variable is true then  where we're processing the patch request   we can pass in false as the second argument  when we call the get validation errors method   let's give that a try if we make  the same patch request as before   just passing in a value for the size now we  get a response saying the record has been   updated and if we view that record we can  indeed see that the value has been changed the final operation we want to carry out on  a resource is to delete it so let's start   in the process resource request method by  adding a delete case to the switch statement   then in the product gateway class we'll add a  new public method called delete passing in the   id as a string and returning an integer in the  body of the method we'll add the sql to delete   a record from the product table identified  by the id we'll prepare the statement bind   the id value to the placeholder and execute it  finally we'll return the number of rows affected   back in the controller we'll call this method  on the gateway property passing in the id and   assigning the return value to a variable for  the body of the response we'll output a message   the number of rows that were affected and  we'll end the case block with a break statement let's give that a try if we send a  delete request to the product with   an id of one then we get a result saying the  product was deleted and one row was affected   if we then try to read that resource by sending  a get request to the same endpoint then we   get a 404 response saying the product wasn't  found confirming that the record was deleted   one final thing we need to do in here is  respond to any request that doesn't use one   of the above methods as we did below so let's add  a default block set the http status code to 405   and send an allow header with the  allowed methods of get patch and delete so now if we send a post request for example  to the endpoint for the product with an id   of 2 we get the 405 method not allowed response  including the allow header so now you know how   to create a restful api using plain object  oriented php and mysql we can make requests   to list all resources to create one to show  an individual resource update it and delete it there's a link to all the code shown  in this video in the description   along with links to sites shown  and relevant documentation   please don't forget to like comment and  subscribe and as always thank you for watching
Info
Channel: Dave Hollingworth
Views: 76,197
Rating: undefined out of 5
Keywords:
Id: X51KOJKrofU
Channel Id: undefined
Length: 43min 53sec (2633 seconds)
Published: Fri May 06 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.