power-modules / persistence
Persistence module for Power Modules framework.
Installs: 18
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/power-modules/persistence
Requires
- php: ^8.4
- power-modules/console: ^0.2.3
- power-modules/framework: ^2.1
- ramsey/uuid: ^4.9
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.91
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.4
README
A type-safe, multi-tenant persistence layer for PHP 8.4+ built on the Modular Framework. It provides a robust Repository pattern implementation with native support for Postgres schemas and strict type safety.
💡 Robust: Built for complex applications requiring strict data integrity, multi-tenancy, and clear separation of concerns.
✨ Why Modular Persistence?
- 🔒 Type-Safe Schemas: Define database schemas using PHP Enums
- 🏢 Multi-Tenancy Native: Built-in support for dynamic Postgres schemas (namespaces)
- 📦 Repository Pattern: Generic CRUD repositories with decoupled SQL generation
- 🔄 Explicit Hydration: Full control over object-relational mapping without magic
- 🛠️ Scaffolding: CLI commands to generate your entire persistence layer
- ⚡ Performance: Lightweight wrapper around PDO with optimized query generation
🚀 Installation
composer require power-modules/persistence
⚙️ Configuration
Register the module in your ModularAppBuilder and provide configuration in config/modular_persistence.php:
// config/modular_persistence.php <?php declare(strict_types=1); use Modular\Persistence\Config\Config; use Modular\Persistence\Config\Setting; return Config::create() ->set(Setting::Dsn, $_ENV['DB_DSN'] ?? 'pgsql:host=localhost;port=5432;dbname=myapp') ->set(Setting::Username, $_ENV['DB_USERNAME'] ?? 'postgres') ->set(Setting::Password, $_ENV['DB_PASSWORD'] ?? 'secret') ->set(Setting::Options, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_TIMEOUT => 5, ]) ;
🏗️ Quick Start
The fastest way to get started is using the scaffolding command:
php bin/console persistence:scaffold User --table=users
This will generate:
UserSchema(Enum)User(Entity)UserHydrator(Mapper)UserRepository(Repository)
Manual Setup
1. Define Schema
use Modular\Persistence\Schema\Contract\ISchema; use Modular\Persistence\Schema\Contract\IHasIndexes; use Modular\Persistence\Schema\Definition\ColumnDefinition; use Modular\Persistence\Schema\Definition\Index; enum UserSchema: string implements ISchema, IHasIndexes { case Id = 'id'; case Email = 'email'; case Name = 'name'; public static function getTableName(): string { return 'users'; } public function getColumnDefinition(): ColumnDefinition { return match ($this) { self::Id => ColumnDefinition::uuid($this)->primaryKey(), self::Email => ColumnDefinition::text($this), self::Name => ColumnDefinition::text($this)->nullable(), }; } public static function getIndexes(): array { return [ Index::make([self::Email], unique: true), ]; } }
2. Create Entity & Hydrator
readonly class User { public function __construct( public string $id, public string $email, public ?string $name, ) {} } class UserHydrator implements IHydrator { use TStandardIdentity; public function hydrate(array $data): User { return new User( Uuid::fromString($data[UserSchema::Id->value]), $data[UserSchema::Email->value], $data[UserSchema::Name->value], ); } public function dehydrate(mixed $entity): array { return [ UserSchema::Id->value => $entity->id, UserSchema::Email->value => $entity->email, UserSchema::Name->value => $entity->name, ]; } }
3. Use Repository
class UserRepository extends AbstractGenericRepository { protected function getTableName(): string { return UserSchema::getTableName(); } } // Usage $repo = $app->get(UserRepository::class); $user = new User(Uuid::uuid7()->toString(), 'test@example.com', 'Test User'); $repo->save($user);
🏢 Multi-Tenancy
Modular Persistence supports multi-tenancy via Postgres schemas (namespaces) using a Decorator Pattern on the database connection. This ensures search_path is correctly set for every query, allowing for clean SQL generation and correct Foreign Key resolution.
// 1. Setup Database with Decorator $rawDb = new PostgresDatabase($pdo); $nsProvider = new RuntimeNamespaceProvider(); // Decorate the database to handle automatic context switching $db = new NamespaceAwarePostgresDatabase($rawDb, $nsProvider); // 2. Setup Factory (No namespace provider needed here for dynamic tenancy) $factory = new GenericStatementFactory(); // 3. Inject into Repository $repo = new UserRepository($db, $hydrator, $factory); // 4. Switch Context $nsProvider->setNamespace('tenant_123'); $repo->findBy(); // Internally executes: // SET search_path TO "tenant_123"; // SELECT * FROM "users";
🛠️ Console Commands
persistence:scaffold- Generate all files for a domain entitypersistence:make-schema- Generate a Schema Enumpersistence:make-entity- Generate an Entity classpersistence:make-hydrator- Generate a Hydratorpersistence:make-repository- Generate a Repositorypersistence:generate-schema- Generate SQL migration from Schema Enums