Skip to content

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:

  1. Download the file: dotapper.php.
  2. Save it to your project directory.
  3. 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:

  1. Download the ZIP: DotApp main.zip.
  2. 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 a howtouse.txt guide 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, including clean.view.php (a sample view) and layouts/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.php activates the module for /helloworld and /helloworld/*.
  • Static routes map both slash variants to HelloWorld:Home@index!.
  • Home::index builds HTML with Renderer::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

  • Viewapp/modules/{Module}/views/{name}.view.php, selected with setView('name').
  • Layoutapp/modules/{Module}/views/layouts/{path}.layout.php, selected with setLayout('path') or included as {{ layout:path }}.
  • Assetsapp/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() before setViewVar().
  • 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 through setViewVar().

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