Skip to content

Your first application

This guide walks through a complete Quiote application: a front controller that boots the framework, one route, one action, one view, and one template. It mirrors the app the CLI scaffolds with quiote new, so you can generate it and follow along.

By the end you will understand the path a request takes, in order: URL, route, action, view, template, response.

A Quiote app is a directory with Config/, Modules/, Routing/, and a public entry point:

app/
├── Config/
│ ├── settings.php
│ ├── factories.yaml
│ ├── output_types.xml
│ └── databases.xml
├── Modules/
│ └── Default/
│ ├── Actions/
│ │ └── IndexAction.php
│ ├── Views/
│ │ └── IndexSuccessView.php
│ └── Templates/
│ └── IndexSuccess.php
├── Routing/
│ └── AppRouting.php
└── pub/
└── index.php

Notice the naming: an action named Index in the Default module, returning the view name Success, resolves to the view class IndexSuccessView and the template IndexSuccess.php. That convention is the thread that ties the pieces together.

Every request enters through pub/index.php. Its whole job is to find the autoloader and start the kernel:

<?php
// ... autoloader setup ...
Quiote\Runtime\Kernel::create([
'app_dir' => dirname(__DIR__),
'env' => getenv('QUIOTE_ENV') ?: 'development',
'context' => 'web',
])->run();

Kernel::create() takes an options array:

  • app_dir — the application root that contains Config/, Modules/, and so on.
  • env — the environment name (development, production, …). It selects which config overlays apply.
  • context — the execution profile. web is the HTTP profile.

run() boots the framework, then serves requests. Under FrankenPHP it runs a persistent worker loop and reuses the booted framework across requests; under a classic SAPI it handles a single request and exits. You write the same code either way — the kernel picks the right worker adapter for you.

Routes live in a Routing subclass. You build a Symfony RouteCollection in a build() method and map each route to a module and action via the _module / _action defaults:

<?php
namespace SampleApp\Routing;
use Quiote\Routing\AttributeRoutes;
use Quiote\Routing\Routing;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
final class AppRouting extends Routing
{
protected function build(): array
{
$routes = new RouteCollection();
$meta = [];
$routes->add('index', new Route('/', ['_module' => 'Default', '_action' => 'Index']));
$meta['index'] = ['gen_path' => '/', 'path' => '/', 'cut' => false];
// Pull in any routes declared with #[Route] attributes on action classes.
AttributeRoutes::mergeInto($routes, $meta);
return [$routes, $meta];
}
}

The route / says: when the path is /, dispatch the Index action in the Default module. Routing is covered in full in Routing, including the fluent addRoute() helper and #[Route] attributes.

An action decides what happens for a request and returns the name of a view — never HTML. Here is the whole Index action:

<?php
namespace SampleApp\Modules\Default\Actions;
use Quiote\Action\Action;
use Quiote\Request\WebRequest;
class IndexAction extends Action
{
public function executeRead(WebRequest $rd)
{
return 'Success';
}
public function getDefaultViewName()
{
return 'Success';
}
}

Two things to understand:

  • executeRead runs for GET requests. Quiote maps the HTTP verb to a method: GET to executeRead, POST to executeWrite, PUT/PATCH to executeUpdate, DELETE to executeRemove. (You can also name a method after the exact verb — executeGet, executePost — when you need to.) This is how one action class handles several verbs. See Actions and views for the full mapping.
  • The return value is a view name, not content. Returning 'Success' tells the framework to render the Success view for this action. The action never touches the response body.

To pass data to the view, call setAttribute():

public function executeRead(WebRequest $rd)
{
$this->setAttribute('title', 'Home');
return 'Success';
}

A view turns the action’s result into output for a specific output type (HTML, JSON, …). The method that runs depends on the negotiated output type: for HTML, that is executeHtml().

<?php
namespace SampleApp\Modules\Default\Views;
use Quiote\Request\WebRequest;
use Quiote\View\View;
class IndexSuccessView extends View
{
public function executeHtml(WebRequest $rd)
{
// Load the layout/layers declared in output_types.xml so the
// "content" layer's template actually renders.
$this->loadLayout();
$this->setAttribute('title', 'Home');
}
}

loadLayout() reads the layout for the current output type from output_types.xml and prepares its layers. Without it, executeHtml() returning nothing produces an empty body. Attributes you set here (and attributes the action set) become variables in the template.

The same view can serve multiple output types by defining more methods — executeJson(), for example. See Output types.

Templates are plain PHP. View and action attributes arrive in a $template array:

<!DOCTYPE html>
<html lang="en">
<head>
<title><?php echo htmlspecialchars($template['title'] ?? 'Home', ENT_QUOTES, 'UTF-8'); ?></title>
</head>
<body>
<h1>It works!</h1>
</body>
</html>

$template['title'] is the title attribute set in the view. Templates do escaping themselves — Quiote’s default PHP renderer does not auto-escape, in keeping with its unopinionated stance. Rendering and layouts are covered in Templates and rendering.

Four config files make this app runnable. Note that they are in three different formats — Quiote reads all of them, so you can pick per file (see Configuration).

settings — application settings. Written here in each of the three formats (pick one file; the app uses PHP):

Config/settings.php
return [
'core.app_name' => 'SampleApp',
'core.namespace_prefix' => 'SampleApp',
'core.available' => true,
'core.debug' => false,
'core.use_database' => false,
'core.use_logging' => true,
'core.use_security' => false,
'core.use_translation' => false,
'core.default_context' => 'web',
];

factories — which class fills each core role. This is Quiote’s equivalent of a service manifest (each role needs a class, plus optional constructor params):

Config/factories.php
return [
'controller' => ['class' => \Quiote\Controller\Controller::class, 'params' => []],
'response' => ['class' => \Quiote\Response\WebResponse::class, 'params' => []],
'routing' => ['class' => \SampleApp\Routing\AppRouting::class, 'params' => []],
'request' => ['class' => \Quiote\Request\WebRequest::class, 'params' => []],
'user' => ['class' => \Quiote\User\RbacSecurityUser::class, 'params' => []],
'database_manager' => ['class' => \Quiote\Database\DatabaseManager::class, 'params' => []],
'validation_manager' => ['class' => \Quiote\Validator\ValidationManager::class, 'params' => []],
'session' => ['class' => \Quiote\Session\FileSessionFactory::class, 'params' => ['dir' => '%core.app_dir%/cache/sessions']],
];

output_types declares the html output type, its PHP renderer, and its layout. Config/databases.xml declares a database connection (unused here because core.use_database is false). Both are shown in Output types and Databases.

Two of these roles are worth a second look.

session is Quiote\Session\FileSessionFactory, pointed at cache/sessions. It needs no database and nothing installed, and it is what gives the app a session cookie — which is what CSRF tokens are stored in. Delete the role and no session cookie is ever sent, so same-origin unsafe requests take the sessionless CSRF exemption while cross-origin ones can never produce a valid token. Read Sessions before changing it.

user is Quiote\User\RbacSecurityUser, not the bare User. It degrades safely with no rbac_definitions.xml present — no roles, no permissions, nothing granted — so the scaffold gets you the role-aware user class without also generating RBAC configuration you may not want. Swap it for Quiote\User\User if you will never need roles, or add the definitions file when you do; see Authentication and authorization.

core.use_security stays false in the scaffold. That’s a functional default rather than a security one: the generated app has no protected actions and no login or secure system actions, so enabling security would make a fresh app fail its first forward rather than make it safer.

Point a web server at pub/index.php and request /. The kernel boots, AppRouting matches / to the Index action, executeRead returns 'Success', IndexSuccessView::executeHtml() loads the layout and sets title, and IndexSuccess.php renders. You get HTML back.

The full path, once more, with the moving parts named:

  1. The kernel boots the framework and builds the middleware pipeline.
  2. RoutingMiddleware matches / and resolves it to Default/Index.
  3. DispatchMiddleware runs the action; executeRead returns 'Success'.
  4. The executor resolves Success to IndexSuccessView, calls executeHtml(), and renders the IndexSuccess template.
  5. The response is emitted.

To understand that pipeline in depth, continue to The request lifecycle.