Small runnable application skeleton for Lemonade Framework. It demonstrates routing, plain PHP views, Bootstrap layout, controller styles, request input, validation, uploads, custom error pages, CLI commands, YAML-first application config, and framework API configuration. YAML is only the app-level input layer; internally the framework still resolves typed config definitions and runtime DTOs.
Routing app/Config/Routing.php
View layout + partials app/Views
Shared page rendering helper app/Controllers/AppController.php
Navigation source app/Navigation/MainNavigation.php
View-based homepage app/Controllers/HomeController.php
PSR response controller app/Controllers/PsrController.php
AbstractController helpers app/Controllers/HelperController.php
Request input app/Controllers/RequestExampleController.php
Form validation service app/Validation/ContactFormValidator.php
Upload options factory app/Upload/UploadExampleOptionsFactory.php
Custom error pages app/Views/errors
CLI commands app/Console and app/Config/Commands.yaml
Discovery config app/Config/Discovery.yaml
Config manifest app/Config/Config.yaml
GET / View-based homepage rendered through layouts.app
GET /examples/psr PSR-7/PSR-17 controller with constructor DI
GET /examples/request Request input and metadata example
GET /examples/contact Contact form example
POST /examples/contact Contact form submit and validation
GET /examples/upload Safe upload examples
POST /examples/upload/image Image upload submit
POST /examples/upload/file Document upload submit
GET /examples/errors/not-found Custom 404 page demo
GET /examples/errors/server-error Custom 500 page demo
GET /examples/helper AbstractController HTML helper
GET /examples/helper/status AbstractController JSON helper
GET /examples/async Event + queue example
POST /examples/async/database Enqueue message to database transport
A real 404 can be checked with any missing URL, for example:
GET /this-route-does-not-exist
The Bootstrap navbar does not hardcode route names in the layout. Navigation items live in:
app/Navigation/MainNavigation.php
App\Controllers\AppController::page() injects the navigation list into layouts.app. The layout iterates over $navigation and uses ViewHelpers::url() for route URLs. Active state is handled through RequestViewHelpers::isRouteActive().
Views are plain PHP templates in app/Views. View names use dot notation:
layouts.app app/Views/layouts/app.php
layouts.error app/Views/layouts/error.php
pages.home app/Views/pages/home.php
pages.examples.request app/Views/pages/examples/request.php
pages.examples.contact app/Views/pages/examples/contact.php
pages.examples.upload app/Views/pages/examples/upload.php
errors/404 app/Views/errors/404.php
errors/500 app/Views/errors/500.php
partials.example-card app/Views/partials/example-card.php
The homepage demonstrates:
View::template() renders pages.home inside layouts.app
View::partial() renders each example card
View::start()/end() defines the page title section
View::section() reads title/footer sections in the layout
View::content() injects page content into the layout
ViewHelpers::url() generates route URLs from route names
Templates use the framework e() helper for HTML escaping. The UI uses Bootstrap 5.3 from CDN; no npm, Vite, Webpack, Sass or frontend build pipeline is required.
The footer links to the official website:
https://lemonadeframework.cz/
PsrController Builds a PSR-7 response with PSR-17 factories injected through the constructor.
HelperController Uses AbstractController helpers for HTML and JSON responses.
HomeController Uses the real view layer and Bootstrap layout for the homepage.
Application pages that use the main layout extend AppController, which provides the page() helper and injects navigation data.
GET /examples/request
GET /examples/request?name=Lemonade
The request example reads query input, HTTP method, user agent, IP address and current URL through AbstractController request helpers, then renders the result through the view layer.
The contact form demonstrates controller + validator service + view errors. Validation rules live in:
app/Validation/ContactFormValidator.php
The controller only reads POST data, calls the validator service, and decides the response.
Rules:
name required, max 100 chars
email required, valid email, max 150 chars
message required, min 10 chars, max 1000 chars
Invalid submit returns 422 with field errors. Valid submit redirects back with a success flash message. The form uses Bootstrap and framework view helpers. No database or mailer is used.
The upload example uses framework upload services and an application options factory:
app/Config/Upload.yaml
app/Upload/UploadExampleOptionsFactory.php
app/Controllers/UploadExampleController.php
app/Views/pages/examples/upload.php
Configured profiles:
upload.images.example_image
upload.files.example_file
Image profile:
Allowed types JPG/JPEG, PNG, WebP
Allowed MIME types image/jpeg, image/png, image/webp
Max size UPLOAD_IMAGE_MAX_BYTES, default 1 MB
Dimensions 128-2048 px width and height
Reencode false
Target directory UPLOAD_IMAGE_TARGET_DIRECTORY, default uploads/examples/files
File profile:
Allowed types PDF, DOC, DOCX, TXT
Allowed MIME types application/pdf, application/msword,
application/vnd.openxmlformats-officedocument.wordprocessingml.document,
text/plain
Max size UPLOAD_FILE_MAX_BYTES, default 1 MB
Target directory UPLOAD_FILE_TARGET_DIRECTORY, default uploads/examples/files
Both flows validate through framework upload services. After a successful upload, the stored file is deleted immediately. The view shows validation metadata only and does not expose a public file URL.
No database record is created and files are not stored permanently.
Application error pages live in:
app/Views/errors/404.php
app/Views/errors/500.php
app/Views/layouts/error.php
The framework ErrorPageRenderer renders errors/404 or errors/500 and wraps the content with layouts/error when real 404/500 handling reaches the HTTP middleware.
Demo routes:
GET /examples/errors/not-found Returns the custom 404 page
GET /examples/errors/server-error Returns the custom 500 page
Production-style 500 pages do not show exception messages or stack traces.
Discovery config lives in:
app/Config/Discovery.yaml
Current skeleton discovery settings come from DiscoveryConfigDefinition:
robots.enabled DISCOVERY_ROBOTS_ENABLED, default true
robots.route /robots.txt
sitemap.enabled DISCOVERY_SITEMAP_ENABLED, default true
sitemap.route /sitemap.xml
sitemap.mode DISCOVERY_SITEMAP_MODE, default stream
sitemap.base_url APP_BASE_URL, default http://localhost
The sitemap route list contains only real skeleton route names:
home.index
examples.psr
examples.request
examples.contact
examples.upload
examples.helper
discovery.sitemap.providers is intentionally empty in the skeleton. Add an application sitemap provider only when the app has real dynamic content to expose.
The framework Discovery service provider also registers this CLI command:
vendor\bin\lemonade discovery:sitemap:generateConfigured application commands:
vendor\bin\lemonade about
vendor\bin\lemonade cron:heartbeatFramework discovery also registers:
vendor\bin\lemonade discovery:sitemap:generateQueue worker expects an async transport. In this skeleton the default queue transport stays sync, so use the database worker explicitly:
vendor\bin\lemonade queue:install
vendor\bin\lemonade queue:work default databaseApplication commands are registered through:
app/Config/Commands.yaml
app/Console/AboutCommand.php
app/Console/Cron/HeartbeatCronCommand.php
cron:heartbeat demonstrates a file-lock based CLI command and writes a heartbeat log entry.
The async example page dispatches one message through the default sync transport and one explicit database transport through:
GET /examples/async
POST /examples/async/database
Current skeleton API config lives in:
app/Config/Api.yaml
Current defaults:
enabled true
prefix /api
Framework API routes are available under the configured prefix, for example:
GET /api/framework/health
GET /api/framework/openapi.json
GET /api/framework/docs
The config enables static bearer authentication support through API_TOKEN and scopes:
api:admin
openapi:read
Current access behavior in this skeleton:
/api/framework/health public
/api/framework/openapi.json protected bearer token
/api/framework/docs protected bearer token
Change the API behavior in app/Config/Api.yaml and .env as needed for a real application. The YAML file is mapped into typed ApiConfigDefinition, not treated as a raw runtime array.
app/Config/Config.yaml is the config manifest. It contains plain file lists for shared, http, and cli.
Current shared config files include:
App.yaml
Container.yaml
Providers.yaml
Localization.yaml
Cache.yaml
Logging.yaml
Session.yaml
Database.yaml
Breadcrumbs.yaml
Pagination.yaml
Events.yaml
Queue.yaml
Upload.yaml
Api.yaml
Discovery.yaml
HttpClient.yaml
View.yaml
Components.yaml
Meta.yaml
Benchmark.yaml
HTTP-only config files:
Cors.yaml
HtmlMinify.yaml
Error.yaml
CLI-only config files:
Commands.yaml
Each application config file is YAML and is mapped into a typed framework definition object such as AppConfigDefinition, ProvidersConfigDefinition, or ApiConfigDefinition.
composer dump-autoload
vendor\bin\lemonade
vendor\bin\lemonade about
vendor\bin\lemonade cron:heartbeatIf a test suite is added later, run the project test command defined in composer.json.