plugins/nette/skills/nette-forms/SKILL.md
Invoke before creating or modifying Nette Forms. Provides form controls, validation, rendering patterns. Use when working with form factories, form controls, validation rules, form events, rendering forms in Latte, Bootstrap integration, form containers, or form error handling. Also trigger for $form/$data in Nette context.
npx skillsauth add nette/claude-code nette-formsInstall 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.
Nette Forms provides secure, reusable forms with automatic validation on both client and server side.
composer require nette/forms
Forms are created in factory methods named createComponent<Name>:
protected function createComponentRegistrationForm(): Form
{
$form = new Form;
$form->addText('name', 'Name:')
->setRequired('Please enter your name.');
$form->addEmail('email', 'Email:')
->setRequired('Please enter your email.');
$form->addPassword('password', 'Password:')
->setRequired('Please enter password.')
->addRule($form::MinLength, 'Password must be at least %d characters', 8);
$form->addSubmit('send', 'Register');
$form->onSuccess[] = $this->registrationFormSucceeded(...);
return $form;
}
private function registrationFormSucceeded(Form $form, \stdClass $data): void
{
// $data->name, $data->email, $data->password
$this->flashMessage('Registration successful.');
$this->redirect('Home:');
}
Render in template:
{control registrationForm}
Unified form for both creating and editing records:
class ProductPresenter extends BasePresenter
{
public function __construct(
private ProductFacade $facade,
) {}
public function actionEdit(int $id = null): void
{
if ($id) {
$product = $this->facade->getProduct($id);
$this->template->product = $product;
$this['productForm']->setDefaults($product->toArray());
}
}
protected function createComponentProductForm(): Form
{
$form = new Form;
$form->addText('name', 'Name:')
->setRequired();
$form->addTextArea('description', 'Description:');
$form->addInteger('price', 'Price:')
->setRequired()
->addRule($form::Min, 'Price must be positive', 1);
$form->addSubmit('send', 'Save');
$form->onSuccess[] = $this->productFormSucceeded(...);
return $form;
}
private function productFormSucceeded(Form $form, \stdClass $data): void
{
$id = $this->getParameter('id');
if ($id) {
$this->facade->update($id, (array) $data);
$this->flashMessage('Product updated.');
} else {
$this->facade->create((array) $data);
$this->flashMessage('Product created.');
}
$this->redirect('default');
}
}
Template:
{block content}
<h1>{if $product}Edit: {$product->name}{else}New Product{/if}</h1>
{form productForm}
<table>
<tr><td>{label name}{input name}</td></tr>
<tr><td>{label description}{input description}</td></tr>
<tr><td>{label price}{input price}</td></tr>
</table>
{input send}
{/form}
{/block}
Defaults are set in actionEdit() via $form->setDefaults() – never set defaults in the template.
A common base FormFactory creates Form instances with shared configuration (CSRF, renderer, translation). Changing it in one place affects all forms in the application – no need to hunt through individual presenters:
final class FormFactory
{
public function create(): Form
{
$form = new Form;
// Shared setup for all forms: renderer, translator, default classes, etc.
return $form;
}
}
For specific forms used in multiple places, create a dedicated factory:
final class ProductFormFactory
{
public function __construct(
private FormFactory $formFactory,
) {}
public function create(callable $onSuccess): Form
{
$form = $this->formFactory->create();
$form->addText('name', 'Name:')
->setRequired();
$form->addTextArea('description', 'Description:');
$form->addInteger('price', 'Price:')
->setRequired();
$form->addSubmit('send');
$form->onSuccess[] = function (Form $form, \stdClass $data) use ($onSuccess) {
$onSuccess($data);
};
return $form;
}
}
Use in presenter:
public function __construct(
private ProductFormFactory $productFormFactory,
) {}
protected function createComponentProductForm(): Form
{
return $this->productFormFactory->create(
function (\stdClass $data): void {
$this->facade->save($data);
$this->redirect('default');
},
);
}
| Method | Creates |
|--------|---------|
| addText($name, $label) | Text input |
| addPassword($name, $label) | Password input |
| addTextArea($name, $label) | Multi-line text |
| addEmail($name, $label) | Email with validation |
| addInteger($name, $label) | Integer input |
| addFloat($name, $label) | Decimal input |
| addCheckbox($name, $caption) | Checkbox |
| addCheckboxList($name, $label, $items) | Multiple checkboxes |
| addRadioList($name, $label, $items) | Radio buttons |
| addSelect($name, $label, $items) | Dropdown |
| addMultiSelect($name, $label, $items) | Multi-select |
| addUpload($name, $label) | File upload |
| addMultiUpload($name, $label) | Multiple files |
| addDate($name, $label) | Date picker |
| addTime($name, $label) | Time picker |
| addDateTime($name, $label) | Combined date+time picker |
| addHidden($name) | Hidden field |
| addSubmit($name, $caption) | Submit button |
| addButton($name, $caption) | Button |
See the complete control reference for all form controls.
$form->addText('name')
->setRequired('Name is required.')
->addRule($form::MinLength, 'At least %d characters', 3)
->addRule($form::MaxLength, 'Maximum %d characters', 100);
$form->addEmail('email')
->setRequired()
->addRule($form::Email, 'Invalid email format.');
$form->addInteger('age')
->addRule($form::Range, 'Age must be between %d and %d', [18, 120]);
$form->addPassword('password')
->setRequired()
->addRule($form::MinLength, 'At least %d characters', 8);
$form->addPassword('password2')
->setRequired()
->addRule($form::Equal, 'Passwords must match', $form['password']);
See the complete validation reference for all rules and conditions.
$form->addCheckbox('newsletter', 'Subscribe to newsletter');
$form->addEmail('email')
->addConditionOn($form['newsletter'], $form::Equal, true)
->setRequired('Email required for newsletter.');
Default rendering:
{control productForm}
Manual rendering:
{form productForm}
<table>
<tr>
<th>{label name /}</th>
<td>{input name} {inputError name}</td>
</tr>
<tr>
<th>{label email /}</th>
<td>{input email} {inputError email}</td>
</tr>
</table>
{input send}
{/form}
With Bootstrap:
{form productForm class => 'form-horizontal'}
<div class="mb-3">
{label name class => 'form-label' /}
{input name class => 'form-control'}
{inputError name class => 'invalid-feedback'}
</div>
{/form}
See the complete rendering reference for all rendering options.
// Before rendering (modify form)
$form->onRender[] = function (Form $form): void {
// Add CSS classes, modify controls
};
// After successful validation
$form->onSuccess[] = function (Form $form, \stdClass $data): void {
// Process valid data
};
// After any submission (valid or invalid)
$form->onSubmit[] = function (Form $form): void {
// Logging, analytics
};
// Custom validation after rules pass
$form->onValidate[] = function (Form $form): void {
if ($this->isBlocked($form->getValues()->email)) {
$form->addError('This email is blocked.');
}
};
private function productFormSucceeded(Form $form, \stdClass $data): void
{
try {
$this->facade->save($data);
$this->redirect('default');
} catch (DuplicateEntryException) {
$form['email']->addError('Email already exists.');
} catch (\Exception $e) {
$form->addError('An error occurred.');
}
}
createComponent* factory. Nette lazy-creates components, so the form only builds when actually needed.$form->setDefaults() in action* method. Template manipulation of form state breaks separation of concerns.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.