Repozitáře

Veškerá práce s daty probíhá výhradně přes repozitáře v app/repositories/. Žádné SQL dotazy nesmí být nikde jinde v projektu — ne v kontrolerech, ne v servisách, ne v jobech.

Struktura

app/repositories/
├── BaseRepository.php          // Abstraktní základ — CRUD metody
├── UserRepository.php          // Repozitář pro tabulku users
├── system/                     // Systémové repozitáře
│   ├── CronVariablesRepository.php
│   └── MigrationsRepository.php
└── admin/                      // Admin repozitáře
    └── ...

Složky odpovídají logickým sekcím (admin, system, ...). Pro každou DB tabulku existuje jeden repozitář.

Vytvoření repozitáře

namespace Moony\app\repositories;

class OrdersRepository extends BaseRepository
{
    public const string MAIN_TABLE = 'orders';

    // Vlastní metody pro složitější dotazy
    public static function getActiveByUserId(int $userId): ?array
    {
        return self::queryBuilder()
            ->setCriteria([
                ['[orders].[user_id] = ?', $userId],
                ['[orders].[status] = ?', 'active']
            ])
            ->order(['created_at', 'DESC'])
            ->getResult();
    }

    public static function getTotalRevenue(): float
    {
        return self::queryBuilder()
            ->getSum('amount', 0);
    }

    public static function hasActiveOrders(int $userId): bool
    {
        return self::queryBuilder()
            ->setCriteria([
                ['[orders].[user_id] = ?', $userId],
                ['[orders].[status] = ?', 'active']
            ])
            ->exists();
    }
}
Každý repozitář musí: 1. extendovat BaseRepository, 2. definovat MAIN_TABLE konstantu, 3. mít všechny metody statické.

BaseRepository — metody

Zděděné metody dostupné v každém repozitáři:

Vytvoření

create(array $data, ?array $onDuplicateUpdate = [])INSERT. Vrátí ID. S onDuplicateUpdate provede UPSERT.
bulkCreate(array $data, bool $insertIgnore = false)Hromadný INSERT více řádků najednou
addToQueue(array $data)Přidá do fronty pro hromadný insert
flushQueue(bool $insertIgnore = false)Provede hromadný insert celé fronty

Čtení — jeden záznam

getOneById(int $id, ?array $columns, ?array $joins, ?array $orders)Jeden záznam podle ID
getOneBy(string $column, mixed $value, ...)Jeden záznam podle libovolného sloupce
getOneByUserId(int $id, ...)Jeden záznam podle user_id
getOneByIdForUpdate(int $id, ...)Jako getOneById ale s FOR UPDATE (zamkne řádek v transakci)

Čtení — více záznamů

getAll(?array $columns, ?array $joins, ?array $orders, ?int $limit)Všechny záznamy z tabulky
getAllBy(string $column, mixed $value, ...)Všechny záznamy podle sloupce
getAllCount(?array $columns, ?array $joins, ?array $orders)Počet všech záznamů
getAllCountBy(string $column, mixed $value, ?array $joins)Počet záznamů podle sloupce

Aktualizace

updateById(int $id, array $data)UPDATE podle ID
updateBy(array $criteria, array $data)UPDATE podle kritérií

Mazání

deleteById(int $id)DELETE podle ID
deleteBy(array $criteria)DELETE podle kritérií
deleteByReturning(array $criteria)DELETE s RETURNING * — vrátí smazané záznamy
safeDeleteById(int $id)Soft delete — nastaví deleted_at místo smazání
safeDeleteBy(array $criteria)Soft delete podle kritérií

QueryBuilder

queryBuilder()Vrátí novou instanci QueryBuilder pro MAIN_TABLE — pro složitější dotazy

Příklady

// Vytvoření
$id = OrdersRepository::create([
    'user_id' => User::getId(),
    'amount' => 199.90,
    'status' => 'pending',
    'created_at' => UTC::get()
]);

// Čtení
$order = OrdersRepository::getOneById($id);
$orders = OrdersRepository::getAllBy('user_id', User::getId());
$count = OrdersRepository::getAllCountBy('status', 'active');

// S joiny a řazením (BaseRepository metody)
$order = OrdersRepository::getOneById($id,
    columns: ['orders.*', 'users.email'],
    joins: ['LEFT JOIN users ON users.id = orders.user_id'],
);

// Update
OrdersRepository::updateById($id, [
    'status' => 'completed'
]);

// Soft delete
OrdersRepository::safeDeleteById($id);

// Hromadný insert (queue)
foreach($items as $item) {
    OrderItemsRepository::addToQueue([
        'order_id' => $orderId,
        'product_id' => $item['id'],
        'quantity' => $item['qty']
    ]);
}
OrderItemsRepository::flushQueue();

QueryBuilder

Pro složitější dotazy se používá queryBuilder(). Všechny metody jsou fluent (chainable).

Filtry a kritéria

setCriteria(array $criteria)Pro více WHERE podmínek najednou
criterion(array $criteria)Pro jednu WHERE podmínku
// Jedna podmínka → criterion()
->criterion(['status' => 'active'])
->criterion([['[orders].[amount] > ?', 100]])

// Více podmínek → setCriteria()
->setCriteria([
    ['[orders].[status] = ?', 'active'],
    ['[orders].[user_id] = ?', $userId]
])

Sloupce

setColumns(array $columns)Pro více sloupců najednou (výchozí *)
column(string $column)Pro jeden sloupec
// Jeden sloupec → column()
->column('users.email')

// Více sloupců → setColumns()
->setColumns(['id', 'email', 'created_at'])

Joiny

setJoins(array $joins)Pro více joinů najednou (pole stringů)
join(string $join)Pro jeden join
// Jeden join → join()
->join('LEFT JOIN users ON users.id = orders.user_id')

// Více joinů → setJoins()
->setJoins([
    'LEFT JOIN users ON users.id = orders.user_id',
    'LEFT JOIN products ON products.id = items.product_id'
])

Řazení a stránkování

order(array $order)Přidá řazení: ['created_at' => 'DESC']
setOrders(array $orders)Nastaví více řazení najednou
setPagination(int $page, int $perPage)Nastaví stránkování (LIMIT + OFFSET)
limit(int $limit)Omezí počet výsledků (= stránka 1 s daným limitem)

GROUP BY

groupBy(string|array $columns, ?string $having = null)GROUP BY s volitelnou HAVING klauzulí
->groupBy('status')
->groupBy(['status', 'user_id'], 'COUNT(*) > 5')

Výsledky

getResult(?Closure $callback)Vrátí pole záznamů. Callback se volá pro každý řádek.
getResultPairs(?string $key, ?string $value)Vrátí asociativní pole klíč→hodnota
getSingleResult(?Closure $callback)Vrátí jeden záznam (pole) nebo null
getSingleScalarResult()Vrátí jednu hodnotu (string/int) nebo null
getSingleResultOrFail(?Closure $fail, ?Closure $row)Jako getSingleResult, ale vyhodí RuntimeException pokud nenajde
getCount()SELECT COUNT(*) — vrátí int
getSum(string $column, $coalesceValue = null)SELECT SUM(column) — vrátí float. S coalesce vrátí default hodnotu místo null.
exists()SELECT 1 LIMIT 1 — vrátí bool, zda existuje alespoň jeden záznam

FOR UPDATE (transakce)

getResultForUpdate()SELECT ... FOR UPDATE — zamkne řádky
getSingleResultForUpdate()Jeden záznam s FOR UPDATE
getSingleScalarResultForUpdate()Jedna hodnota s FOR UPDATE

Zápis a mazání

update(array $data, bool $force = false)UPDATE s kritérii. Bez kritérií vyhodí výjimku (ochrana).
multipleUpdates(string $idColumn, array $data)Hromadný UPDATE pomocí CASE WHEN — efektivní pro update více řádků najednou
delete(?Closure $returningCallback, bool $force = false)DELETE s kritérii. S callback vrátí smazané řádky (RETURNING *).

Cache

useAPCuCache(int $ttl = 3600)Cachuje výsledek v APCu (lokální cache)
useMemcachedCache(int $ttl = 3600)Cachuje výsledek v Memcached (sdílená cache)

Debug

print(bool $forceExit = true)Vypíše SQL dotaz (pro debugging)
getSql(bool $first = false, bool $forUpdate = false)Vrátí SQL string bez jeho spuštění

Kompletní příklad

$results = OrdersRepository::queryBuilder()
    ->setCriteria([
        ['[orders].[status] IN (?)', ['active', 'pending']],
        ['[orders].[created_at] > ?', '2026-01-01']
    ])
    ->join('LEFT JOIN users ON users.id = orders.user_id')
    ->setColumns(['orders.*', 'users.email'])
    ->order(['created_at' => 'DESC'])
    ->setPagination($page, $perPage)
    ->getResult();

// Počet a součet
$count = OrdersRepository::queryBuilder()
    ->setCriteria(['status' => 'active'])
    ->getCount();

$total = OrdersRepository::queryBuilder()
    ->setCriteria(['user_id' => User::getId()])
    ->getSum('amount', 0);

// Existuje?
$hasOrders = OrdersRepository::queryBuilder()
    ->setCriteria(['user_id' => User::getId()])
    ->exists();

// S cache (5 minut APCu)
$topProducts = ProductsRepository::queryBuilder()
    ->order(['sales' => 'DESC'])
    ->limit(10)
    ->useAPCuCache(300)
    ->getResult();

// Hromadný update
OrdersRepository::queryBuilder()
    ->multipleUpdates('id', [
        42 => ['status' => 'completed', 'completed_at' => UTC::get()],
        43 => ['status' => 'cancelled'],
        44 => ['status' => 'completed', 'completed_at' => UTC::get()],
    ]);