plugins/nette/skills/nette-database/SKILL.md
Invoke before writing database queries or working with Selection API, ActiveRow in Nette. Use when creating entity classes, configuring database connections, writing queries, fetching data, using joins, or designing Row classes. Also consult when deciding between Selection API and raw SQL, or setting up database configuration in .neon files.
npx skillsauth add nette/claude-code nette-databaseInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Uses Nette Database, typically with MySQL, PostgreSQL or SQLite as the backend.
composer require nette/database
See the Explorer API reference for the full ActiveRow/Selection API. See the SQL query reference for direct SQL queries.
user not users)id for primary keysutf8mb4 with appropriate collation (e.g. utf8mb4_0900_ai_ci, or utf8mb4_cs_0900_ai_ci for Czech)created_at DATETIME DEFAULT CURRENT_TIMESTAMPNette Database has a built-in EntityMapping interface (since 3.2) that the Explorer uses to resolve an ActiveRow subclass for each table and (optionally) translate between column names and PHP property names. The default implementation (DefaultEntityMapping) supports:
special_table), wildcard patterns (forum_*) or a bare * catch-all. Class names may contain *, which is replaced with the PascalCase of the captured portion: forum_post + App\Forum\*Row → App\Forum\PostRow. A class without * is a fixed class (log_*: App\Logging\LogRow maps every log_* table to the same LogRow). Schema prefixes like public. are stripped before PascalCase conversion. Exact keys take precedence; wildcard entries are tried in declaration order, so put more specific patterns first and the bare * last.author_id is exposed as property authorId everywhere: property access, iteration, toArray(), insert(), update(), and column references in where() / order().Activate via the mapping config option (see Database Configuration below).
All entities in App\Entity with consistent Row suffix (matches the App\Entity\*Row convention):
product table → ProductRoworder_item table → OrderItemRowvariant_expiration table → VariantExpirationRowWhy flat: Entities are data structures that cross domain boundaries. A ProductRow might be used in catalog, orders, inventory, and reporting contexts. Subdividing entities by domain forces you to either pick one arbitrary "home" domain or duplicate references.
All entities in single App\Entity namespace - avoid domain subdivision:
app/Entity/
├── ProductRow.php ← Core business entities
├── OrderItemRow.php ← Relationship entities
└── StockTransferRow.php ← Operational entities
use Nette\Database\Table;
/**
* @property-read int $id
* @property-read string $title
* @property-read bool $active
* @property-read ?CategoryRow $category ← nullable relationship
* @property-read UserRow $author ← required relationship
*/
final class ProductRow extends Table\ActiveRow
{
}
Documentation rules:
Foreign key patterns:
@property-read ?CategoryRow $category for optional relationships@property-read UserRow $author for required relationships@property-read Selection<OrderItemRow> $order_items for back-referencesNaming convention: Follow Nette Database relationship naming (foreign key without _id suffix).
With camelCase: true mapping: scalar columns map to camelCase properties ($firstName, $createdAt). Relationship property names ($author, $category) are resolved by Conventions against database columns, so they aren't affected by the camelCase setting.
Use for:
return $this->db->table('product')
->where('active', true)
->where('category_id', $categoryId)
->order('name');
Use for:
return $this->db->query('
WITH RECURSIVE category_tree AS (...)
SELECT ...
', $params)->fetchAll();
Build queries by progressive refinement – start with a base method, then add conditions. Always use generic types for Selection returns:
/** @return Selection<ProductRow> */
public function getProducts(): Selection
{
return $this->db->table('product');
}
/** @return Selection<ProductRow> */
public function getActiveProducts(): Selection
{
return $this->getProducts()->where('active', true);
}
/** @return Selection<ProductRow> */
public function getProductsInCategory(int $categoryId): Selection
{
return $this->getActiveProducts()
->where(':product_category.category_id', $categoryId);
}
Benefits: Reusable base queries, clear evolution of filtering logic, easy testing. Full IDE support, type safety, clear contracts.
Use colon notation for efficient joins:
// Forward relationship (via foreign key)
->where('category.slug', $categorySlug)
// Back-reference (reverse relationship)
->where(':order_item.quantity >', 1)
// Deep relationships
->where('category.parent.name', 'Root Category')
Single optional result: ->fetch()
All results as array: ->fetchAll()
Key-value pairs: ->fetchPairs('key_column', 'value_column')
Single scalar value: ->fetchField() (first column of first row)
Count only: ->count('*')
Structured data with fetchAssoc:
// Key by column value
$byId = $db->table('product')->fetchAssoc('id');
// [1 => ProductRow, 2 => ProductRow, ...]
// Group by column
$byCategory = $db->table('product')->fetchAssoc('category_id[]');
// [5 => [ProductRow, ProductRow], 8 => [ProductRow, ...]]
// Nested grouping
$nested = $db->table('product')->fetchAssoc('category_id|active');
// [5 => [true => ProductRow, false => ProductRow], ...]
The path string uses [] for array grouping, | for nested keys, and = to extract a single value.
Use direct SQL migrations rather than ORM-style migrations – store schema in sql/db.sql with manual migration scripts. Rely on database constraints (foreign keys, unique, check) for data integrity and handle constraint violations in services with meaningful business exceptions.
Wrap multi-step writes in transactions to ensure consistency:
$this->db->transaction(function () use ($data, $items) {
$order = $this->db->table('order')->insert($data);
foreach ($items as $item) {
$order->related('order_item')->insert($item);
}
});
The callback approach automatically commits on success and rolls back on exception.
Don't create separate Repository classes – in Nette, services combine data access with business logic. A separate repository layer adds indirection without benefit because Nette Database Explorer already provides a clean query API. The service IS the repository.
Don't use Selection API for complex queries – raw SQL is cleaner for analytics, reporting, and recursive queries. Selection API excels at CRUD and simple filtering; forcing complex JOINs through it creates hard-to-read code.
Don't fetch more data than needed – use appropriate fetching methods (fetchPairs for dropdowns, count('*') for pagination) and SELECT only required columns for large datasets.
Transform database errors to business exceptions:
try {
$customer->update(['email' => $newEmail]);
} catch (Nette\Database\UniqueConstraintViolationException) {
throw new EmailAlreadyExistsException();
}
Handle at service boundary – presenters should receive business exceptions, not database exceptions. This keeps the UI layer independent of the storage layer and produces meaningful error messages.
database:
dsn: 'mysql:host=127.0.0.1;dbname=myapp'
user: root
password: secret
# Entity mapping (table → ActiveRow subclass, optional column ↔ property)
mapping:
tables: App\Entity\*Row # string shortcut: equivalent to { '*': 'App\Entity\*Row' }
options:
lazy: true # Connect on first query
charset: utf8mb4 # Default
convertBoolean: true # TINYINT(1) to bool
newDateTime: true # Return DateTimeImmutable
Full mapping form when you need explicit overrides or camelCase properties:
database:
dsn: 'mysql:host=127.0.0.1;dbname=myapp'
mapping:
tables:
special_table: App\Entity\SpecialRow # exact match wins over wildcards
forum_*: App\Forum\*Row # wildcard in key (* captured as PascalCase)
"*": App\Entity\*Row # catch-all fallback (must be last)
camelCase: true # column author_id ↔ property authorId
Multiple connections:
database:
main:
dsn: 'mysql:host=127.0.0.1;dbname=app'
user: root
password: secret
logs:
dsn: 'mysql:host=127.0.0.1;dbname=logs'
user: logs
password: secret
autowired: false # Must reference explicitly
Reference non-autowired connection:
services:
- LogService(@database.logs.connection)
For detailed information, use WebFetch on these URLs:
tools
CRITICAL: Read BEFORE writing or modifying any PHP file. A PostToolUse hook automatically runs nette/coding-standard (ECS) on every PHP file after each Edit or Write. The fixer removes unused `use` statements - so never add `use` statements in a separate edit before the code that references them. Always include `use` imports in the same edit as the referencing code, or add the code first then `use` statements. This skill should be used whenever creating new PHP files, editing existing PHP code, adding methods, refactoring, or fixing bugs in PHP - even for small one-line changes.
development
Install nette/coding-standard globally for PHP code style checking
tools
Invoke when fetching web pages from localhost, debugging PHP errors, or interpreting Tracy output (BlueScreen, Tracy Bar, dump). Read BEFORE running curl or Chrome to any local development PHP URL – with Tracy >= 2.12 and a detected agent, Tracy mirrors BlueScreen, Tracy Bar and dumps as markdown into the JS console for easy machine reading. For Chrome MCP, call list_console_messages() to read Tracy output. Essential when: 500 error, blank page, PHP exception, slow page, N+1 queries, or inspecting variables with dump().
tools
Provides Nette Utils helper classes. Use when working with Arrays, Strings, Image, Finder, FileSystem, Json, Validators, DateTime, Html element builder, Random, Callback, Type, or SmartObject from nette/utils. Do NOT use for Nette Schema, Nette Forms, Nette Database, Latte filters, or DI configuration.