Lazy initialization
Lazy initialization, also known as late initialization, means that an object is not fully initialized in the constructor, but only on demand, i.e. when we actually use the object.
You can find a good example of lazy initialization here.
Lazy initialization is particularly useful for database connections. Instead of using a PHP built-in object like PDO
or mysqli
directly and thereby establishing a database connection as soon as the object is instantiated, we can create a wrapper that automatically establishes a connection exactly when we actually talk to the database for the first time.
Here is an example for SQLite
, because it works so well without external dependencies in the PHP installation:
<?php declare(strict_types=1);
final class SqliteConnection implements Connection
{
private ?SQLite3 $connection = null;
private string $database;
public function __construct(string $database)
{
$this->database = $database;
}
public function prepare(string $statement): SQLite3Stmt
{
return $this->connection()->prepare($statement);
}
public function exec(string $statement): bool
{
var_dump('Executing statement ' . $statement);
return $this->connection()->exec($statement);
}
public function query(string $query): SQLite3Result
{
return $this->connection()->query($query);
}
private function connection(): Sqlite3
{
if ($this->connection === null) {
var_dump('Connecting to the database');
$this->connection = new SQLite3($this->database);
$this->connection->enableExceptions(true);
$this->connection->exec('PRAGMA journal_mode=WAL');
}
return $this->connection;
}
}
We have included two var_dump()
statements here so that we can see what happens later.
By the way, the class is final because we will neither extend nor mock it. Instead, we would mock the Connection
interface:
<?php declare(strict_types=1);
interface Connection
{
public function prepare(string $statement): SQLite3Stmt;
public function exec(string $statement): bool;
public function query(string $query): SQLite3Result;
}
It's much better to mock an interface instead of a concrete class, because an interface doesn't have a constructor. Well, at least an interface should not contain a constructor method. But that's just a side note.
Our SqliteConnection
is great because we also configure the database connection in the connect()
method. At last we have a central place in the code where we can do this and the problem of configuring a connection set up elsewhere differently is thus eliminated.
Let's try it out. We use an in-memory database because we said that we wanted to work without external dependencies:
<?php declare(strict_types=1);
require __DIR__ . '/autoload.php';
$db = new SqliteConnection(':memory:');
$db->exec('SELECT * FROM sqlite_schema');
The database used is empty, or rather it has no schema. But that doesn't bother us, because we only want to establish a connection and not actually execute productive queries.
So let's get started:
SqliteConnection.php:20: string(47) "Executing statement SELECT * FROM sqlite_schema" SqliteConnection.php:34: string(26) "Connecting to the database"
We see that the exec()
method is called first and then connect()
. That's how it should be.
The connect()
method has code similar to what we would find in a singleton. This ensures that we only establish a database connection once and not every time we interact with the database.
To test whether this really works, we send two queries one after the other:
<?php declare(strict_types=1);
require __DIR__ . '/autoload.php';
$db = new SqliteConnection(':memory:');
$db->exec('SELECT * FROM sqlite_schema');
$db->exec('SELECT * FROM sqlite_schema');
Does this work?
SqliteConnection.php:20: string(47) "Executing statement SELECT * FROM sqlite_schema" SqliteConnection.php:34: string(26) "Connecting to the database" SqliteConnection.php:20: string(47) "Executing statement SELECT * FROM sqlite_schema"
Indeed, it works. The connection was only established once and reused for the second query. You could of course do the same for other database interfaces such as PDO
or mysqli
.
Are you wondering why we have a leaky abstraction here, because the return values of the methods are the original PHP built-in classes? It is good if you have recognized this. But in this case, it's not about abstracting us from the actual database interface, but implementing the lazy initialization design pattern. The leaky abstraction is intentional and not harmful.