Getting Started

Install

Using Composer, the installation is trivial:

composer require ondrej-bouda/ivory

PHP ≥ 7.2 is required with several standard extensions1, checked by Composer. PostgreSQL ≥ 9.4 is officially supported, although older PostgreSQL versions might work just fine.

During installation, a PSR-6-compliant cache is suggested. We’ll get back to it later.

Connect

First, a database connection must be set up:

<?php
$connParams = ConnectionParameters::create([
    ConnectionParameters::HOST => 'localhost',
    ConnectionParameters::USER => 'ivory',
    ConnectionParameters::PASSWORD => '***',
    ConnectionParameters::DBNAME => 'ivory_test',
]);
$conn = Ivory::setupNewConnection($connParams);

The connection is both returned to $conn and registered in a global connection storage so that it is available from anywhere. Since this is the first registered connection, it automatically becomes the default one.

Once the connection is defined, we may start connecting to the database:

<?php
$conn->connect();

The method returns immediately while the connection is being established in the background. In the meantime, the application may do jobs not requiring database, such as routing or input processing.

Query

<?php
$conn = Ivory::getConnection();
$relation = $conn->query(
    'SELECT * FROM album WHERE EXTRACT(YEAR FROM released) >= %i ORDER BY released',
    2000
);
foreach ($relation as $tuple) {
    echo $tuple->name . ', released ' . $tuple->released->format('n/j/Y') . PHP_EOL;
}

First, the default connection is grabbed. The query() method is given an SQL pattern with parameters. It waits for the connection to be ready, then sends the SQL query to the database and returns the resulting relation. In the relation, a text column name and a date column released are present, which are available as values of PHP type string and \Ivory\Value\Date, respectively.

Out of the box, Ivory supports all standard data types shipped with PostgreSQL and all type constructors – arrays, composites, enums, ranges, and domains.

<?php
$ranges = $conn->querySingleValue(<<<'SQL'
    SELECT ARRAY[
               daterange('2015-05-19', '2015-12-01'),
               daterange('2015-12-01', '2017-02-19'),
               daterange('2017-02-19', NULL)
           ]
SQL
);
echo $ranges[2]->getLower()->format('n/j/Y'); // prints "12/1/2015"

Note that Ivory keeps the array indices when converting to PHP arrays (and vice versa), i.e., $ranges is a one-based array. That may be suppressed by configuring the array type converter, though.

There’s a lot more on types – see the documentation chapter Data Types.

Command

Statements which do not yield data sets, such as INSERT or UPDATE, are executed using the command() method, which only returns the summary effect of the statement:

<?php
$result = $conn->command(
    'INSERT INTO album (name, released) VALUES (%s, %date)',
    'Hardwired...To Self-Destruct',
    \Ivory\Value\Date::fromParts(2016, 11, 18)
);
echo 'Inserted ' . $result->getAffectedRows() . ' rows' . PHP_EOL; // prints "Inserted 1 rows"

Unlike other database layers, which use query() for everything, Ivory strictly distinguishes between query() and command() as they have different effects and return a different class of result.

Set Up Cache

Using a cache is vital for Ivory to prevent introspecting the data types on each request and parsing the same SQL patterns over and over again. For basic usage, a filesystem cache will do the job. For production, Memcached or similar shared memory cache is more appropriate.

The only step is to plug in a PSR-6-compliant cache before connecting to the database. For example, install:

composer require cache/filesystem-adapter

and use:

<?php
$fsAdapter = new \League\Flysystem\Adapter\Local(sys_get_temp_dir());
$fs = new \League\Flysystem\Filesystem($fsAdapter);
$cachePool = new \Cache\Adapter\Filesystem\FilesystemCachePool($fs);
Ivory::setDefaultCacheImpl($cachePool);

Putting It All Together

All the pieces, including the test database DDL and data, follow for you to play with:

composer require ondrej-bouda/ivory cache/filesystem-adapter
<?php
use Ivory\Connection\ConnectionParameters;
use Ivory\Ivory;

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

// Setup cache
$fsAdapter = new \League\Flysystem\Adapter\Local(sys_get_temp_dir());
$fs = new \League\Flysystem\Filesystem($fsAdapter);
$cachePool = new \Cache\Adapter\Filesystem\FilesystemCachePool($fs);
Ivory::setDefaultCacheImpl($cachePool);

// Setup connection
$connParams = ConnectionParameters::create([
    ConnectionParameters::HOST => 'localhost',
    ConnectionParameters::USER => 'ivory',
    ConnectionParameters::PASSWORD => '***',
    ConnectionParameters::DBNAME => 'ivory_test',
]);
$conn = Ivory::setupNewConnection($connParams);
$conn->connect();

// Sample database
$conn->runScript(<<<'SQL'
    DROP TABLE IF EXISTS album;
    CREATE TABLE album (
        id BIGSERIAL PRIMARY KEY,
        name TEXT NOT NULL,
        released DATE
    );
    INSERT INTO album (name, released) VALUES
        ('The Piano Guys', '2012-10-02'),
        ('Black Album', '1991-08-12'),
        ('S & M', '1999-11-23'),
        ('Live One', '2005-01-01'),
        ('Meeting Point', '2014-10-27');
SQL
);

// Query
$relation = $conn->query(
    'SELECT * FROM album WHERE EXTRACT(YEAR FROM released) >= %i ORDER BY released',
    2000
);
foreach ($relation as $tuple) {
    echo $tuple->name . ', released ' . $tuple->released->format('n/j/Y') . PHP_EOL;
}

$ranges = $conn->querySingleValue(<<<'SQL'
    SELECT ARRAY[
               daterange('2015-05-19', '2015-12-01'),
               daterange('2015-12-01', '2017-02-19'),
               daterange('2017-02-19', NULL)
           ]
SQL
);
echo $ranges[2]->getLower()->format('n/j/Y') . PHP_EOL; // prints "12/1/2015"

// Command
$result = $conn->command(
    'INSERT INTO album (name, released) VALUES (%s, %date)',
    'Hardwired...To Self-Destruct',
    \Ivory\Value\Date::fromParts(2016, 11, 18)
);
echo 'Inserted ' . $result->getAffectedRows() . ' rows' . PHP_EOL; // prints "Inserted 1 rows"

$cnt = $conn->querySingleValue('SELECT COUNT(*) FROM album');
echo 'Album count: ' . $cnt . PHP_EOL; // prints "Album count: 6"

Further Steps

First, reading the Features page is recommended to get the idea about what Ivory offers.

To dive more into details, you can study the documentation. There is a more enjoyable way, however – a set of unit tests especially written to demonstrate specifics and capabilities of Ivory. Come and play with the unit test classes within Ivory\Showcase!


1 Currently, Ivory needs the following PHP extensions, all of which are a part of the standard PHP distribution: bcmath, dom, iconv, json, mbstring, pgsql, simplexml, and xmlreader.