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
composer require flytachi/winter-cdoRequires PHP 8.3+, ext-pdo and psr/log ^3.0.
| 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.
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- Write operations, not a query language —
insert,update,delete,upserttake a table, an entity and a condition; the SQL is generated per driver. - Streaming batches —
insertBatch/upsertBatchaccept a generator, so peak memory follows the chunk size rather than the size of the job. - A composable
WHERE—Qbfragments combine withand/or/xor, skipnull, and parenthesise groups so an innerORcannot break the surroundingAND. - Type-aware binding — the
PDO::PARAM_*type is derived from the PHP value; objects go throughDateTimeInterface/JsonSerializable/__toString(). - Named binds — one
CDOBindreused 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.
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.
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.
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- Changes and upgrade notes: CHANGELOG.md
- How to contribute (setup, tests, coding standard): CONTRIBUTING.md
- Reporting a vulnerability: SECURITY.md
MIT License. See LICENSE.