{"id":21157091,"url":"https://github.com/initorm/database","last_synced_at":"2026-05-24T20:06:56.495Z","repository":{"id":211628174,"uuid":"729611754","full_name":"InitORM/Database","owner":"InitORM","description":"InitORM Database Manager","archived":false,"fork":false,"pushed_at":"2023-12-15T11:37:24.000Z","size":9,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-10-03T08:16:38.999Z","etag":null,"topics":["database","dbal","mysql","pdo","php","php8","postgresql","query-builder"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/InitORM.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-12-09T19:23:14.000Z","updated_at":"2023-12-09T19:26:00.000Z","dependencies_parsed_at":"2023-12-09T20:29:16.524Z","dependency_job_id":"c80584d8-7648-4f0b-8128-cc615a53ea6c","html_url":"https://github.com/InitORM/Database","commit_stats":null,"previous_names":["initorm/database"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/InitORM/Database","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/InitORM%2FDatabase","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/InitORM%2FDatabase/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/InitORM%2FDatabase/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/InitORM%2FDatabase/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/InitORM","download_url":"https://codeload.github.com/InitORM/Database/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/InitORM%2FDatabase/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28133015,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-30T02:00:05.476Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["database","dbal","mysql","pdo","php","php8","postgresql","query-builder"],"created_at":"2024-11-20T12:00:06.973Z","updated_at":"2026-05-24T20:06:56.481Z","avatar_url":"https://github.com/InitORM.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# InitORM Database\n\nComposes [`initorm/dbal`](https://github.com/InitORM/DBAL) (PDO connection + result mapper) and [`initorm/query-builder`](https://github.com/InitORM/QueryBuilder) (fluent SQL builder) into a single Database manager with CRUD helpers, transactions, query logging, and an optional static facade.\n\n[![Latest Stable Version](http://poser.pugx.org/initorm/database/v)](https://packagist.org/packages/initorm/database)\n[![Total Downloads](http://poser.pugx.org/initorm/database/downloads)](https://packagist.org/packages/initorm/database)\n[![License](http://poser.pugx.org/initorm/database/license)](https://packagist.org/packages/initorm/database)\n[![PHP Version Require](http://poser.pugx.org/initorm/database/require/php)](https://packagist.org/packages/initorm/database)\n[![PHPUnit](https://github.com/InitORM/Database/actions/workflows/phpunit.yml/badge.svg)](https://github.com/InitORM/Database/actions/workflows/phpunit.yml)\n[![PHPStan](https://github.com/InitORM/Database/actions/workflows/phpstan.yml/badge.svg)](https://github.com/InitORM/Database/actions/workflows/phpstan.yml)\n[![PHP_CodeSniffer](https://github.com/InitORM/Database/actions/workflows/phpcs.yml/badge.svg)](https://github.com/InitORM/Database/actions/workflows/phpcs.yml)\n\n---\n\n## Requirements\n\n- **PHP 8.1 or later**\n- `ext-pdo`\n- One of `ext-pdo_mysql`, `ext-pdo_pgsql`, or `ext-pdo_sqlite` depending on the database you target.\n\n## Supported databases\n\nAny database with a PDO driver that follows standard SQL works out of the box. The query builder ships dialect-aware identifier quoting for **MySQL/MariaDB**, **PostgreSQL**, and **SQLite**; for everything else (`oci`, `sqlsrv`, …) it falls back to a generic, no-escaping driver.\n\n## Installation\n\n```bash\ncomposer require initorm/database\n```\n\n---\n\n## Quick start\n\n```php\n\u003c?php\nrequire_once 'vendor/autoload.php';\n\nuse InitORM\\Database\\Facade\\DB;\n\nDB::createImmutable([\n    'dsn'       =\u003e 'mysql:host=localhost;port=3306;dbname=app;charset=utf8mb4',\n    'username'  =\u003e 'app',\n    'password'  =\u003e 'secret',\n    'charset'   =\u003e 'utf8mb4',\n    'collation' =\u003e 'utf8mb4_unicode_ci',\n]);\n\n// CRUD shortcut\nDB::create('users', ['name' =\u003e 'Alice', 'email' =\u003e 'alice@example.com']);\n\n// Fluent builder + CRUD\n$rows = DB::select('id', 'name')\n    -\u003ewhere('active', '=', 1)\n    -\u003eorderBy('id', 'DESC')\n    -\u003elimit(10)\n    -\u003eread('users')\n    -\u003easAssoc()\n    -\u003erows();\n```\n\n\u003e `createImmutable()` sets the application-wide facade once. Calling it a second time throws — see [`docs/10-facade-vs-instance.md`](docs/10-facade-vs-instance.md) for swap and multi-connection patterns.\n\n---\n\n## Configuration reference\n\nAll keys are passed through to the underlying [`InitORM\\DBAL\\Connection\\Connection`](https://github.com/InitORM/DBAL/blob/master/src/Connection/Connection.php) constructor. Defaults are sane for MySQL.\n\n| Key            | Type                                 | Default       | Notes                                                                                          |\n| -------------- | ------------------------------------ | ------------- | ---------------------------------------------------------------------------------------------- |\n| `dsn`          | `string`                             | _(built)_     | When empty, a DSN is constructed from `driver`, `host`, `port`, `database`, `charset`.         |\n| `driver`       | `string`                             | `'mysql'`     | `mysql`, `pgsql`/`postgres`/`postgresql`, `sqlite`, or any PDO driver name.                    |\n| `host`         | `string`                             | `'127.0.0.1'` | Ignored when `dsn` is set explicitly.                                                          |\n| `port`         | `int\\|string`                        | `3306`        | Ignored when `dsn` is set explicitly.                                                          |\n| `database`     | `string`                             | `''`          | For SQLite use `':memory:'` or a file path.                                                    |\n| `username`     | `string\\|null`                       | `null`        |                                                                                                |\n| `password`     | `string\\|null`                       | `null`        |                                                                                                |\n| `charset`      | `string`                             | `'utf8mb4'`   | Applied on MySQL via `SET NAMES`. Pass `''` to skip (e.g. SQLite).                             |\n| `collation`    | `string\\|null`                       | `null`        | MySQL-only. Validated against `[A-Za-z0-9_]` before interpolation.                             |\n| `options`      | `array\u003cint, mixed\u003e`                  | `[]`          | Merged on top of safe PDO defaults (exceptions on errors, FETCH_ASSOC, no emulation).          |\n| `queryOptions` | `array\u003cint, mixed\u003e`                  | `[]`          | PDO `prepare()` options used for every statement.                                              |\n| `log`          | `string\\|callable\\|object\\|null`     | `null`        | See [Logger](#logger). File path, callable, or any object with a `critical(string)` method.    |\n| `debug`        | `bool`                               | `false`       | When true, query failure messages also include the bound parameters (JSON-encoded).            |\n| `queryLogs`    | `bool`                               | `false`       | Bootstrap value for the query log buffer (see [Query log](#query-log)).                        |\n\n---\n\n## CRUD\n\nAll CRUD helpers reset the builder's state on completion, so the next call starts with a clean slate. Every helper returns `bool true` on successful execution and throws on failure — use [`affectedRows()`](#affected-rows) when you also need to know how many rows changed.\n\n### Create\n\n```php\nuse InitORM\\Database\\Facade\\DB;\n\nDB::create('posts', [\n    'title'   =\u003e 'Post Title',\n    'content' =\u003e 'Post Content',\n]);\n\n$newId = DB::insertId();\n```\n\nGenerated SQL: `INSERT INTO posts (title, content) VALUES (:title, :content)`\n\n### Create batch\n\n```php\nDB::createBatch('posts', [\n    ['title' =\u003e 'Post #1', 'content' =\u003e 'Body 1', 'author_id' =\u003e 5],\n    ['title' =\u003e 'Post #2', 'content' =\u003e 'Body 2'],\n]);\n```\n\nGenerated SQL: `INSERT INTO posts (title, content, author_id) VALUES (:title, :content, :author_id), (:title_1, :content_1, NULL)`\n\nMissing columns in any row compile to `NULL`.\n\n### Read\n\n```php\n$res = DB::select('user.name AS author_name', 'post.id', 'post.title')\n    -\u003efrom('post')\n    -\u003eselfJoin('user', 'user.id=post.author')\n    -\u003ewhere('post.status', '=', 1)\n    -\u003eorderBy('post.id', 'ASC')\n    -\u003eorderBy('post.created_at', 'DESC')\n    -\u003eoffset(20)\n    -\u003elimit(10)\n    -\u003eread();\n\nforeach ($res-\u003easAssoc()-\u003erows() as $row) {\n    echo $row['title'] . ' by ' . $row['author_name'] . PHP_EOL;\n}\n```\n\n### Update\n\n```php\nDB::update('post', ['title' =\u003e 'New Title', 'content' =\u003e 'New Content'], ['id' =\u003e 13]);\n```\n\nGenerated SQL: `UPDATE post SET title = :title, content = :content WHERE id = :id`\n\n### Update batch\n\n```php\nDB::where('status', '!=', 0)\n    -\u003eupdateBatch('id', 'post', [\n        ['id' =\u003e 5,  'title' =\u003e 'New Title #5',  'content' =\u003e 'New Content #5'],\n        ['id' =\u003e 10, 'title' =\u003e 'New Title #10'],\n    ]);\n```\n\nGenerated SQL (formatted):\n\n```sql\nUPDATE post SET\n    title = CASE WHEN id = :id THEN :title WHEN id = :id_1 THEN :title_1 ELSE title END,\n    content = CASE WHEN id = :id_2 THEN :content ELSE content END\nWHERE status != :status AND id IN (:id_3, :id_4)\n```\n\n### Delete\n\n```php\nDB::delete('post', ['id' =\u003e 13]);\n```\n\nGenerated SQL: `DELETE FROM post WHERE id = :id`\n\n### Affected rows\n\n```php\nDB::update('users', ['active' =\u003e 0], ['active' =\u003e 1]);\necho DB::affectedRows(); // e.g. 42\n```\n\n`affectedRows()` returns the row count of the most recent CRUD call on the same Database instance.\n\n---\n\n## Raw queries\n\n```php\n$res = DB::query(\n    'SELECT id, title FROM post WHERE user_id = :id',\n    [':id' =\u003e 5]\n);\n\nif ($res-\u003enumRows() \u003e 0) {\n    $result = $res-\u003easObject()-\u003erow();\n    echo $result-\u003etitle;\n}\n```\n\nYou can also use `DB::raw()` inside the builder to inject literal SQL fragments — **never embed unsanitized user input**:\n\n```php\n$res = DB::select(DB::raw(\"CONCAT(name, ' ', surname) AS fullname\"))\n    -\u003ewhere(DB::raw('status = 1 OR status = 0'))\n    -\u003elimit(5)\n    -\u003eread('users');\n```\n\n---\n\n## Transactions\n\n```php\nDB::transaction(function (\\InitORM\\Database\\Interfaces\\DatabaseInterface $db) {\n    $db-\u003ecreate('orders',     ['user_id' =\u003e 5, 'total' =\u003e 199.90]);\n    $db-\u003ecreate('order_items',['order_id' =\u003e $db-\u003einsertId(), 'sku' =\u003e 'X', 'qty' =\u003e 1]);\n});\n```\n\n- The closure receives the Database instance.\n- Throw to abort: the current transaction is rolled back; if `$attempt \u003e 1` the closure is retried; otherwise the original error is rethrown wrapped in a `DatabaseException` (the original is reachable via `$e-\u003egetPrevious()`).\n- Pass `testMode: true` to roll back even on success — useful for integration tests.\n\n```php\n$caught = null;\ntry {\n    DB::transaction(function ($db) {\n        $db-\u003ecreate('orders', [...]);\n        throw new \\RuntimeException('boom');\n    });\n} catch (\\InitORM\\Database\\Exceptions\\DatabaseException $e) {\n    $caught = $e-\u003egetPrevious(); // \\RuntimeException 'boom'\n}\n```\n\n---\n\n## Multiple connections\n\n`DB::createImmutable()` registers a single shared facade. For secondary connections, use `DB::connect()` or instantiate `Database` directly — these do not touch the facade slot.\n\n```php\nuse InitORM\\Database\\Database;\n\n$reports = new Database([\n    'dsn'      =\u003e 'pgsql:host=reports.internal;dbname=reports',\n    'username' =\u003e 'reports_ro',\n    'password' =\u003e '…',\n    'driver'   =\u003e 'pgsql',\n]);\n\n$reports-\u003eread('events')-\u003easAssoc()-\u003erows();\n```\n\nIf you must swap the immutable facade target (rare; mostly for tests), call `DB::replaceImmutable($next)` explicitly — silent overrides are forbidden.\n\n---\n\n## Developer tools\n\n### Logger\n\nThe `log` credential accepts three shapes — a file path, a callable, or any object with a `critical(string)` method. The DBAL Logger writes a single string message per failed query, prefixed with the SQL and (when `debug` is on) the bound parameters.\n\n```php\n// 1) File path — file_put_contents() with append\nDB::createImmutable([\n    'dsn'  =\u003e 'mysql:host=localhost;dbname=app;charset=utf8mb4',\n    'log'  =\u003e __DIR__ . '/var/log/db-{year}-{month}-{day}.log',\n]);\n\n// 2) Callable\nDB::createImmutable([\n    'dsn'  =\u003e 'mysql:host=localhost;dbname=app;charset=utf8mb4',\n    'log'  =\u003e function (string $msg): void {\n        error_log($msg);\n    },\n]);\n\n// 3) Object with critical() (or a [$obj, 'method'] callable)\nclass Logger {\n    public function critical(string $msg): void { /* … */ }\n}\n\nDB::createImmutable([\n    'dsn'  =\u003e 'mysql:host=localhost;dbname=app;charset=utf8mb4',\n    'log'  =\u003e new Logger(),\n]);\n```\n\n### Debug mode\n\n```php\nDB::createImmutable([\n    'dsn'   =\u003e 'mysql:host=localhost;dbname=app;charset=utf8mb4',\n    'debug' =\u003e true, // include bound parameters in failure messages\n]);\n```\n\n\u003e Enable in development only — bound parameter dumps can include credentials and PII.\n\n### Query log\n\n```php\nDB::enableQueryLog();\nDB::read('users', ['id', 'name'], ['active' =\u003e 1]);\n\nvar_dump(DB::getQueryLogs());\n/*\n[\n    [\n        'query' =\u003e 'SELECT id, name FROM users WHERE active = :active',\n        'args'  =\u003e [':active' =\u003e 1],\n        'timer' =\u003e 0.000642,\n    ],\n]\n*/\n```\n\n`enableQueryLog()` / `disableQueryLog()` return the Database instance for chaining; `getQueryLogs()` returns every recorded entry. The buffer lives on the Connection — calling `disableQueryLog()` stops recording but does not clear previously-collected entries.\n\n---\n\n## Documentation\n\nIn-depth, code-first guides live under [`docs/`](docs/):\n\n- [`01-getting-started.md`](docs/01-getting-started.md)\n- [`02-configuration.md`](docs/02-configuration.md)\n- [`03-crud.md`](docs/03-crud.md)\n- [`04-query-builder.md`](docs/04-query-builder.md)\n- [`05-transactions.md`](docs/05-transactions.md)\n- [`06-raw-queries.md`](docs/06-raw-queries.md)\n- [`07-multiple-connections.md`](docs/07-multiple-connections.md)\n- [`08-logger-and-debug.md`](docs/08-logger-and-debug.md)\n- [`09-query-log-profiler.md`](docs/09-query-log-profiler.md)\n- [`10-facade-vs-instance.md`](docs/10-facade-vs-instance.md)\n- [`11-architecture.md`](docs/11-architecture.md)\n- [`12-upgrade-guide.md`](docs/12-upgrade-guide.md) — **migrating from v2 to v3**\n\n---\n\n## Contributing\n\nContributions are welcome. The general flow is:\n\n1. Fork and branch off `master`.\n2. Add tests for the behaviour you change — see [`tests/`](tests/) for patterns (SQLite in-memory, fast and dependency-free).\n3. Run the full quality suite locally:\n   ```bash\n   composer qa   # phpcs + phpstan + phpunit\n   ```\n4. Open a PR — CI will run the same suite across PHP 8.1–8.4.\n\nBy submitting a contribution you agree to license it under the MIT License.\n\n## Credits\n\n- [Muhammet ŞAFAK](https://www.muhammetsafak.com.tr) — `\u003cinfo@muhammetsafak.com.tr\u003e`\n\n## License\n\nReleased under the [MIT License](./LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finitorm%2Fdatabase","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Finitorm%2Fdatabase","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finitorm%2Fdatabase/lists"}