Search by

yahyaerturan / auth-pdo

yahyaerturan

Portable PDO persistence adapters and reference migrations for yahyaerturan/auth, supporting SQLite, MariaDB/MySQL and PostgreSQL with explicit SQL and no ORM.

Package info

github.com/yahyaerturan/auth-pdo

pkg:composer/yahyaerturan/auth-pdo

Statistics

Installs: 2

Dependents: 0

Suggesters: 2

Stars: 0

Open Issues: 0

v1.0.0 2026-09-08 23:19 UTC

This package is auto-updated.

Last update: 2026-09-08 23:28:40 UTC


README

Portable PDO persistence for yahyaerturan/auth — explicit SQL, no ORM, no query builder, and reference migrations for SQLite, MySQL/MariaDB and PostgreSQL that ship in the package.

composer require yahyaerturan/auth-pdo

Requires PHP 8.5, ext-pdo and yahyaerturan/auth. Nothing else.

Why you would install it

yahyaerturan/auth defines repository ports and no implementations, so it can be used with any storage. This package is the reference implementation of those ports on top of PDO: seven adapters, a transaction manager, and the SQL to create the schema they read.

It exists so that "use this library with a relational database" is a composer require rather than a week of writing adapters and discovering, one subtle bug at a time, that a compare-and-set implemented as read-then-write is not a compare-and-set.

Every adapter is verified against the shared repository contract suites from yahyaerturan/auth-testing, on all three engines, in CI, with no skips.

Supported databases

Engine Tested against Driver
SQLite 3.35+ pdo_sqlite
MySQL / MariaDB MySQL 8.4, MariaDB 12.3 pdo_mysql
PostgreSQL 17, 18 pdo_pgsql

"Tested against" means the full contract suite plus driver-specific suites actually ran — CI asserts the absence of skips per suite per engine, because a suite whose DSN is unset skips, and a skipped suite is indistinguishable from a passing one in an aggregate run.

PostgreSQL runs against a server deliberately configured to a non-UTC timezone, because that is what proves the timestamp without time zone choice holds rather than merely happening to work.

Minimal usage

<?php

declare(strict_types=1);

use YahyaErturan\Auth\Pdo\MigrationSet;
use YahyaErturan\Auth\Pdo\Migrator;
use YahyaErturan\Auth\Pdo\PdoCredentialRepository;
use YahyaErturan\Auth\Pdo\PdoSessionRepository;
use YahyaErturan\Auth\Pdo\PdoTransactionManager;
use YahyaErturan\Auth\Pdo\PdoUserRepository;

$pdo = new PDO($dsn, $username, $password, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,   // required
]);

// Required on SQLite, and only on SQLite. Per connection, not per database.
$pdo->exec('PRAGMA foreign_keys = ON');

// Once, from a deploy step or a console command — never from a request.
(new Migrator($pdo, new DateTimeImmutable('now', new DateTimeZone('UTC')), MigrationSet::Core))->migrate();

$users        = new PdoUserRepository($pdo);
$credentials  = new PdoCredentialRepository($pdo);
$sessions     = new PdoSessionRepository($pdo);
$transactions = new PdoTransactionManager($pdo);

Hand those to the core's use cases and you have a working authentication stack.

Two connection requirements, and why they are requirements

PDO::ATTR_ERRMODE must be ERRMODE_EXCEPTION. Under the silent modes a failed statement returns false rather than raising, so a save that never happened is indistinguishable from one that did — and a failed lookup looks like "no such row", which is a storage failure rendered as an authentication answer. Construction raises ConnectionNotConfigured rather than fixing it silently: the connection is yours, and calling setAttribute() on it would change the behaviour of every other query you run through it.

On SQLite, PRAGMA foreign_keys must be on. SQLite parses REFERENCES clauses and then ignores them unless the connection asks. Without it, a repository would happily create credentials and sessions belonging to identities that do not exist — while the identical schema refuses on MySQL and PostgreSQL. Same treatment: refused at construction, never changed behind your back.

Everything else is yours. Emulated or native prepared statements, either MySQL row-count mode, any default fetch mode — all are tested and none is altered.

Migrations

The SQL ships in the package, under migrations/, one directory per engine. They are plain files: read them, diff them, paste them into your own migration tool, or let Migrator run them.

(new Migrator($pdo, $appliedAt, MigrationSet::Core))->migrate();

Nothing migrates during a request. There is no auto-migration, no "apply pending migrations on boot", and no code path from a repository to the migrator — asserted by an architecture test rather than left to discipline. Migrations are forward-only; destructive rollback of credentials and sessions is intentionally unsupported.

See docs/MIGRATIONS.md.

Authorization is optional — and it really is optional

This package ships three relational adapters for the authorization ports, under YahyaErturan\Auth\Pdo\Authorization\, plus a separate authorization migration set. It does not require yahyaerturan/auth-authorization.

Installing auth-pdo alone is fully supported. Every authentication adapter, the migrator and the core migration set work with no authorization package present. PSR-4 is lazy, so the three authorization classes are simply never loaded — no fatal error, no warning, no side effect at autoload time. That is verified by an installation test that composes and drives the package with authorization genuinely absent.

To use the authorization adapters, install the package explicitly:

composer require yahyaerturan/auth-authorization

Then run the second migration set and compose them:

use YahyaErturan\Auth\Pdo\Authorization\PdoAuthorizationGrantRepository;
use YahyaErturan\Auth\Pdo\Authorization\PdoPermissionRepository;
use YahyaErturan\Auth\Pdo\Authorization\PdoRoleRepository;

(new Migrator($pdo, $appliedAt, MigrationSet::Authorization))->migrate();

$roles       = new PdoRoleRepository($pdo);
$permissions = new PdoPermissionRepository($pdo);
$grants      = new PdoAuthorizationGrantRepository($pdo);

Attempting to use those classes without the package installed fails the way any missing class does — at the point of use, with a clear "class not found" — rather than at install time or at boot.

yahyaerturan/auth-authorization is declared as a suggest, never a require. Making it mandatory would install an authorization model in every application that merely wanted a database, which is precisely the coupling ADR-059 rejects.

What is in here

Repositories PdoUserRepository, PdoCredentialRepository, PdoSessionRepository, PdoOneTimeTokenRepository, PdoApiTokenRepository, PdoAuditRepository
Transactions PdoTransactionManager — refuses to nest (ADR-036)
Migrations Migrator, MigrationSet::Core, MigrationSet::Authorization
Exceptions ConnectionNotConfigured, UnsupportedDriver, NestedTransaction, MigrationFailed
Optional Authorization\PdoRoleRepository, Authorization\PdoPermissionRepository, Authorization\PdoAuthorizationGrantRepository

Everything else is Internal\ and not public API.

Security-relevant defaults

Every value is a bound parameter SQL lives in const nowdoc strings that cannot interpolate, and no statement is rewritten on its way to the driver — both asserted structurally
Identifier collation is binary on every engine a _ci collation would silently merge two accounts (ADR-037)
No usability filtering in SQL lookups return rows the domain will reject; deciding usability is the domain's job, where it can be tested (ADR-015)
Timestamps are UTC, without time zones, to the microsecond one codec, one format, on all three engines (ADR-034)
Compare-and-set is a single conditional statement never read-then-write; proven under real concurrency on MariaDB and PostgreSQL
Driver exceptions are translated a constraint violation becomes the core's uniqueness conflict; PDOException does not leak upward
No secret is ever stored a whole-database scan on all three engines asserts that no column contains a plaintext password or a raw bearer token
Nothing migrates during a request there is no path from a repository to the migrator

Do not show a chained exception to a user

Translated exceptions keep the driver exception as $previous, which is what makes them diagnosable. A PDOException message can contain the SQL, and therefore column names and sometimes values. Log the chain; render your own message.

What this package does not own

  • Authentication logic. Password verification, session lifetime, token consumption, rate limiting: all yahyaerturan/auth. This package stores rows.
  • The authorization model. yahyaerturan/auth-authorization defines roles, permissions and the decision. The three adapters here only persist them.
  • HTTP. No request, no response, no cookie. That is yahyaerturan/auth-psr15.
  • Scheduling. purgeExpiredBefore() exists on the stores that own expiring rows; calling it is your cron's job, not the library's (ADR-082).
  • Your connection's configuration. It validates two attributes and changes none.

Relationship to the rest of the ecosystem

Package Repository Relationship
yahyaerturan/auth https://github.com/yahyaerturan/auth required
yahyaerturan/auth-pdo https://github.com/yahyaerturan/auth-pdo this package
yahyaerturan/auth-authorization https://github.com/yahyaerturan/auth-authorization suggest — needed only for the Authorization\ adapters
yahyaerturan/auth-testing https://github.com/yahyaerturan/auth-testing dev — the contract suites this package is verified against
yahyaerturan/auth-psr15 https://github.com/yahyaerturan/auth-psr15 independent

Writing your own adapter instead

You do not have to use this package. If your storage is not relational — or is relational but not through PDO — implement the core's ports directly and run the same contract suites this package runs, from yahyaerturan/auth-testing.

That is what those suites are published for. See docs/CONTRACT_TESTING.md.

Development

git clone https://github.com/yahyaerturan/auth-pdo
cd auth-pdo
composer install
composer qa          # platform, coding standard, PHPStan, PHPUnit

composer test alone runs SQLite. The MySQL and PostgreSQL suites skip unless a DSN is configured, and a skip is not a pass — CI asserts per suite per engine that it actually ran. To run the full matrix locally:

AUTH_PDO_MYSQL_DSN='mysql:host=127.0.0.1;port=3306;dbname=auth_test;charset=utf8mb4' \
AUTH_PDO_MYSQL_USER=... AUTH_PDO_MYSQL_PASSWORD=... \
AUTH_PDO_PGSQL_DSN='pgsql:host=127.0.0.1;port=5432;dbname=auth_test' \
AUTH_PDO_PGSQL_USER=... AUTH_PDO_PGSQL_PASSWORD=... \
composer test

Working across several packages at once

Clone the repositories you need as siblings, then point Composer at the checkouts without committing anything:

cp composer.json composer.dev.json
composer config --file composer.dev.json repositories.siblings \
    '{"type":"path","url":"../auth*","options":{"symlink":true}}'
composer config --file composer.dev.json minimum-stability dev
COMPOSER=composer.dev.json composer update

composer.dev.json and composer.dev.lock are git-ignored.

Documentation

Contributing

Issues and pull requests are welcome — see CONTRIBUTING.md for the setup, the quality gate, and the conventions this project expects. Participation is governed by the Code of Conduct.

Found a security vulnerability? Do not open an issue or a pull request.

Security

See SECURITY.md.

License

MIT — see LICENSE.