A Moderately Short PHP Tutorial

Databases

PHP applications often connect to databases. This is even harder to avoid in PHP than it would be in other languages you can run on a web server like JS, Java, or C#, because PHP has a shared-nothing architecture. That means that the code dealing with each request runs in isolation, and does not share any objects or variables with other requests. At the end of the request-response cycle all stack frames are unwound and all objects become garbage.

So if your program needs to persist anything from one page to the next, a database is a natural choice.

For simplicity, we will use SQLite. This is not a database server that you connect to, but a full featured SQL database engine supplied as a C library, and available as an add-on module for PHP, and the world's most widely deployed DB engine. An SQLite database is just a file.

The details of SQL, the programming language for defining and querying databases, are beyond the scope of this tutorial. We will see some SQL code, but as far as PHP is concerned they are just strings to send to the database engine.

Install the SQLite PHP Module

We will use our SQLite database via PHP's PHP Data Objects (PDO) extension, which provides a consistent interface for accessing many different DB types.

First check if the necessary PHP modules are installed. Run php -i | grep sqlite. If you have the module set up, you should see a line like "PDO drivers => sqlite". There may be other drivers listed alongside sqlite.

If you don't have the PDO SQLite module, how to install it will depend on your OS and how you installed PHP.

Linux

If you installed PHP through your package manager, there should be an SQLite module available in the same place. Make sure you get the module that matches your PHP version number. For instance, on Debian or Ubuntu search your repository with:

apt-cache search php | grep sqlite

Once you've found the package name, e.g. php7.4-sqlite3 install it with a command such as

sudo apt install php7.4-sqlite3.

Mac

If you installed PHP via homebrew, I think it will automatically have come with SQLite built in and enabled.

Windows

If you installed PHP as part of XAMPP this should have SQLite enabled by default.

Creating a database

First, let's write a PHP script src/create-database.php to create a new database with one table:

<?php declare(strict_types=1);

namespace AModeratelyShortPhpTutorial;

use PDO;

$filename =__DIR__ . "/database.sqlite";

file_put_contents($filename, '');

$connection = new PDO("sqlite:" . $filename);

$connection->query('
    CREATE TABLE planets (
        id INTEGER PRIMARY KEY,
        name text,
        population_size INTEGER
    );
'
);

Run the script with php create-database.php. It should create a new binary file database.sqlite.

file_put_contents writes a string to a file, overwriting the file if it already exists. We're using here to create a completely empty file.

The PDO class is supplied by the PDO extension. A PDO object represents a connection to a database, and allows us to send it queries and get back results. When we call the PDO constructor we pass it a string holding details of the database we want to connect to, known as a Data Source Name or DSN. For SQLite the DSN consists of 'sqlite:' followed by an absolute file path.

Querying the database

Let's write a very small program to put the planets in the database, and let us view info about any planet. First we will need a class that can insert all the planets. Make a file src/PlanetStore.php

<?php declare(strict_types=1);

namespace AModeratelyShortPhpTutorial;

use PDO;

final class PlanetStore
{
    private PDO $connection;

    public function __construct(PDO $connection) {
        $this->connection = $connection;
    }

    public function storePlanets(): void
    {
        $statement = $this->connection->prepare('INSERT INTO planets (name, population_size) values (?, ?);');
        foreach ($this->generatePlanets() as $planet) {
            $statement->execute([$planet->getName(), $planet->getPopulationSize()]);        
        }
    }

    /** @return Planet[] */
    private function generatePlanets(): array
    {
        return [
            new Planet('Mercury', 0),
            new Planet('Venus', 0),
            new Planet('Earth', 7.7 * 10**9),
            new Planet('Mars', 0),
            new Planet('Jupiter', 0),
            new Planet('Saturn', 0),
            new Planet('Uranus', 0),
            new Planet('Neptune', 0),
        ];
    } 
}

Once again if you don't have PHP 7.4 you will need to remove the PDO property type on the $connection property.

Now write a script storePlanets.php to use this class:

<?php declare(strict_types=1);

namespace AModeratelyShortPhpTutorial;

require_once __DIR__ . '/vendor/autoload.php';

use PDO;

$planetStore = new PlanetStore(
    new PDO("sqlite:". __DIR__ . '/database.sqlite')
);

$planetStore->storePlanets();

Run php storePlanets.php. This should add the eight planets to the database. It shouldn't produce any visible output.

The most important thing to notice about our code so far is that we didn't concatenate any strings to create queries to send to the database. Instead we prepared a query, using question marks as placeholders for our data, and then supplied the values of those parameters separately each time we executed the query.

This practice makes our database use much less error prone and more secure. The database engine knows what exactly what's code that it should interpret and what's data that it should simply store.

Now let's write the code to show a planet. First let's add a function to the PlanetStore class to pull a Planet out of the database:

    public function getPlanet(string $planetName): ?Planet
    {
        $statement = $this->connection->prepare(
            'SELECT name, population_size FROM planets where name = :name;'
        );
        $statement->bindValue(':name', $planetName);
        $statement->execute();

        $row = $statement->fetch(PDO::FETCH_ASSOC);

        if (! $row) {
            return null;
        }

        return new Planet($row['name'], (float) $row['population_size']);
    }

Notice the return type: ?Planet means that the function may either return an instance of Planet, or the value null. We can add ? to the beginning of any type name to make it nullable.

We use (float) to cast or convert the population size data from a string, as it comes back from the sqlite DB, to a floating point number.

In this case we have used a named parameter in the database query, instead of a question mark.

Finally we need to make a PHP script to go between this function and the browser. Call it viewPlanet.php:

<?php declare(strict_types=1);

namespace AModeratelyShortPhpTutorial;

require_once __DIR__ . '/vendor/autoload.php';

use PDO;

$planetStore = new PlanetStore(
    new PDO("sqlite:". __DIR__ . '/database.sqlite')
);

$planet = $planetStore->getPlanet((string)$_GET['name']);

header('Content-Type: text/plain');
if ($planet === null) {
    http_response_code(404);
    echo ('Planet not found');
    exit();
}

echo "**********   {$planet->getName()}   **********\n\n";
echo "Population: {$planet->getPopulationSize()}\n";

Notice the $_GET array - PHP automatically fills this so-called superglobal array with any query parameters sent by the browser. It is available to us anywhere in our program, but it's best to limit where we use it - code that directly pulls data from superglobals can be very hard to test and re-use and understand.

Run the PHP server:

php -S localhost:8080

Open http://localhost:8080/viewPlanet.php?name=Earth in your browser.

Other Database Engines.

PDO has drivers for eleven other database engines that you can use use instead of SQLite. Connecting, sending queries, and receiving rows should work in the same way, but of course the details of connection strings and SQL code will vary from DB to DB.