Base of Dependencies for Derafu’s Web Applications
Overview
Derafu Foundation has no PHP code. It declares the packages that every Derafu web application needs, so an application requires only derafu/foundation and gets them all, and it provides the configuration files they share.
It is a library and not a metapackage on purpose: Composer does not write anything of a metapackage in vendor, and the configuration files (resources/config) have to be there.
{
"require": {
"derafu/foundation": "dev-main"
}
}
The application declares in its own composer.json only what is extra to the base (for example derafu/auth).
What it requires
derafu/captcha, derafu/contact-form, derafu/content, derafu/csrf, derafu/data-processor, derafu/form, derafu/http, derafu/kernel, derafu/markdown, derafu/renderer, derafu/routing, derafu/session, derafu/support, derafu/translation, derafu/twig, guzzlehttp/guzzle, mpdf/mpdf, symfony/cache and symfony/yaml.
The packages whose configuration files foundation imports are declared even when another package already requires them, because if that package stopped requiring them the import would fail.
Configuration
It also provides the configuration that every site repeated, so each site imports one file for its services and one for its routes.
config/services.yaml of the site:
imports:
- { resource: '../vendor/derafu/foundation/resources/config/foundation-services.yaml' }
resources/config/foundation-services.yaml imports the services of the packages above and defines the request handler with its middlewares (who the client is, the router, the session, the flash messages and the CSRF tokens of the forms, in that order), the router, the renderer with the Twig extensions, the translator, and the HTTP client (PSR-18) with the PSR-17 factories, which are Guzzle’s, with a timeout of 10 seconds, and a cache pool (PSR-6) in the cache directory of the kernel, for the packages that cache what they read (the keys of Keycloak, the failed logins). Who the client of a request is (what the limits of requests and of failed logins count by) is decided by ClientIpMiddleware of derafu/http: by default it is the address of the connection and no header is believed, and behind a proxy the site says which ones are trusted with HTTP_TRUSTED_PROXIES (and in which headers the address comes with HTTP_CLIENT_IP_HEADERS). The language of the site is the parameter app.locale: the value of the environment variable APP_LOCALE, or app.locale.default (English). The forms of the site can be protected with a captcha (derafu/captcha), which foundation configures with the environment variables CAPTCHA_PROVIDER, CAPTCHA_SITE_KEY and CAPTCHA_SECRET_KEY. The contact form is protected with it, so a site that has the contact page needs CAPTCHA_PROVIDER (for example altcha with a CAPTCHA_SECRET_KEY, which needs no account), or CAPTCHA_PROVIDER=none to say, on purpose, that it has none: without it the page of the form fails with a message that says so. For reCAPTCHA v3, foundation sets the default of CAPTCHA_MIN_SCORE (the lowest score that is accepted) to 0.7. It also gives a default (true) to the environment variable CONTENT_CACHE_ENABLED, which the site can use in its derafu.content.config to enable the cache of the content.
config/routes.yaml of the site:
imports:
- { resource: '../vendor/derafu/foundation/resources/config/foundation-routes.yaml' }
resources/config/foundation-routes.yaml imports the routes of derafu/content and derafu/contact-form, and defines homepage (templates/pages/home.html.twig).
The page of the errors is not a route, because it is not a URL to ask for: foundation-services.yaml sets the parameter http.error_pages with templates/error.html.twig as the page for every error. A site with other pages sets the parameter in its own services.yaml (see Error Handling).
Changing the defaults
A site that needs something different declares, in its own file and after the import, a service (or a route) with the same name: its definition replaces the one of foundation. The replacement is complete: the arguments of both definitions are not merged, so the site writes the whole definition.
For example, a site in Spanish replaces the translator:
services:
Symfony\Component\Translation\Translator:
factory: ['Derafu\Translation\TranslatorFactory', 'create']
arguments:
$defaultLocale: 'es'
$fallbackLocales: ['es', 'en']
$resourceProviders: !tagged_iterator derafu_translation.resource_provider
The routes of derafu/auth and the ones of the site are not in foundation: each site declares them in its routes.yaml.
Testing a site
Derafu\Foundation\Testing\SiteTestCase is a base class for the smoke tests of a site: it boots the kernel with the real config/ of the site and asks for pages. A site that extends it has the tests of what every site must do, without writing them:
#[CoversNothing]
final class WebsiteTest extends SiteTestCase
{
}
| Test | What it checks |
|---|---|
| The pages of the site answer | Each path of paths() answers with a 200 and without “Internal Server Error”. By default /, /contact and /contact/success. |
| A page that does not exist is not found | An unknown path is a 404, not a 500. |
| The error of a page that does not exist is the page of the site | The 404 is answered, to a browser, with the HTML of templates/error.html.twig and not with the Markdown text that is the last resort for a serious failure. |
| The error page is not a page of the site | /error and /error404 are a 404 like any other path: an error page is the answer to an error, not something to ask for. |
The code in src/ is measured by coverage |
If src/ has PHP files, the phpunit.xml of the site has an active <coverage> with a <report> (one that is commented out does not count). It is the other half of a rule: a site without code must have that block commented out, because PHPUnit refuses to make a report of nothing and fails before running any test, with the warning “Configured source filter (include-path: …/src) does not match any files, code coverage will not be processed”. That one can not be a test (none runs), so the phpunit.xml says it in a comment next to the block. The day the site has code, this test fails until the block is uncommented. |
| No route of the site fails in the server | Every route that a GET can ask without parameters answers with less than a 500 (a redirect, a 401 or a 403 is fine). It finds the routes that a package registers but that the site has not configured. |
| Every route name used in the templates is defined | The templates/ of the site only refer, with path(), url() or is_active_path(), to routes that the router of the site has. It is checked by reading the templates, with the Twig environment that renders them, so a link to a route that does not exist fails here and not the day someone renders that page. A name that is built at runtime can not be checked by reading, and it fails too. |
A site without a contact page overrides paths(); a route that needs a service that is not there when testing (a game server, a payment gateway) is listed in excludedPaths() so the test of the routes does not ask for it, with a comment that says why. The tests that are only about the site use get($path, $locale), which returns the status and the body:
protected function paths(): array
{
return ['/'];
}
protected function excludedPaths(): array
{
return [
// It asks the Minecraft server through RCON, which is not there when testing.
'/minecraft/map',
];
}
The kernel of the site is built once, in setUpBeforeClass(), before PHPUnit takes its photo of the global state: it reads the .env of the site and writes its variables to $_ENV and $_SERVER, and the services that are created later (when a page needs them) still read them. A test class that overrides setUpBeforeClass() has to call the parent.
The tests need CAPTCHA_PROVIDER=none in the phpunit.xml (<env name="CAPTCHA_PROVIDER" value="none"/>) so the forms do not ask for a captcha, and FORM_CONTACT_ENABLED=false (<env name="FORM_CONTACT_ENABLED" value="false"/>) so the contact page answers with its page of a form that is not available, a 200, and not with a 503 for the webhook that a test does not have. They also need phpunit/phpunit, that this package only suggests. A site that does not want the contact form in production sets FORM_CONTACT_ENABLED=false in its .env too; one that wants it sets the webhook (see Contact form).
How it relates to derafu/website-skeleton and derafu/library-skeleton
There are three packages, and each has one job:
| Package | Is | Use it to |
|---|---|---|
derafu/foundation |
The dependencies and the base configuration of every web application. | Be required by the sites. |
derafu/website-skeleton |
The template of a site: everything is in its repository (templates, configuration, public/, Vite, translations, tests). It requires derafu/foundation. |
Start a new site. |
derafu/library-skeleton |
The template of a library: the tools, the CI, an example class with its test and the pattern of the translations. It does not depend on derafu/foundation. |
Start a new library. |
A site starts from derafu/website-skeleton and depends on derafu/foundation. A library starts from derafu/library-skeleton and depends on nothing of the web.
Where to go next
- To start a site, see Website Skeleton: the structure of a site, its assets, its tests and its GitHub Actions.
- To start a library, see Library Skeleton: the tools, the strict tests and the pattern of the translations.
Both create the repository with composer create-project:
composer create-project derafu/website-skeleton www.example.com --stability=dev
composer create-project derafu/library-skeleton my-library --stability=dev