1. Go to this page and download the library: Download power-modules/persistence library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
use Modular\Persistence\Schema\Contract\ISchema;
use Modular\Persistence\Schema\Contract\IHasIndexes;
use Modular\Persistence\Schema\Definition\ColumnDefinition;
use Modular\Persistence\Schema\Definition\Index;
use Modular\Persistence\Schema\Definition\IndexType;
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),
];
}
}
readonly class User
{
public function __construct(
public string $id,
public string $email,
public ?string $name,
) {}
}
/**
* @implements IHydrator<User>
*/
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,
];
}
}
/**
* @extends AbstractGenericRepository<User>
*/
class UserRepository extends AbstractGenericRepository
{
protected function getTableName(): string
{
return UserSchema::getTableName();
}
}
class UserModule implements PowerModule, ImportsComponents
{
public static function imports(): array
{
return [
ImportItem::create(PersistenceModule::class, PostgresDatabase::class),
];
}
public function register(ConfigurableContainerInterface $container): void
{
$container->set(UserRepository::class, UserRepository::class)
->addArguments([
PostgresDatabase::class,
UserHydrator::class,
])
;
}
}
#### 4. Use Repository
$repo = $app->get(UserRepository::class);
$user = new User(Uuid::uuid7()->toString(), '[email protected]', 'Test User');
$repo->upsert($user);
use Modular\Persistence\Repository\Contract\IRepository;
class UserService
{
/** @param IRepository<User> $users */
public function __construct(
private readonly IRepository $users,
) {}
}
// Create
$repo->insert($user); // Insert a single entity
$repo->insertAll([$user1, $user2, ...]); // Bulk insert (auto-chunked, auto-transaction)
$repo->insertAll($entities, chunkSize: 50); // Custom chunk size (default: 100)
$repo->upsert($user); // Insert or update in a single query (ON CONFLICT by default)
// Read
$repo->find($id); // Find by primary key (or null)
$repo->findOrFail($id); // Find by primary key (or throw EntityNotFoundException)
$repo->findBy([ // Find all matching conditions
Condition::equals(UserSchema::Email, '[email protected]'),
]);
$repo->findBy(limit: 10, offset: 20); // Paginated results
$repo->findOneBy([ // Find first match or null
Condition::equals(UserSchema::Name, 'John'),
]);
$repo->findOneByOrFail([...]); // Find first match or throw EntityNotFoundException
$repo->count(); // Count all rows
$repo->exists([...]); // Check existence (bool)
// Update
$repo->update($user); // Update entity by ID
$repo->updateBy( // Update arbitrary columns by conditions
['name' => 'New Name'],
[Condition::equals(UserSchema::Email, '[email protected]')],
);
// Delete
$repo->delete($id); // Delete by primary key
$repo->deleteBy([...]); // Delete by conditions
use Modular\Persistence\Repository\Condition;
use Modular\Persistence\Repository\ConditionXor;
// Comparison
Condition::equals(UserSchema::Email, '[email protected]');
Condition::notEquals(UserSchema::Status, 'inactive');
Condition::greater(UserSchema::Age, 18);
Condition::greaterEquals(UserSchema::Age, 18);
Condition::less(UserSchema::Age, 65);
Condition::lessEquals(UserSchema::Age, 65);
// Sets
Condition::in(UserSchema::Role, ['admin', 'editor']);
Condition::notIn(UserSchema::Status, ['banned', 'suspended']);
// Nullability
Condition::isNull(UserSchema::DeletedAt);
Condition::notNull(UserSchema::Email);
// Pattern matching
Condition::like(UserSchema::Name, 'John'); // LIKE '%John%'
Condition::ilike(UserSchema::Name, 'john'); // ILIKE '%john%' (Postgres, case-insensitive)
// Subqueries
Condition::exists('SELECT 1 FROM orders WHERE orders.user_id = users.id');
// OR conditions
new Condition(UserSchema::Role, Operator::Equals, 'admin', ConditionXor::Or);
// Containment: metadata @> '{"status":"active"}'::jsonb
Condition::jsonContains(ArticleSchema::Metadata, '{"status":"active"}');
// Reverse containment: metadata <@ '{"status":"active","lang":"en"}'::jsonb
Condition::jsonContainedBy(ArticleSchema::Metadata, '{"status":"active","lang":"en"}');
// Key existence: jsonb_exists(metadata, 'status')
Condition::jsonHasKey(ArticleSchema::Metadata, 'status');
// Any key existence: jsonb_exists_any(metadata, array['status','lang'])
Condition::jsonHasAnyKey(ArticleSchema::Metadata, ['status', 'lang']);
// All keys existence: jsonb_exists_all(metadata, array['status','lang'])
Condition::jsonHasAllKeys(ArticleSchema::Metadata, ['status', 'lang']);
// JSON path expression with standard operators (column expression is NOT quoted)
Condition::jsonPath('"metadata"->>\'status\'', Operator::Equals, 'active');
Condition::jsonPath('"metadata"->>\'title\'', Operator::Ilike, 'search term');
use Modular\Persistence\Repository\Statement\Contract\Bind;
$select = $this->getSelectStatement();
$select->addCondition(Condition::ilike(ArticleSchema::Title, 'search'));
$select->addRawCondition(
"\"metadata\"->'keywords' @> :kw_filter::jsonb",
[Bind::json('keywords', ':kw_filter', ['php', 'jsonb'])],
);
$select->setLimit(20);
$select->setStart(0);
// Works seamlessly with both select and count
$data = $this->select($select);
$total = $this->count([], $select);
use Modular\Persistence\Repository\Join;
use Modular\Persistence\Repository\JoinType;
$select = $this->statementFactory->createSelectStatement($this->getTableName());
$select->addJoin(
new Join(
JoinType::Left,
OrderSchema::getTableName(),
UserSchema::Id, // Local key (accepts BackedEnum)
OrderSchema::UserId, // Foreign key (accepts BackedEnum)
),
);
$select->addCondition(Condition::notNull(OrderSchema::Id));
$select->addOrder('created_at', 'DESC');
$select->setLimit(10);
$select->setStart(0);
// Single-query upsert via ON CONFLICT ... DO UPDATE
$repo->upsert($user); // Defaulted to conflict on primary key, updates all non-key columns
// Always returns 1 (whether inserted or updated)
use Modular\Persistence\Repository\Statement\Dialect\MysqlDialect;
use Modular\Persistence\Repository\Statement\Factory\GenericStatementFactory;
$factory = new GenericStatementFactory('', new MysqlDialect());
$repo = new UserRepository($db, $hydrator, $factory);
// Ignore duplicates (ON CONFLICT DO NOTHING)
$insert = $this->statementFactory->createInsertStatement('users', ['id', 'email']);
$insert->prepareBinds(['id' => $id, 'email' => $email]);
$insert->ignoreDuplicates();
// Custom upsert (ON CONFLICT ... DO UPDATE)
$insert = $this->statementFactory->createInsertStatement('users', ['id', 'email', 'name']);
$insert->prepareBinds(['id' => $id, 'email' => $email, 'name' => $name]);
$insert->onConflictUpdate(
conflictColumns: ['email'],
updateColumns: ['name'],
);
// Generates: INSERT INTO "users" ... ON CONFLICT ("email") DO UPDATE SET "name" = EXCLUDED."name"
// ignoreDuplicates() => INSERT IGNORE INTO `users` ...
// onConflictUpdate() => INSERT INTO `users` ... ON DUPLICATE KEY UPDATE `name` = VALUES(`name`)
// 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"; # Will query tenant_123.users due to search_path
use Modular\Persistence\Schema\Contract\IHasForeignKeys;
use Modular\Persistence\Schema\Definition\ForeignKey;
enum OrderSchema: string implements ISchema, IHasForeignKeys
{
case Id = 'id';
case UserId = 'user_id';
// ...
public static function getForeignKeys(): array
{
return [
ForeignKey::make(
localColumnName: self::UserId,
foreignTableName: 'users',
foreignColumnName: UserSchema::Id,
foreignSchemaName: 'public', // Optional: cross-schema FK
),
];
}
}
public static function getIndexes(): array
{
return [
// Full-column GIN index (column quoted as identifier)
Index::make([self::Metadata], type: IndexType::Gin),
// Expression-based GIN index (expression NOT quoted)
Index::expression("(\"metadata\"->'keywords')", IndexType::Gin),
// Functional index
Index::expression("(lower(\"email\"))", IndexType::Btree, unique: true),
];
}
// Generates:
// CREATE INDEX ... USING GIN ("metadata");
// CREATE INDEX ... USING GIN (("metadata"->'keywords'));
// CREATE UNIQUE INDEX ... ((lower("email")));
use Modular\Persistence\Repository\Condition;
use Modular\Persistence\Repository\ConditionGroup;
public static function getIndexes(): array
{
return [
// Unique per template, ignoring NULL keys and soft-deleted rows
Index::make(
[self::TemplateId, self::ExternalKey],
unique: true,
where: ConditionGroup::all(
Condition::notNull(self::ExternalKey),
Condition::isNull(self::DeletedAt),
),
),
// Raw SQL predicate
Index::make([self::Email], unique: true, where: '"deleted_at" IS NULL'),
];
}
// Generates:
// CREATE UNIQUE INDEX ... ("template_id", "external_key") WHERE "external_key" IS NOT NULL AND "deleted_at" IS NULL;
// CREATE UNIQUE INDEX ... ("email") WHERE "deleted_at" IS NULL;
use Modular\Persistence\Database\LoggingQueryExecutor;
$loggingExecutor = new LoggingQueryExecutor($queryExecutor, $logger);
// Logs: query string, elapsed_ms, affected_rows (for exec)
Loading please wait ...
Before you can download the PHP files, the dependencies should be resolved. This can take some minutes. Please be patient.