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()],
]);
