Tutorial outline

  • What is PDO?
  • The Tools we shall use in this tutorial
  • What we build in this tutorial
  • Installing MariaDB, PHP and Apache using XAMPP
  • Creating out database
  • Connecting to database using PDO
  • Performing Database operations - CRUD operations and database transactions
  • Wiring the whole application and setting up Postman
  • Changing dialect to PostgreSQL
  • Connecting to PostgreSQL database using PDO
  • Conclusion

What is PDO?

If you are building data driven applications, then you have to use a database. PHP provides different ways in which you can interact with MySQL database such as mysql and mysqli.

MySQL database was acquired by Oracle. MariaDB, which is a fork of MySQL, is available for community development and commercial use free of charge.

However, PHP applications can interface with other databases such as PostgreSQL and Oracle. This makes it cumbersome to connect to different databases using different implementations from one PHP application. To unify the process of connecting to different databases, PHP introduced PDO (PHP Data Objects). Thus if you are going to use more that one database or change your database in the middle of your project, using PDO to interface your application with the database means that you will not re-write your queries.

Then the advantage of using PDO lies in the ability to use an identical interface for 12 different databases and once you are familiar with this PDO API, it is portable from database to database.
However, if you're sure that the only database you will be using is MySQL or MariaDB, then you can choose between PDO and mysqli.

With PDO, you use a driver to connect to each particular database and the driver must be available in your PHP installation. You do not need to install/enable a driver for MySQL database because PHP ships with it, especially when you use PHP and MySQL bundled in XAMPP software. However, if you are using a separately installed databases such as PostgreSQL, then you will need some additional steps to set it up- we will explain the steps in this post.

PDO has drivers that supports 12 different databases. The table below shows some of the driver names for the supported databases. See more documentation here.

Database name Driver name
MySQL PDO_MYSQL
PostgreSQL PDO_PGSQL
SQLite 3 and SQLite 2 PDO_SQLITE
Microsoft SQL Server / SQL Azure PDO_SQLSRV
IBM Informix Dynamic Server PDO_INFORMIX
IBM DB2 PDO_IBM
Firebird PDO_FIREBIRD
4D PDO_4D
Cubrid PDO_CUBRID

The Tools we shall use in this tutorial

  • MySQL/MariaDB database and Apache web server both bundled in XAMPP software
  • PostgreSQL database  
  • pg Admin 4 - a GUI tool for accessing PostgreSQL
  • Postman - to help us send requests and receives responses
  • Windows OS
  • Code editor - I am using Visual Studio Code (You can use your favorite).

What we build in this tutorial  

Think of a factious application, which stores company details, and employees belonging to that company.

The entities we have are company and employee with a relationship/association of ONE TO MANY i.e. 1 company has many employees (see ERD below)

Entity Relationship Diagram 

The operations we aim to perform include:

  • Inserting records
  • Reading records
  • Inserting Multiple records at once in one table  
  • Insert records in both tables, both of which must either succeed or fail - Transaction

These operations are executed in both MySQL/MariaDB and PostgreSQL to show that, when PDO is used, your code will remain the same irregardless of the database used.

Installing MariaDB, PHP and Apache using XAMPP

You can install MariaDB, PHP and Apache by installing XAMMP. XAMPP for Windows OS can be downloaded from here and should be easy to install.

After installation, use XAMPP control panel to ensure that Apache and MySQL/MariaDB is running.

XAMPP control panel

When MySQL is running, there is a default database user called root with no password.

If you are using your machine, MySQL can be accessed via your browser using http://localhost/phpmyadmin, where you can create and administer your databases as shown below.

PHPMyAdmin database ministration. All your databases are on the left side and you can run queries using the SQL tab.

 Creating out database

We will create a database with name decode (You can name your database the way you like) by running this query.

CREATE DATABASE decode;
Creating decode database 

You will see your created database on the left side of phpMyAdmin window. Click on it and run the following queries to create company and employee tables.

CREATE TABLE company (
	id int PRIMARY KEY AUTO_INCREMENT,
	name varchar (32) NOT NULL,
	website varchar (255)
)
creating company table
CREATE TABLE employee (
    id int PRIMARY KEY AUTO_INCREMENT,
    full_name varchar (32) NOT NULL,
    age int,
    companyId int,
    FOREIGN KEY (companyId) REFERENCES company (id)
);
Creating employee table

Connecting to database using PDO

Create your project inside htdocs directory of XAMPP so that your PHP scripts will be interpreted, your http requests processed and responses returned.

The project structure is as as shown below

htdocs
  decodeapp
     -util.php
     -db.php
     -index.php
     -operations.php
     
    
decodeapp is a folder/directory containing 3 php files

The util.php is a PHP class, which contains static database variables and its contents are shown below.

<?php
	class Util {			
        //About DB
        static $DB_NAME = "decode";
        static $DB_USER = "root";
        static $DB_USER_PASS = "";
        static $SERVER_NAME = "localhost";
	}
?>
util.php for connecting to MariaDB/MySQL databse 

By making the variables static, we will access them without creating an object of the class. The contents in util.php can be changed to match your development environment. The contents of db.php are as shown below. It is a class called DBConnector with a function connectToDB, which returns the database connection handle, $pdo.

<?php
    include_once './util.php';	
    class DBConnector {
        protected $pdo;
        function __construct(){
          $dsn="mysql:host=".Util::$SERVER_NAME.";dbname=" .Util::$DB_NAME ."";
          $options = [ 
              PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
              PDO::ATTR_EMULATE_PREPARES => false,
              PDO::ATTR_DEFAULT_FETCH_MODE =>PDO::FETCH_ASSOC
          ];
          try{
             $this->pdo=new PDO($dsn,Util::$DB_USER,Util::$DB_USER_PASS, $options);				
           }catch (PDOException $e){
                 	echo $e->getMessage();
           }			
        }
        public function connectToDB(){
                return $this->pdo;
        } 			
        public function closeDB(){
                $this->pdo = null;
        }
    }
?>
db.php - Complete code for DB connection 

From the code, the database connection happens in the class constructor. The data source name (dsn) contains the dialect i.e. target DBMS name, host and the database name. Notice that we have included the util.php file in db.php. Also notice how we get the host  and database name values from the Util class using :: (double colon). The PDO database handle is then created by instantiating the PDO class:

$this->pdo=new PDO($dsn,Util::$DB_USER,Util::$DB_USER_PASS, $options);

Database connection options are prepared in form of associative array. The connectToDB method returns the PDO connection handle:

public function connectToDB(){
	return $this->pdo;
} 	

Should any error occur when connecting to the database, the catch block will print the error message.

What do the options really mean?

There are many options you can add to the options associative array (see them here). However, I will explain the ones we have used in our connection.

PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION: Any errors, which occur when we perform a database transaction will be reported in form of exceptions. You can use PDOException class to catch errors specific to PDO or use Exception class to catch any error.

PDO::ATTR_EMULATE_PREPARES => false: PDO interface uses prepared statements under the hood. If you do not know what prepared statements are and why they useful, read here.  If a given database driver does not support prepared statements, then PDO will emulate prepared statements. In our case, we've turned off prepared statements emulation since we are certain that PDO_MYSQL driver supports them out of the box. Note that using emulated prepared statements is equally acceptable.    

PDO::ATTR_DEFAULT_FETCH_MODE =>PDO::FETCH_ASSOC: PDO gives you the luxury of deciding how you want your data to presented when you read it from a database table. In our case, we want data presented as an a associative array i.e. an array indexed by columns name. If you do not specify, then the default is PDO::FETCH_BOTH, which returns an indexed array (indexed by number) and associative array (indexed by table column name).

Performing Database operations - CRUD operations and database transactions

We write a class called Operations inside the operations.php file. The class has the methods, which will perform these operations.

We will not create properties for this class because all operations for both company and employee are performed here.

The skeleton of the class looks as shown below. Notice that each method receives all the data it needs plus a pdo connection handle.

<?php
    class Operations {
        function __construct(){}
        public function saveCompany ($pdo, $name, $website){

        }
        public function saveEmployee ($pdo, $full_name, $age, $companyId){

        }
        public function readCompany ($pdo){

        }
        public function searchCompany ($pdo, $companyId){

        }
        public function saveManyCompany ($pdo, $companyList){
			//For inserting multiple records into the company table
        }

        public function saveCompanyAndEmployee ($pdo, $company, $employee){
            //For inserting a record both in company and employee in which 				//case both are expected to be successiful or fail - all or nothing
            //i.e a transaction. 
        }

    }

?>
Skeleton Operations class

 Inserting a record  company table

The saveCompany function would look like this.

public function saveCompany ($pdo, $name, $website){
    try {
        $stmt=$pdo->prepare("INSERT INTO company (name,website)VALUES (?,?)");
        $stmt->execute([$name,$website]);
        $stmt = null;
        return "Company has been saved";
    } catch (PDOException $e) {
    	return $e->getMessage();
    }
}
The saveCompany() function using placeholders 

A SQL query is prepared using the prepare() method in $pdo object and then executed using the execute() method in $stmt. The execute() accepts an array with same number of values/elements as the placeholders.

We set the error mode to be exceptions, thus the code above will return a message explaining the error in case it happens.

There are two ways in which you can prepare queries using prepared statements. The one used above uses placeholders (?) but one can also use named variable. If we rewrite the function above to use named variable, it would change as shown below:

public function saveCompany ($pdo, $name, $website){
    try {
        $stmt=$pdo->prepare("INSERT INTO company (name,website)VALUES (:name,:website)");
        $stmt->execute([':name'=>$name,':website'=>$website]);
        $stmt = null;
        return "Company has been saved";
    } catch (PDOException $e) {
    	return $e->getMessage();
    }       

}
The saveCompany() function using named variables

  Insert a record in employee table

Because we must show the company to which an employee belongs, we need to have a valid id in company table. Thus, the function saveEmployee() would look like as shown below:  

public function saveEmployee ($pdo, $full_name, $age, $companyId){
    try {
        $stmt = $pdo->prepare ("INSERT INTO employee (full_name,age,companyId) VALUES (?,?,?)");
        $stmt->execute([$full_name,$age,$companyId]);
        $stmt = null;
        return "Employee has been saved";
    } catch (PDOException $e) {
    	return $e->getMessage();
    }
}
The saveEmployee() function 

If you try to insert an employee record with a companyId that is not already in the company table, you will see this error:

SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails
(`decode`.`employee`, CONSTRAINT `employee_ibfk_1` FOREIGN KEY (`companyId`) REFERENCES `company` (`id`))
Referential integrity exception/error

Reading all records from the company tables

Reading all records from the company table will yield an associative array whose keys are names of table columns. The readCompany() function would be as shown below.

public function readCompany ($pdo){
    try {
        $stmt=$pdo->prepare("SELECT * FROM company");
        $stmt->execute();
        $result = $stmt->fetchAll();
        $stmt=null;
        return json_encode($result);
    } catch (PDOException $e) {
    	return $e->getMessage();
    }
}
Fetching all records using fetchAll() function 

Notice the use fetchAll() method on $stmt to read all the records returned from the table. We then use json_encode() to convert the associative array to JSON format. Thus, the output from the function would be similar to as below. The output is described as 'an array of objects'.  

[
    {
        "id": 2,
        "name": "fourth profile",
        "website": "fourthprofile.com"
    },
    {
        "id": 3,
        "name": "Skillsday",
        "website": "Skillsday.co"
    },
    {
        "id": 4,
        "name": "Bobolink Ltd",
        "website": "Bobolink.co.ke"
    }
]
A sample JSON output - An array of objects 

Searching for a company using an id (primary key)

For one to search the company table, a unique column value would be needed such as the company id. Thus the function searchCompany() would look like this.

public function searchCompany ($pdo, $companyId){
    try {
        $stmt=$pdo->prepare("SELECT * FROM company WHERE id=?");
        $stmt->execute([$companyId]);
        $result = $stmt->fetch();
        return json_encode($result);
    } catch (PDOException $e) {
    	return $e->getMessage(); 
    }
}
Searching a record using WHERE clause with PDO

Notice that because we were expecting one record, we have used fetch() on $stmt instead of fetchAll().

In this case, if companyId vale is 3, the output from the function is a JSON object as shown below.

{
    "id": 3,
    "name": "Skillsday",
    "website": "Skillsday.co"
}

Inserting multiple records into the the company table

The beauty of PDO is that you prepare a query/statement once and execute it many times. Suppose you have multiple company records in JSON format as shown below, and you want to insert all the records into the companies table.

[
        {
            "name": "Safaricom",
            "website": "Safaricom.co.ke"
        },
        {
            "name": "Centum",
            "website": "Centum.co.ke"
        },
        {
            "name": "Microsoft",
            "website": "microsoft.com"
        }
]

The data above is described as an array of objects. Thus, you need to loop through the array and retrieve name and website as properties of the objects in the array. The saveManyCompany() function would then look like as shown below.

public function saveManyCompany ($pdo, $companyList){
    try {
        $stmt = $pdo->prepare ("INSERT INTO company (name, website) VALUES (?,?)");

        foreach ($companyList as $company){
        	$stmt->execute([$company->name,$company->website]);
        }                           
        return "Companies has been saved";
    } catch (PDOException $e) {
    	return $e->getMessage();
    }
}
Inserting multiple records with PDO - 'Prepare once, execute many principle'

Running transactions with PDO object

One of the properties of database transactions is Atomicity  It means that a transaction should be treated as a unit, which means that all the operations in a transactions are all executed or none i.e. 'all or nothing'.

For instance if we were to insert two records, one in company table and the other in employee table, if the two queries belong to a transaction, then if one query fails, for some reason, all must fail.

By default, a query effect is auto-committed in a table but this behavior can be changed. If you turn off auto-commit behavior, then you have the opportunity to commit or rollback any query effects manually. Lets see how we can run transactions using PDO in the saveCompanyAndEmployee() function.

public function saveCompanyAndEmployee ($pdo, $company, $employee){
    /*$company and $employee are arrays, which contain data belonging to 
    company table and employee table respectively*/
    $pdo->setAttribute(PDO::ATTR_AUTOCOMMIT,FALSE);
    try {
        //begin a transaction 
        $pdo->beginTransaction();
        //prepare the two queries 
        $stmtCompany = $pdo->prepare ("INSERT INTO company (name, website) VALUES (?,?)");
        $stmtEmployee = $pdo->prepare ("INSERT INTO employee (full_name,age,companyId) VALUES (?,?,?)");
        //execute both queries
        $stmtCompany->execute([$company[0],$company[1]]);
        $stmtEmployee->execute([$employee[0],$employee[1],$employee[2]]);                
        //commit here
        $pdo->commit();
        return 'Transaction was successiful';
    } catch (Exception $e) {
        //rollback to cancel if anything goes wrong 
        $pdo->rollBack();
        return $e->getMessage();
    }
} 
Running PDO transactions 

From the code above, notice that $pdo -> setAttribute (PDO :: ATTR_AUTOCOMMIT, FALSE) removes auto-commit. On the other hand $pdo->beginTransaction() starts the transaction to which the 2 queries belong. You will have to commit changes manually ($pdo->commit()) if all goes well or  roll back any changes, which may have occurred ($pdo->rollBack()).

The complete Operations Class

<?php
class Operations {
	//class constructor
    function __construct(){}
    public function saveCompany ($pdo, $name, $website){
        try {
            $stmt=$pdo->prepare("INSERT INTO company (name,website)VALUES (?,?)");
            $stmt->execute([$name,$website]);
            $stmt = null;
            return "Company has been saved";
        } catch (PDOException $e) {
            return $e->getMessage();
        }
    }
    public function saveEmployee ($pdo, $full_name, $age, $companyId){
        try {
            $stmt = $pdo->prepare ("INSERT INTO employee (full_name,age,companyId) VALUES (?,?,?)");
            $stmt->execute([$full_name,$age,$companyId]);
            $stmt = null;
            return "Employee has been saved";
        } catch (PDOException $e) {
            return $e->getMessage();
        }
    }
    public function readCompany ($pdo){
        try {
            $stmt=$pdo->prepare("SELECT * FROM company");
            $stmt->execute();
            $result = $stmt->fetchAll();
            $stmt=null;
            return json_encode($result);
        } catch (PDOException $e) {
            return $e->getMessage();
        }
    }
    public function searchCompany ($pdo, $companyId){
        try {
            $stmt=$pdo->prepare("SELECT * FROM company WHERE id=?");
            $stmt->execute([$companyId]);
            $result = $stmt->fetch();
            return json_encode($result);
        } catch (PDOException $e) {
            return $e->getMessage(); 
        }
    }
    public function saveManyCompany ($pdo, $companyList){
        try {
            $stmt = $pdo->prepare ("INSERT INTO company (name, website) VALUES (?,?)");

            foreach ($companyList as $company){
                $stmt->execute([$company->name,$company->website]);
            }                           
            return "Companies has been saved";
        } catch (PDOException $e) {
            return $e->getMessage();
        }
    }

    public function saveCompanyAndEmployee ($pdo, $company, $employee){
        /*$company and $employee are arrays, which contain data belonging to 
        company table and employee table respectively*/
        $pdo->setAttribute(PDO::ATTR_AUTOCOMMIT,FALSE);
        try {
            //begin a transaction 
            $pdo->beginTransaction();
            //prepare the two queries 
            $stmtCompany = $pdo->prepare ("INSERT INTO company (name, website) VALUES (?,?)");
            $stmtEmployee = $pdo->prepare ("INSERT INTO employee (full_name,age,companyId) VALUES (?,?,?)");
            //execute both queries
            $stmtCompany->execute([$company[0],$company[1]]);
            $stmtEmployee->execute([$employee[0],$employee[1],$employee[2]]);                
            //commit here
            $pdo->commit();
            return 'Transaction was successiful';
        } catch (Exception $e) {
            //rollback to cancel if anything goes wrong 
            $pdo->rollBack();
            return $e->getMessage();
        }
    } 

}

?>
The full code for Operations class

Wiring the whole application

Now that we know all the contents of util.php, db.php and util.php, it is time to understand how index.php will help us wire the application. index.php is the entry point to our application. It receives HTTP requests and as well as return HTTP responses.

To insert a record in the company table using Postman, follow these steps:

  1. Add the following code in the index.php file.
<?php
    include_once './db.php';
    include_once './operations.php';

    //Database handle $pdo
    $con = new DBConnector();
    $pdo = $con->connectToDB();

    //creating object of Operations class. 
    $operations = new Operations();


    //Inserting one record into the company table. 
    $name = $_POST['name'];
    $website = $_POST['website'];
    echo $operations->saveCompany($pdo,$name,$website);
?>
Contents of index.php to insert a record in company table 

In index.php, we expect that data will be sent using POST method and there are two values sent, name and website. The only thing happening in this file is to connect to the database, then use Operations class to perform all the transactions. Do not forget to include db.php and operations.php files inside index.php.

2. Setup postman:

Postman is a software used by API designers but it can also be used to create and send HTTP requests and receive responses.

You can download Postman for here and it should be easier to install it in Windows OS.  The postman's free plan already has many features you can use hence you do not need to pay anything. When you open it, it will appear as shown below.

The Postman interface 

You click on 'New' to create a new request, then the interface you see after creating the request will allow you to construct a new HTTP request by specifying all the information needed i.e URL, Selecting method, adding data in the body or as URL parameters and any custom headers.  

3. Create a postman request

URL: http://localhost/decodedapp/index.php

Method: POST

Body: Key pair values for company name and website

headers: No modification needed.

Click send

The figure below shows the structure of the request. For the data format, you can select form-data or x-www-form-urlencoded.

Inserting a record in the company table using Postman

To read all the records in the company table using Postman, follow these steps.

  1. Modify index.php page as follows
<?php
    include_once './db.php';
    include_once './operations.php';


    //Database handle $pdo
    $con = new DBConnector();
    $pdo = $con->connectToDB();

    //creating object of Operations class. 
    $operations = new Operations();
    echo $operations->readCompany($pdo);
?> 

2. Create a request on postman as shown below and click Send.

Read all the records in the company table using Postman

Creating postman requests for inserting a record into employee table, searching a record in the company table and running a transaction should now be easier.

In the case of inserting multiple records in the company table, you can send JSON formatted data (having multiple records) from postman by following these steps.

  1. Create a postman request, click on body tab, then click on raw on body data format options and in the last option, select JSON.
  2. Construct your request as shown below
Postman request body with JSON data

3. Before you click Send, add this code in your index.php file.

<?php
    include_once './db.php';
    include_once './operations.php';


    //Database handle $pdo
    $con = new DBConnector();
    $pdo = $con->connectToDB();

    //creating object of Operations class. 
    $operations = new Operations();
    
    //Read the body of your request and store it in $jsondata variable.
    $jsondata = file_get_contents('php://input');
    //Call the saveManyCompany() but you have to use json_decode() to convert your raw body data into a JSON data variable. 
    echo $operations->saveManyCompany($pdo, json_decode($jsondata));
?>

If everything goes well, then you should see "Companies has been saved" in your postman as response.

Changing dialect to PostgreSQL

All along, we have been using MySQL/MariaDB but we had mentioned that the benefit of using PDO is the ability to hop from one database to another without touching your queries. So now we need to setup a new database connection using PostgreSQL instead.

Installing PostgreSQL and pgAdmin 4

Download PostgreSQL from here and it should easy to install in Windows OS. When installing PostgreSQL, it will ask you to set a password for a default user postgres. This is the password you will use to make a connection.

Download pgAdmin 4 from here and it should be easier to to install it in Windows OS. pgAdmin 4 is a management tool for PostgreSQL just like phpMyAdmin for MySQL.

Open pgAdmin 4 and it should open your browser ready to access your PostgreSQL. You must login using the password you had set when installing PostgreSQL. See diagram below. If you have difficulties setting up pgAdmin 4 seek help here.

Creating the company and employee table  

Use the pgAdmin 4 query tool to run your queries shown below

CREATE TABLE company (
	id serial PRIMARY KEY,
	name varchar (32) NOT NULL,
	website varchar (255)
)
SQL query to create company table in PostgreSQL
CREATE TABLE employee (
    id serial PRIMARY KEY,
    full_name varchar (32) NOT NULL,
    age int,
    companyId int,
    FOREIGN KEY (companyId) REFERENCES company (id)
);
SQL query to create employee table in PostgreSQL

Notice that these queries are exactly the same as the ones we used to create the tables in MySQL/MariaDB, except in the id column, where we have used serial. Serial in PostgreSQL adds the auto-increment on that column, makes the column unique and adds NOT NULL constraint.

Connecting to PostgreSQL database using PDO

Change your util.php file as shown below

<?php
    class Util{

        //About DB
        static $DB_NAME = "decode";
        static $DB_USER = "postgres";
        //Use password you set to postgres user
        static $DB_USER_PASS = "your password here";
        static $SERVER_NAME = "localhost";
    }
?>

Notice that we changed the $DB_USER value to postgres and of course $DB_USER_PASS value need to have the right value.

Next, change the contents of the db.php as shown below:

<?php
    include_once './util.php';	
    class DBConnector {
        protected $pdo;
        function __construct(){
          $dsn="pgsql:host=".Util::$SERVER_NAME.";dbname=" .Util::$DB_NAME ."";
          $options = [ 
              PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
              PDO::ATTR_EMULATE_PREPARES => false,
              PDO::ATTR_DEFAULT_FETCH_MODE =>PDO::FETCH_ASSOC
          ];
          try{
             $this->pdo=new PDO($dsn,Util::$DB_USER,Util::$DB_USER_PASS, $options);				
           }catch (PDOException $e){
                 	echo $e->getMessage();
           }			
        }
        public function connectToDB(){
                return $this->pdo;
        } 			
        public function closeDB(){
                $this->pdo = null;
        }
    }
?>

Notice that the only thing that we have changed is changing the database dialect from mysql to pgsql in $dsn.

At this point, if you try to connect to PostgreSQL database, you will this message 'could not find driver'. This is because we have not activated the PDO_PGSQL driver. This means that the methods such as prepare() and execute() will not be available.

To enable PDO_PGSQL driver you will need to comment it in php.ini file in XAMPP's php directory.

//Location of php.ini file
xampp
  php
    -php.ini
    
    
//Inside the php.ini file, 
;extension=pdo_pgsql //Remove the ; at the beginning of this line

Restart your Apache server for the changes to take effect.

Now if you repeat everything we did with MySQL/MariaDB, it will work without changing anything else in the code.  

Lastly, there are many aspects of PDO,which we did not touch in this tutorial. For instance getting count of rows affected by the last DELETE, INSERT, or UPDATE statement, getting the last inserted index and many more. You can find all these in PDO's official documentation.

Conclusion

This was a long tutorial but I hope you had fun exploring PDO. Thank you for stopping.

You've successfully subscribed to Decoded For Devs
Welcome back! You've successfully signed in.
Great! You've successfully signed up.
Your link has expired
Success! Your account is fully activated, you now have access to all content.