Philosophy
The DotApp PHP Framework is built with modularity at its core. From the ground up, DotApp is designed to provide a robust and scalable foundation for modern web applications, prioritizing modular architecture to ensure flexibility, maintainability, and efficiency.
Why Modular Design?
Modularity is the heart of DotApp’s philosophy. By structuring applications as a collection of independent, reusable modules, DotApp enables developers to:
- Build scalable applications with clear separation of concerns.
- Reuse components across projects, reducing development time.
- Maintain and update specific parts of an application without affecting the whole system.
- Integrate new features or third-party tools seamlessly.
This approach ensures that your projects remain organized and adaptable, whether you're building a small prototype or a large-scale enterprise application.
Robust Foundation, Recommended Practices
DotApp provides a solid foundation with tools and conventions tailored for modular development. In this guide, we focus on the recommended practices that align with DotApp’s design goals:
- Module-Centric Workflow: Organize your application into self-contained modules for clarity and scalability.
- Consistent Structure: Follow DotApp’s conventions for controllers, templates, and configurations to streamline collaboration.
- Best Practices: Leverage built-in tools for routing, templating, and module management to avoid common pitfalls.
- Future-Proofing: Build with modularity to make future expansions or refactoring effortless.
While DotApp is flexible enough to support alternative approaches, this guide emphasizes the methods that best utilize its modular architecture. We aim to teach techniques that maximize the framework’s strengths and help you avoid inefficient or error-prone patterns.
What’s Next?
Ready to start building with DotApp? Head to the Installation section to set up the framework and begin your modular journey. For a deeper dive into creating your first module, check out the First Module & Setup section.
Proudly made in Slovakia 🇸🇰
Installation
Installing the DotApp PHP Framework is quick and flexible. Choose one of the three methods below to set up your project. Each method results in the same modular project structure, ready for development.
Option 1: Git Clone
If you have Git installed, you can clone the DotApp repository directly. Run the following command in your terminal:
git clone https://github.com/dotsystems-sk/dotapp.git ./
This creates a DotApp project in your current directory. Don’t have Git? No problem—try one of the other methods.
Option 2: DotApper CLI
Download the dotapper.php CLI tool and use it to install DotApp. Follow these steps:
- Download the file: dotapper.php.
- Save it to your project directory.
- Run the installation command:
php dotapper.php --install
This sets up DotApp with all necessary dependencies.
Option 3: ZIP Download
Prefer a manual approach? Download the DotApp ZIP file and extract it:
- Download the ZIP: DotApp main.zip.
- Extract the contents to your project directory.
Once extracted, your project is ready to use.
Project Structure
After installation, your project directory will have the following modular structure:
project-root/
├── index.php
├── dotapper.php
├── app/
│ ├── config.php
│ ├── modules/ # your application logic
│ │ └── HelloWorld/
│ │ ├── module.init.php
│ │ ├── module.listeners.php
│ │ ├── Controllers/
│ │ ├── Middleware/
│ │ ├── Models/
│ │ ├── views/
│ │ └── assets/
│ ├── parts/ # framework core — do not edit
│ ├── runtime/
│ └── vendor/
└── assets/
├── dotapp/
└── modules/
Application controllers, middleware, models, and views belong in app/modules/{ModuleName}/. app/parts/ is the framework core. Routes are declared in each module’s module.init.php.
What’s Next?
With DotApp installed, you’re ready to create your first module. Head to the First Module & Setup section to start building your modular application.
First Module & Setup
With the DotApp PHP Framework installed, you’re ready to create your first module. Modules are the core of DotApp’s modular architecture, allowing you to organize your application into reusable, self-contained components. In this section, we’ll create a HelloWorld module and configure it to serve /helloworld.
Creating the Module
Use the DotApper CLI to generate a new module. Run the following command in your project directory:
php dotapper.php --create-module=HelloWorld
You’ll see the output:
Module successfully created in: ./app/modules/HelloWorld
This creates a new HelloWorld module in the app/modules directory.
Module Structure
The HelloWorld module has the following structure:
├───modules
│ │ .gitkeep
│ │
│ └───HelloWorld
│ │ module.init.php
│ │ module.listeners.php
│ │
│ ├───Api
│ │ Api.php
│ │
│ ├───assets
│ │ howtouse.txt
│ │
│ ├───Controllers
│ │ Controller.php
│ │
│ ├───Libraries
│ ├───Middleware
│ ├───Models
│ ├───translations
│ └───views
│ │ clean.view.php
│ │
│ └───layouts
│ example.layout.php
Here’s what each file and directory is for:
module.init.php: Defines the module’s routes and initialization conditions, controlling when and how the module is loaded.module.listeners.php: Registers event listeners for the module, allowing it to respond to framework events like module loading.Api/Api.php: A sample API controller for building API endpoints (can be deleted or ignored).assets/: Stores module-specific assets like CSS, JavaScript, or images. Contains ahowtouse.txtguide for beginners.Controllers/Controller.php: A sample controller (can be deleted or ignored).Libraries/: Holds custom PHP libraries or classes specific to the module.Middleware/: Contains middleware classes for request processing, such as authentication or validation.Models/: Stores model classes for database interactions or business logic.translations/: Manages language files for internationalization.views/: Contains view templates, includingclean.view.php(a sample view) andlayouts/example.layout.php(a sample layout), both of which can be deleted or ignored.
The sample files (Api.php, Controller.php, clean.view.php, example.layout.php) are included as examples for beginners. In this guide, we’ll create our own controller and views, so you can safely delete or ignore these files.
Configuring the Module
Configure the HelloWorld module to serve /helloworld.
Step 1: Event listeners
Generated app/modules/HelloWorld/module.listeners.php is the place for module events. Routes go in initialize() (next steps). Leave register() empty unless you subscribe to events.
DotApp fires several module-specific events if you need them later:
dotapp.module.HelloWorld.init.start: Fired when module initialization begins.dotapp.module.HelloWorld.init.loading: Fired when the module’s main functions (e.g., routes) start loading, if initialization conditions are met.dotapp.module.HelloWorld.init.loaded: Fired after the module’s routes and functions are loaded.dotapp.module.HelloWorld.init.end: Fired when module initialization ends, regardless of whether conditions were met.dotapp.modules.loaded: Fired after all modules are loaded.
Step 2: Configure Module Initialization
Open app/modules/HelloWorld/module.init.php to define when the module should activate. Modify the initializeRoutes function to specify that the module activates for routes starting with /helloworld:
public function initializeRoutes() {
return ['/helloworld', '/helloworld/*'];
}
This ensures the module only activates for URLs starting with /helloworld (e.g., /helloworld, /helloworld/, /helloworld/sekcia). Using ['*'] (activating for all URLs) is less efficient and not recommended for large projects, so we optimize by specifying our route prefix.
Next, configure the initializeCondition function to determine if the module should initialize based on the route match. By default, set it to:
public function initializeCondition($routeMatch) {
return $routeMatch;
}
This activates the module whenever a route from initializeRoutes matches. You can add custom logic. For example, to activate only in a given year:
public function initializeCondition($routeMatch) {
if ($routeMatch === true) {
if (date("Y") == 2026) return true;
}
return false;
}
That example is only an illustration. For this guide, keep return $routeMatch;.
Step 3: Define Initial Routes
In the same module.init.php file, import Config and Router at the top, then define routes in initialize:
public function initialize($dotApp) {
Config::module('HelloWorld', 'prefix') ?? Config::module('HelloWorld', 'prefix', '/helloworld');
$p = rtrim((string) Config::module('HelloWorld', 'prefix'), '/');
Router::get($p, 'HelloWorld:Home@index!', Router::STATIC_ROUTE);
Router::get($p . '/', 'HelloWorld:Home@index!', Router::STATIC_ROUTE);
}
This sets up static routes for /helloworld and /helloworld/, pointing to the index method of the Home controller in the HelloWorld module. Router::STATIC_ROUTE matches the exact path.
What’s Next?
Your HelloWorld module is now created and configured. Next, we’ll create the Home controller to handle the /helloworld route. Head to the First Controller section to continue.
First Controller
With your HelloWorld module configured, create a controller for the /helloworld route. Controllers live in app/modules/{Module}/Controllers/. This guide uses Home, which is also the controller shipped with the live demo.
Creating the Controller
Use the DotApper CLI to generate the Home controller:
php dotapper.php --module=HelloWorld --create-controller=Home
You’ll see:
Controller 'Home' successfully created!
That creates app/modules/HelloWorld/Controllers/Home.php.
Setting Up the Controller
Open that file and implement index as a public static method. The live demo renders a view with the Renderer facade. setView() must run before setViewVar(). If the view is missing, renderView() returns an empty string.
namespace Dotsystems\App\Modules\HelloWorld\Controllers;
use Dotsystems\App\Parts\Logger;
use Dotsystems\App\Parts\Renderer;
use Dotsystems\App\Parts\Response;
class Home extends \Dotsystems\App\Parts\Controller
{
public static function index($request)
{
$html = Renderer::new()
->module('HelloWorld')
->setView('hello')
->setViewVar('title', 'Hello World')
->setViewVar('message', 'DotApp 2.0 is running.')
->renderView();
if ($html === '') {
Logger::use()->error('HelloWorld view produced empty output');
return new Response(500, 'Template error');
}
return $html;
}
}
Create app/modules/HelloWorld/views/hello.view.php as a full HTML page (the live demo does not use a nested layout for this screen):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ var: $title }}</title>
<link rel="stylesheet" href="/assets/modules/HelloWorld/css/hello.css" />
</head>
<body>
<main>
<h1>{{ var: $title }}</h1>
<p>{{ var: $message }}</p>
<p><a href="/documentation/step-by-step">Back to the guide</a></p>
</main>
</body>
</html>
The route string is 'HelloWorld:Home@index!'. The trailing ! turns dependency injection off for that method. The first argument is always $request.
What’s Next?
Head to the Hello World section to open the page in the browser.
Hello World
Your HelloWorld module and Home controller are ready. This section confirms that /helloworld renders.
Testing Your Application
Use PHP’s built-in server from the project root:
php -S 127.0.0.1:8000
Then open http://127.0.0.1:8000/helloworld (or /helloworld/). You should see the heading Hello World and the message from the view.
Viewing the Hello World Page
The live site serves the same module at /helloworld. Output comes from app/modules/HelloWorld/Controllers/Home.php rendering views/hello.view.php.
Understanding the Flow
module.init.phpactivates the module for/helloworldand/helloworld/*.- Static routes map both slash variants to
HelloWorld:Home@index!. Home::indexbuilds HTML withRenderer::new()->module('HelloWorld')->setView('hello').- The documentation site serves
/from the Docs module. HelloWorld is available at/helloworld.
Congratulations
Next, add a layout include and a named form. Head to Introduction to the template system.
Introduction to the template system
Hello World already rendered a view. This section names the pieces so you can grow that page: files, the Renderer facade, and the {{ … }} directives. The full reference lives on the documentation hub: Template system.
Files
- View —
app/modules/{Module}/views/{name}.view.php, selected withsetView('name'). - Layout —
app/modules/{Module}/views/layouts/{path}.layout.php, selected withsetLayout('path')or included as{{ layout:path }}. - Assets —
app/modules/{Module}/assets/..., served as/assets/modules/{Module}/....
{{ layout:h1-test }} loads views/layouts/h1-test.layout.php. Do not prefix the name with layouts/.
Renderer
$html = Renderer::new()
->module('HelloWorld')
->setView('hello')
->setViewVar('title', 'Hello World')
->renderView();
if ($html === '') {
return new Response(500, 'Template error');
}
- Call
setView()beforesetViewVar(). - The second argument of
setView()is a fallback view, not a wrapper layout. - A missing file returns
""— no exception. Check the string. renderView()sees view variables only. Pass everything throughsetViewVar().
Directives
| Write | Meaning |
|---|---|
{{ var: $title }} |
Print a value. Not {{ $title }}. |
{{ if … }} … {{ /if }} |
Conditional. Space after {{. |
{{ foreach $items as $item }} … {{ /foreach }} |
Loop. |
{{ layout:partials/header }} |
Include a layout file. |
{{ content }} |
Slot for setLayout() when you call renderView(). |
{{ formName(saveItem) }} |
Between <fo-rm method="POST"> and </fo-rm>. |
{{ enc(key): $id }} |
Encrypt a field. Decrypt with the same key. |
{{_ "Login" }} |
Translate a string. |
Load /assets/dotapp/dotapp.js on pages that submit <fo-rm> or call $dotapp().load().
What’s next
The next section adds a notes page to Hello World: a layout include, a named form, and form() in the controller. That extra route is a local exercise — it is not on the public demo. For every directive and the render pipeline, open Template system.
Hello World with a form and a layout
Extend the live HelloWorld module on your own copy of the project. You will add /helloworld/notes, include a small layout, and process a named form. The public site keeps only /helloworld.
Step 1: Route
In app/modules/HelloWorld/module.init.php, next to the existing /helloworld routes:
Router::match(
['GET', 'POST'],
['/helloworld/notes', '/helloworld/notes/'],
'HelloWorld:Home@index2!',
Router::STATIC_ROUTE
);
initializeRoutes() already returns /helloworld/*, so the new path is loaded with the module. The ! on the controller string turns dependency injection off; the method receives $request only.
Step 2: Layout
Create app/modules/HelloWorld/views/layouts/notes-heading.layout.php:
<h1>{{ var: $heading }}</h1>
Step 3: View
Create app/modules/HelloWorld/views/notes.view.php. {{ formName(saveNote) }} sits inside <fo-rm>. The form has no action, so it posts to the current URL.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ var: $title }}</title>
<link rel="stylesheet" href="/assets/modules/HelloWorld/css/hello.css" />
</head>
<body>
<p>{{ var: $lead }}</p>
{{ if $saved }}
<p>You submitted: {{ var: $saved }}</p>
{{ /if }}
<fo-rm method="POST" id="noteForm">
<label for="note">Note</label>
<input type="text" id="note" name="note" placeholder="Text to echo" />
{{ formName(saveNote) }}
<button type="submit">{{ var: $btnText }}</button>
</fo-rm>
<h2>Tips from foreach</h2>
<ul>
{{ foreach $tips as $tip }}
<li>{{ var: $tip }}</li>
{{ /foreach }}
</ul>
<p><a href="/helloworld">Back to Hello World</a></p>
<script src="/assets/dotapp/dotapp.js"></script>
</body>
</html>
This page does not use AJAX, so dotapp.js is optional for a classic POST. Keep the script if you later bind $dotapp().form('#noteForm') like the secure forms demo.
Step 4: Controller
Add index2 in app/modules/HelloWorld/Controllers/Home.php. Always pass an error callback to form(). Pass $request->getPath() so the encrypted handler matches the posted URL (with or without a trailing slash).
public static function index2($request)
{
$saved = '';
$request->form(['POST'], 'saveNote', function ($request) use (&$saved) {
$saved = (string) ($request->data()['note'] ?? '');
}, function () {
// Required. Runs when the name does not match or the signature is invalid.
}, $request->getPath());
$html = Renderer::new()
->module('HelloWorld')
->setView('notes')
->setViewVar('title', 'Notes')
->setViewVar('heading', 'Notes')
->setViewVar('lead', 'Submit a line of text. The next render shows it below the heading.')
->setViewVar('btnText', 'Save')
->setViewVar('saved', $saved)
->setViewVar('tips', [
'setView() before setViewVar()',
'formName stays between fo-rm tags',
'Empty renderView() means a missing file',
])
->renderView();
if ($html === '') {
Logger::use()->error('HelloWorld notes view produced empty output');
return new Response(500, 'Template error');
}
return $html;
}
$request->data() is the XSS-protected bag, which is what you want to print. Use $request->data(true) when you decrypt or compare secrets. Do not build HTML strings in the controller — pass data and format it in the view.
Try it
From the project root:
php -S 127.0.0.1:8000
Open http://127.0.0.1:8000/helloworld/notes. You should see the heading from the layout, three tips from foreach, and the form. Submit text; the page reloads and shows the protected value.
Next: Try it live for the public Hello World page, or the full template system reference (directives, assets, custom renderers, sandbox).
Try it live
The HelloWorld module from this guide is running on this site. You do not need a local server to see the first page.
Hello World
Open /helloworld. That is HelloWorld:Home@index! rendering views/hello.view.php with Renderer::new().
Notes page (local only)
The form-and-layout exercise in the previous section (/helloworld/notes) is not deployed here. Add that route on your own copy, then compare it with the public page.
What to read next
- Template system — every directive, assets, custom renderers, sandbox.
- Secure forms —
fo-rm, CRC, encrypted fields. - Examples — live demos.