PHP code example of power-modules / persistence

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/ */

    

power-modules / persistence example snippets


// config/modular_persistence.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::SearchPath, $_ENV['DB_SCHEMA'] ?? 'public')
    ->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,
    ])
;

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);

$select->addJoin(
    new Join(
        JoinType::Inner,
        'departments',
        'dept_uuid',        // Local key (text column)
        'id',               // Foreign key (uuid column)
        localKeyType: 'uuid',
    ),
);
// Produces: INNER JOIN "departments" ON "departments"."id" = NULLIF("employees"."dept_uuid", '')::uuid

$db->beginTransaction();

try {
    $userRepo->insert($user);
    $orderRepo->insert($order);
    $db->commit();
} catch (\Throwable $e) {
    $db->rollBack();
    throw $e;
}

// 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

ColumnDefinition::uuid($col)->primaryKey();        // UUID primary key
ColumnDefinition::autoincrement($col)->primaryKey(); // BIGINT auto-increment
ColumnDefinition::text($col);                       // TEXT
ColumnDefinition::varchar($col, 255);                // VARCHAR(255)
ColumnDefinition::int($col);                         // INTEGER
ColumnDefinition::bigint($col);                      // BIGINT
ColumnDefinition::smallint($col);                    // SMALLINT
ColumnDefinition::tinyint($col);                     // TINYINT
ColumnDefinition::decimal($col, 10, 2);              // DECIMAL(10,2)
ColumnDefinition::date($col);                        // DATE
ColumnDefinition::timestamp($col);                   // TIMESTAMP
ColumnDefinition::timestamptz($col);                 // TIMESTAMPTZ
ColumnDefinition::jsonb($col);                       // JSONB
ColumnDefinition::mediumblob($col);                  // MEDIUMBLOB

// Modifiers (returns new immutable instance)
ColumnDefinition::text($col)->nullable()->default('N/A');

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)