flytachi/winter-cdo

Extended, type-safe PDO wrapper for PostgreSQL, MySQL/MariaDB, SQLite and Oracle with a composable, injection-safe query builder.

Maintainers

Package info

github.com/Flytachi/winter-cdo

Homepage

Issues

Documentation

pkg:composer/flytachi/winter-cdo

Transparency log

Statistics

Installs: 720

Dependents: 3

Suggesters: 0

Stars: 1

v4.0.1 2026-08-17 14:00 UTC

README

Latest Version on Packagist PHP Version Require Software License

CDO (Connection Data Object) extends PDO with the write operations applications actually perform โ€” insert, update, delete, upsert and their streaming batch variants โ€” and with Qb, a composable builder for the WHERE clause.

Every value travels as a bound parameter, so SQL text and data never meet by string concatenation. The driver-specific dialect is generated for you (PostgreSQL, MySQL / MariaDB, SQLite, Oracle), so the same call works across all of them.

๐Ÿ“– Documentation ยท Quick start ยท CDO API ยท Qb operators

Installation

composer require flytachi/winter-cdo

Requires PHP 8.3+, ext-pdo and psr/log ^3.0.

Supported databases

Database insert insertBatch upsert upsertBatch update delete
PostgreSQL โœ… โœ… โœ… โœ… โœ… โœ…
MySQL / MariaDB โœ… โœ… โœ… โœ… โœ… โœ…
SQLite โœ… โœ… โœ… โœ… โœ… โœ…
Oracle โš ๏ธ โœ… โŒ โŒ โœ… โœ…

SQLite uses PostgreSQL-style ON CONFLICT upserts; insert() / upsert() return the last inserted id via lastInsertId() rather than RETURNING, and timezone sync is a no-op because SQLite has no session timezone.

Quick start

Declare a database by filling in setUp():

use Flytachi\Winter\Cdo\Config\PgDbConfig;

class AppDb extends PgDbConfig
{
    public function setUp(): void
    {
        $this->host     = env('DB_HOST', 'localhost');
        $this->port     = (int) env('DB_PORT', 5432);
        $this->database = env('DB_NAME', 'myapp');
        $this->username = env('DB_USER', 'postgres');
        $this->password = env('DB_PASS', '');
    }
}

Then ask the pool for a connection and write:

use Flytachi\Winter\Cdo\ConnectionPool;
use Flytachi\Winter\Cdo\Qb;

$cdo = ConnectionPool::db(AppDb::class);

$id = $cdo->insert('users', ['name' => 'Alice', 'email' => 'alice@example.com']);

$cdo->update('users', ['name' => 'Alice Smith'], Qb::eq('id', $id));
$cdo->delete('users', Qb::eq('id', $id));

A one-off connection needs no class โ€” the inline Call variants take the credentials directly, and SQLite needs none at all:

use Flytachi\Winter\Cdo\Config\Call\SqliteDbCall;

$cdo = (new SqliteDbCall())->connection();   // in-memory, handy in tests

What you get

  • Write operations, not a query language โ€” insert, update, delete, upsert take a table, an entity and a condition; the SQL is generated per driver.
  • Streaming batches โ€” insertBatch / upsertBatch accept a generator, so peak memory follows the chunk size rather than the size of the job.
  • A composable WHERE โ€” Qb fragments combine with and / or / xor, skip null, and parenthesise groups so an inner OR cannot break the surrounding AND.
  • Type-aware binding โ€” the PDO::PARAM_* type is derived from the PHP value; objects go through DateTimeInterface / JsonSerializable / __toString().
  • Named binds โ€” one CDOBind reused across several conditions stays a single placeholder.
  • Lazy connections โ€” a config is instantiated once and cached; the socket opens on first use, with ping() / reconnect() for long-lived workers.
  • PSR-3 logging โ€” give it a logger and each statement, its bindings and its timing are recorded.

A taste of Qb

Qb::and(
    Qb::eq('status', 'active'),
    Qb::gte('age', 18),
    Qb::or(
        Qb::like('email', '%@example.com'),
        Qb::in('role', ['admin', 'editor']),
    ),
);
// (status = :iqb0 AND age >= :iqb1 AND (email LIKE :iqb2 OR role IN (:iqb3, :iqb4)))

Optional filters drop out by themselves, because logical operators skip null:

Qb::and(
    Qb::eq('published', true),
    $categoryId ? Qb::eq('category_id', $categoryId) : null,
    $tagIds     ? Qb::in('tag_id', $tagIds)          : null,   // in() throws on []
);

Values are bound; column names are not. A column name cannot be a placeholder, so it goes into the SQL verbatim. Qb::eq('status', $userInput) is safe; Qb::eq($userInput, 'active') is an injection vector โ€” never let user input choose a column without a whitelist.

Every operator with the SQL it emits: Qb operators.

Documentation

The user-facing documentation lives at winterframe.net/packages/cdo (the link picks your language; RU and EN are both complete).

Start here

Page What it answers
Introduction What CDO is, and where it sits next to plain PDO
Installation Requirements, install, driver extensions
Quick start Config, connection, first write
Mental model How config, pool, CDO and Qb relate

Guides

Page What it answers
Inserting records Single rows, returned ids, batches
Updating and deleting Conditions, affected rows, staying safe
Upserts Conflict columns and what gets updated
Building conditions Composing Qb, optional filters, grouping
Logging and diagnostics Seeing the SQL, the bindings and the timing

Reference

Page What it answers
CDO API Every method, its arguments and its return value
Qb operators All operators with the SQL they emit
Configuration Config classes, inline calls, driver options
Upsert placeholders :new, :current, and expressions between them
Exceptions What is thrown, and which SQLSTATE means what

Deep dive

Page What it answers
Batches and chunking Memory, partial failure, choosing a chunk size
Parameter binding How a PHP value becomes a bound parameter
Driver detection What changes per driver, and how it is decided

Classes in this package carry an @link to their page, so the same documentation is one click away from your IDE.

Contributing

Internal technical notes โ€” exact contracts, the SQL each operator emits, and the reasoning behind decisions that are not obvious from the code โ€” live in docs/. Read that before changing generated SQL.

composer test        # phpunit
composer test-detail # phpunit --testdox
composer cs-check    # phpcs
composer cs-fix      # phpcbf

License

MIT License. See LICENSE.