A CLI code generator for Nette Framework. Scaffold presenters, models, repositories, services, Latte templates, and database migrations with a single command — without touching boilerplate ever again.
| Dependency | Version |
|---|---|
| PHP | >= 7.4 |
| symfony/console | ^5.0 || ^6.0 || ^7.0 |
| nette/php-generator | ^3.5 || ^4.0 |
| nette/neon | ^3.3 || ^4.0 |
| doctrine/inflector | ^2.0 |
Optional:
nette/diis required only when integrating viaMakerExtensionin your Nette DI config.
composer require unquam/nette-makerSince this package is a Composer Plugin, during installation Composer will ask:
Do you trust "unquam/nette-maker" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?]
Press y to allow. The plugin will automatically create two files in your project root:
| File | Purpose |
|---|---|
nette |
Executable PHP runner — run php nette <command> |
nette-maker.neon |
Configuration file (database credentials, migrations path) |
If you pressed n, create the runner manually:
cp vendor/bin/nette-maker nette
chmod +x nette
php nette make:initEdit nette-maker.neon in your project root:
# nette-maker.neon
database:
dsn: 'mysql:host=127.0.0.1;dbname=your_database'
user: root
password: ''
migrations:
directory: db/migrations| Key | Type | Description |
|---|---|---|
database.dsn |
string |
PDO DSN — driver is auto-detected from the prefix (mysql, pgsql, sqlite, sqlsrv) |
database.user |
string |
Database username |
database.password |
string |
Database password |
migrations.directory |
string |
Path relative to the config file where migration files are stored (default: db/migrations) |
All commands are available through the php nette runner or through vendor/bin/nette-maker.
php nette <command> [arguments] [options]| Command | Description |
|---|---|
make:init |
Create the default nette-maker.neon config file |
make:presenter <Name> |
Generate a Presenter class |
make:model <Name> |
Generate a Model class |
make:repository <Name> |
Generate a Repository class |
make:service <Name> |
Generate a Service class |
make:latte <Name> |
Generate a Latte template |
make:request <Module/Name> |
Generate an API Form Request validation class |
make:request <Module/Name> --web |
Generate a Web Frontend Form Request class |
make:test <Module/Name> |
Generate a Nette Tester test class |
make:test <Module/Name> --phpunit |
Generate a PHPUnit test class |
make:module <Name> |
Generate a full module (all of the above) |
make:auth |
Scaffold full authentication system |
make:resource <Name> |
Generate a JSON API resource transformer |
make:seeder <Name> |
Generate a database seeder class |
make:factory <Name> |
Generate a database factory class |
make:migration <Name> |
Generate a migration file |
migrate |
Run pending migrations |
migrate --rollback |
Roll back all ran migrations |
migrate --status |
Show migration status |
migrate:fresh |
Drop all tables and re-run all migrations |
migrate:fresh --seed |
Drop all tables, re-run migrations and seed |
db:seed |
Run all database seeders |
db:seed --class=Name |
Run a specific seeder class |
db:wipe |
Drop all database tables |
clear:cache |
Clear application cache directories |
Creates nette-maker.neon in the project root.
php nette make:initIf the file already exists the command exits with a warning and does nothing.
Generates a Presenter class in app/Presentation/<Name>/<Name>Presenter.php.
php nette make:presenter Article
# → app/Presentation/Article/ArticlePresenter.phpGenerated class:
<?php
declare(strict_types=1);
namespace App\Presentation\Article;
use Nette\Application\UI\Presenter;
final class ArticlePresenter extends Presenter
{
public function renderDefault(): void
{
}
}Generates a Model class in app/Model/<Name>.php that uses Nette\Database\Explorer.
php nette make:model Article
# → app/Model/Article.phpGenerated class:
<?php
declare(strict_types=1);
namespace App\Model;
use Nette\Database\Explorer;
final class Article
{
public function __construct(private Explorer $explorer)
{
}
}Generates a Repository class in app/Model/Repositories/<Name>Repository.php.
php nette make:repository Article
# → app/Model/Repositories/ArticleRepository.phpGenerated class includes findAll(): Selection, findById(int $id), create(array $data), update(int $id, array $data): bool and delete(int $id): bool methods pre-wired to the correct database table via a private TABLE constant.
Generates a Service class in app/Model/Services/<Name>Service.php, pre-injecting the corresponding Repository.
php nette make:service Article
# → app/Model/Services/ArticleService.phpGenerates an empty Latte template at app/Presentation/<Name>/default.latte.
php nette make:latte Article
# → app/Presentation/Article/default.latteEncapsulate your form input validation logic inside standalone, highly testable Request classes structured beautifully within your Feature Folders.
By default, it generates an API-specific request class (inside the Api/Requests/{Module} namespace):
php nette make:request Article/StoreTo generate a Web Frontend specific request class (inside the Requests/{Module} namespace):
php nette make:request User/Update --webDefine your validation rules using core constraints (required, nullable, sometimes, string, integer, numeric, boolean, array, email, email:rfc, email:dns, email:rfc,dns, url, min:n, max:n, min_length:n, max_length:n, in:a,b, not_in:a,b, regex:/pattern/, confirmed, date, date_format:Y-m-d, before:date, after:date, alpha, alpha_num, alpha_dash, digits:n, digits_between:a,b, between:a,b, ip, ipv4, ipv6, uuid, json, accepted, declined, filled, present, prohibited, size:kb, mimetypes:types).
Note: Use
email:dnsoption with caution in high-traffic production environments, as DNS lookups introduce synchronous network latency.
Rules can be defined as a pipe-separated string or as an array:
// String format
'email' => 'required|email:rfc,dns|max_length:255',
// Array format
'email' => ['required', 'email:rfc,dns', 'max_length:255'],<?php
declare(strict_types=1);
namespace App\Presentation\Api\Requests\Article;
use Unquam\NetteMaker\Requests\FormRequest;
class StoreRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => 'required|string|min_length:5',
'content' => 'required|string',
'email' => ['required', 'email:rfc,dns', 'max_length:255'],
];
}
public function messages(): array
{
return [
'title.required' => 'Název článku je povinný.',
'title.min_length' => 'Název musí mít alespoň :min znaků.',
];
}
}...
The RuleValidator uses dynamic placeholder tokens (:field, :min, :max, :values). You can easily return error messages in Czech (or any other language) by overriding them directly in the messages() method of your request class, or by passing translated strings through your architecture.
If you override messages for Czech localization:
public function messages(): array
{
return [
'email.required' => 'E-mailová adresa je povinné pole.',
'email.email' => 'Zadejte prosím platnou e-mailovou adresu.',
'password.min_length' => 'Heslo musí mít alespoň :min znaků.',
];
}public function actionCreate(): void
{
try {
$request = new \App\Presentation\Api\Requests\Article\StoreRequest($this->getHttpRequest());
$validatedData = $request->validate(); // Safe, explicit verification loop $this->model->create($validatedData); $this->sendJson(['status' => 'success']);
} catch (\Unquam\NetteMaker\Exceptions\ValidationException $e) {
$this->getHttpResponse()->setCode($e->getCode());
$this->sendJson([
'message' => 'The given data was invalid.',
'errors' => $e->getErrors()
]);
}
}Czech API Validation Failure Output (422 Client Error):
{
"message": "The given data was invalid.",
"errors": {
"email": "E-mailová adresa je povinné pole.",
"password": "Heslo musí mít alespoň 6 znaků."
}
}public function actionSave(): void
{
try {
$request = new \App\Presentation\Requests\User\UpdateRequest($this->getHttpRequest());
$validatedData = $request->validate();
$this->model->save($validatedData);
$this->flashMessage('Profil byl úspěšně aktualizován!', 'success');
$this->redirect('User:default');
} catch (\Unquam\NetteMaker\Exceptions\ValidationException $e) {
// Handle failed states via standard Nette flash messages and redirect back
foreach ($e->getErrors() as $errorText) {
$this->flashMessage($errorText, 'danger');
}
$this->redirect('this');
}
}Pass the Explorer instance as second constructor argument to enable database-backed rules:
public function rules(): array
{
return [
'email' => 'required|email|unique:users,email',
'role_id' => 'required|integer|exists:roles,id',
];
}Usage in Presenter:
public function actionCreate(): void
{
try {
$request = new StoreUserRequest($this->getHttpRequest(), $this->explorer);
$validated = $request->validate();
$this->model->create($validated);
$this->sendJson(['status' => 'success']);
} catch (\Unquam\NetteMaker\Exceptions\ValidationException $e) {
$this->getHttpResponse()->setCode($e->getCode());
$this->sendJson(['errors' => $e->getErrors()]);
}
}If
Exploreris not passed,uniqueandexistsrules are silently skipped.
Generate test classes for Nette Tester (default) or PHPUnit. The command automatically analyzes the target class using reflection and scaffolds test methods for all its public actions.
💡 Note: To run default tests, make sure you have
nette/testerin your dev dependencies.
# Generate Nette Tester class (creates tests/Unit/Services/UserServiceTest.phpt)
php nette make:test Services/UserService
# Generate PHPUnit class (creates tests/Unit/Services/UserServiceTest.php)
php nette make:test Services/UserService --phpunitIf you run the command without arguments, it will ask for the class name interactively:
php nette make:testIf your project has a deep folder structure (e.g., app/Model/Security/Authenticator.php), just pass the path relative to your app directory:
php nette make:test Model/Security/AuthenticatorThe tool will dynamically build the correct nested test structure:
- Target Location:
tests/Unit/Model/Security/AuthenticatorTest.phpt - Smart Imports: Automatically generates
use App\Model\Security\Authenticator; - Bootstrap Resolution: Dynamically calculates directory depth (
require __DIR__ . '/../../../bootstrap.php';)
- Method Auto-Scaffolding: Reads public methods from your class and generates matching
testMethodName()boilerplate blocks. - Smart Namespaces: Dynamically detects your dev-dependencies autoloading settings from
composer.json. - Zero Config Setup: Automatically creates a fully functional
tests/bootstrap.phpfor Nette Tester if it's missing in your project.
Scaffolds a complete module in one command: Presenter, Model, Repository, Service, Migration, and Latte template.
php nette make:module ArticleAll parts are optional. Skip specific ones with --no-* flags:
php nette make:module Article --no-migration --no-latte
php nette make:module Article --no-service --no-repository
php nette make:module Article --no-presenter --no-modelGenerate only specific parts with --only:
php nette make:module Article --only=presenter,model
php nette make:module Article --only=migration
php nette make:module Article --only=presenter,model,repository,serviceComma-separated values accepted: presenter, model, repository, service, migration, latte.
Scaffold a fully operational authentication, registration, and logout system out-of-the-box. It automatically generates a secure database schema migration table script, custom security Authenticator model services compliant with Nette Security standards, controller presenter forms logic handlers, and responsive front-end view templates layouts:
php nette make:authdb/migrations/%timestamp%_create_users_table.php— Secure table structure handling password storage hashing hashes.app/Model/Security/Authenticator.php— DB credentials comparison core evaluating identities rules.app/Presentation/Sign/SignPresenter.php— Controller factories mapping login (signInForm), registration (signUpForm), and clean session logouts (actionOut).app/Presentation/Sign/in.latte&up.latte— Styled UI templates interfaces ready to serve layout pages.
Once generated, simply wire up the fresh class service structure boundary mapping inside your core Nette application DI container tracking block configuration setup (config.neon layout configuration file):
services:
- App\Model\Security\AuthenticatorAfterward, instantly run your migrations schema setup to provision your backend database tracking table structures allocation layouts:
php nette migrateTransform your database models into secure, structured JSON layers with native support for Nette Framework pagination.
To generate a single item transformer resource (extends JsonResource):
php nette make:resource UserTo generate a paginated resource collection transformer (extends ResourceCollection):
php nette make:resource UserCollectionSafely filter your database fields inside the toArray() block to prevent sensitive credentials leaks:
<?php
declare(strict_types=1);
namespace App\Presentation\Api\Resources;
use Unquam\NetteMaker\Resources\JsonResource;
class UserResource extends JsonResource
{
public function toArray(): array
{
return [
'id' => (int) $this->resource->id,
'email' => (string) $this->resource->email,
];
}
}Define which single item resource class should map the nesting loop:
<?php
declare(strict_types=1);
namespace App\Presentation\Api\Resources;
use Unquam\NetteMaker\Resources\ResourceCollection;
class UserCollection extends ResourceCollection
{
protected function collectWith(): string
{
return UserResource::class;
}
}$user = $this->explorer->table('users')->get(1);
$this->sendJson(UserResource::make($user));Natively maps Nette Database query streams (Selection) combined with pagination settings out-of-the-box:
// Select records for page 2, limiting to 15 entries per page
$users = $this->explorer->table('users')->page(2, 15);
// Automatically injects structured data and pagination meta hashes
$this->sendJson(UserCollection::make($users));JSON Output Format:
{
"data": [
{ "id": 16, "email": "user16@test.com" },
{ "id": 17, "email": "user17@test.com" }
],
"meta": {
"current_page": 2,
"per_page": 15,
"last_page": 4,
"total": 52,
"from": 16,
"to": 30
}
}If you need to protect these generated JSON endpoints with lightweight, secure bearer access/refresh tokens, use our native companion API package:
It provides full token life-cycle management, strict CORS controls, and built-in rate-limiting filters out-of-the-box.
Generates a database seeder class stub.
php nette make:seeder UserSeederConfigure the seeders directory in nette-maker.neon:
seeders:
directory: db/seedersGenerate powerful blueprint schemas for your database records to simplify seeding and testing.
php nette make:factory UserConfigure your custom factories lookup directory path inside nette-maker.neon:
factories:
directory: db/factoriesEach generated factory is a structured PHP class extending AbstractFactory. You can easily map default attributes using standard PHP or external libraries like Faker:
<?php
declare(strict_types=1);
use Unquam\NetteMaker\Migration\AbstractFactory;
class UserFactory extends AbstractFactory
{
protected function defineTable(): string
{
return 'users';
}
protected function definition(): array
{
// $faker = \Faker\Factory::create();
return [
'name' => 'John Doe',
'email' => 'user_' . uniqid() . '@example.com',
'role' => 'user',
'created_at' => date('Y-m-d H:i:s'),
];
}
}Since generated blueprints are native PHP classes, you get complete IDE autocompletion. Simply instantiate the factory inside your seeders context loop using a standard object constructor:
<?php
declare(strict_types=1);
return new class
{
public function run(\PDO $pdo): void
{
// Require the generated factory class layout
require_once dirname(__DIR__) . '/factories/UserFactory.php';
$factory = new UserFactory($pdo);
// Fluent interface: instantly seed exactly 50 default users!
$factory->count(50)->create();
// Or create separate entries while seamlessly overriding default values
$factory->count(2)->create([
'role' => 'admin',
]);
}
};Generates a timestamped migration file in the configured migrations directory.
php nette make:migration CreateArticlesTable
# → db/migrations/2026_05_18_120000_create_articles_table.phpGenerated migration:
<?php
declare(strict_types=1);
use Unquam\NetteMaker\Migration\TableBuilder;
return new class
{
public function up(TableBuilder $builder): void
{
$builder->create('{{table}}', function (TableBuilder $table): void {
$table->id();
// Available column types:
// $table->string('title');
// $table->string('slug', 191);
// $table->text('body');
// $table->integer('views');
// $table->bigInteger('score');
// $table->boolean('is_active');
// $table->float('rating');
// $table->decimal('price', 10, 2);
// $table->timestamp('published_at');
// $table->timestamps();
// Modifiers (chain after a column):
// $table->string('email')->nullable();
// $table->string('role')->default('user');
// $table->string('email')->unique();
// $table->string('name')->after('id'); // MySQL/MariaDB only
// Indexes:
// $table->index('user_id');
// $table->index(['user_id', 'status']);
// Foreign keys:
// $table->foreign('user_id', 'users')->cascadeOnDelete();
// $table->foreign('user_id', 'users', 'id', null, 'SET NULL', 'RESTRICT');
// Composite primary key:
// $table->primary(['user_id', 'role_id']);
$table->timestamps();
});
}
public function down(TableBuilder $builder): void
{
$builder->drop('{{table}}');
// To alter an existing table instead of dropping:
// $builder->table('{{table}}', function (TableBuilder $table): void {
// $table->dropColumn('email');
// $table->dropIndex('idx_{{table}}_email');
// $table->dropForeign('fk_{{table}}_user_id');
// $table->dropPrimary();
// });
}
};$builder->create('table_name', function (TableBuilder $table): void {
$table->id(); // Auto-increment primary key
$table->string('title'); // VARCHAR(255) NOT NULL
$table->string('slug', 191); // VARCHAR(191) NOT NULL
$table->text('body'); // TEXT NOT NULL
$table->integer('views'); // INT NOT NULL
$table->bigInteger('score'); // BIGINT NOT NULL
$table->boolean('is_published'); // BOOLEAN/TINYINT NOT NULL
$table->float('rating'); // FLOAT NOT NULL
$table->decimal('price', 10, 2); // DECIMAL(10,2) NOT NULL
$table->timestamp('published_at'); // TIMESTAMP NULL
$table->timestamps(); // created_at + updated_at
// Modifiers (chain after a column):
$table->string('email')->nullable();
$table->string('role')->default('user');
$table->string('email')->unique();
$table->string('name')->after('id'); // MySQL/MariaDB only
// Indexes:
$table->index('user_id');
$table->index(['user_id', 'status']);
$table->index(['user_id', 'status'], 'custom_index_name');
// Foreign keys:
$table->foreign('user_id', 'users');
$table->foreign('user_id', 'users')->cascadeOnDelete();
$table->foreign('user_id', 'users', 'id', null, 'SET NULL', 'RESTRICT');
// Composite primary key:
$table->primary(['user_id', 'role_id']);
});
// Alter existing table:
$builder->table('table_name', function (TableBuilder $table): void {
$table->dropColumn('email');
$table->dropIndex('idx_table_email');
$table->dropForeign('fk_table_user_id');
$table->dropPrimary();
});
$builder->drop('table_name');
$builder->dropIfExists('table_name');Supported database drivers: mysql, mariadb, pgsql/postgres, sqlite, sqlsrv/mssql.
Run all pending migrations:
php nette migrateShow migration status:
php nette migrate --statusRoll back all ran migrations:
php nette migrate --rollbackDrops all database tables completely and re-runs all migration scripts sequentially from scratch. This provides a fresh starting state for local development:
php nette migrate:freshYou can automatically run database seeders right after resetting your database schema using the --seed (or -s) shortcut flag:
php nette migrate:fresh --seed
# or shortcut notation format
php nette migrate:fresh -sRun all available seeders alphabetically:
php nette db:seedRun a specific seeder class directly using the --class (or -c) option:
php nette db:seed --class=UserSeeder
# or shortcut notation format
php nette db:seed -c UserSeederCompletely drops all tables from the database without running down() migration methods. It safely disables foreign key constraints internally during the process:
php nette db:wipeSafely clears application cache and maintenance directories.
php nette clear:cacheBy default, it targets the temp/ folder. It uses smart isolation: inside the standard Nette temp/ directory, it removes only the compiled configuration and templates (temp/cache/ and temp/proxies/), strictly ignoring temp/session/ so active web users won't log out.
You can configure multiple custom directories to be fully cleared inside your nette-maker.neon configuration array:
cache:
# List of directories to be cleared during clear:cache execution
directories:
- temp
# - log
# - www/assets/cacheAll code generation commands support a smart interactive mode. If you forget to provide the name argument, the CLI will prompt you for it in real-time:
php nette make:module
? Enter the name of the module (e.g. Article): Running the commands creates files in the following locations (all relative to your project root by default, or to the directory containing nette-maker.neon when using the nette runner):
app/
├── Model/
│ ├── Article.php # make:model
│ ├── Repositories/
│ │ └── ArticleRepository.php # make:repository
│ └── Services/
│ └── ArticleService.php # make:service
└── Presentation/
├── Article/
│ ├── ArticlePresenter.php # make:presenter
│ └── default.latte # make:latte
├── Api/
│ ├── Requests/
│ │ └── Article/
│ │ └── StoreRequest.php # make:request Article/Store
│ └── Resources/
│ └── UserResource.php # make:resource User
├── Requests/
│ └── User/
│ └── UpdateRequest.php # make:request User/Update --web
└── Sign/
├── SignPresenter.php # make:auth
├── in.latte # make:auth
└── up.latte # make:auth
db/
└── migrations/
└── 2026_05_18_120000_create_articles_table.php # make:migration
If your project uses nette/di, you can register all commands as services and wire them into your Symfony Console application via the DI extension.
extensions:
maker: Unquam\NetteMaker\DI\MakerExtension/** @var \Nette\DI\Container $container */
$app = $container->getByType(\Unquam\NetteMaker\Application::class);
exit($app->run());The extension registers every make:* command and the migrate command as individual DI services. This allows them to participate in standard DI autowiring.
Contributions are welcome! Please follow these steps:
- Fork the repository.
- Create a feature branch:
git checkout -b feat/my-feature. - Write tests for any new behaviour in
tests/. - Ensure the test suite passes:
composer test. - Follow PSR-12 coding standards.
- Open a pull request against the
mainbranch.
This package is open-sourced software licensed under the MIT licence.