amp-framework / kit
Small building blocks that applications on ampf share: UUID ids, the MariaDB UUID column, the migrations' set-up, an integration test case on a disposable database, and guards that hold conventions as tests
Requires
- php: >=8.5
- ext-pdo: *
- ext-pdo_mysql: *
- amp-framework/ampf: dev-master
- doctrine/migrations: ^3.9
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- infection/infection: ^0.35
- phpstan/phpstan: ^2.2
- phpstan/phpstan-doctrine: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^13.0
- slevomat/coding-standard: ^8.31
- squizlabs/php_codesniffer: ^4.0
Suggests
- ext-dom: ampf\Kit\Testing\Guard\TemplateTextGuard reads the templates with PHP's HTML parser (Dom\HTMLDocument)
- ext-intl: ampf\Kit\Testing\Guard\TranslationFilesGuard reads the texts as ICU messages (MessageFormatter)
- ext-tokenizer: ampf\Kit\Testing\Guard\SourceCode reads the attributes and the class-strings of a source file with PHP's tokenizer (PhpToken), for EntityConventionsGuard
- phpunit/phpunit: ampf\Kit\Testing, the test case and the guards, extend PHPUnit's TestCase: an application that uses them has it as a development dependency
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-06 10:01:57 UTC
README
Small building blocks that applications on ampf have in common and that are
not the framework's business: UUID ids with MariaDB's UUID column, the set-up of Doctrine's migrations, an integration
test case on a disposable database, and guards that hold an application to a set of conventions.
What belongs here fits an application as it is. What every application would have to override — its users, its login, its controllers, its settings, its look — is the application's own, and so is what only one application needs.
Installation. composer require amp-framework/kit:dev-main; the package is on
Packagist and requires ampf. While the package and an application are
developed together, a path repository ({"type": "path", "url": "../kit"}) links a checkout into the application's
vendor/ instead.
What it holds
ampf\Kit\ in src/:
| Part | What it is |
|---|---|
Helper\Uuid |
A UUID v7 (generate()), whether a text is a UUID (isValid()), and the pattern of one for a route (PATTERN) |
Helper\Text |
What the rules for the texts a person types have in common: kept without white space at its ends (trim()), one line (isOneLine()), lines (isLines()) |
Doctrine\Entity\BaseEntity |
What every entity has: a UUID that the application assigns when the object is made, and TABLE_OPTIONS, the one character set and collation of every table (utf8mb4 and utf8mb4_uca1400_ai_ci) |
Doctrine\Type\UuidType |
DBAL's guid as MariaDB's UUID column (16 bytes, text in and out) |
Doctrine\Type\UtcDateTimeImmutableType |
DBAL's datetime_immutable (and datetimetz_immutable) with the database in UTC: a datetime is written as its UTC time, a value read is a DateTimeImmutable in UTC, whole seconds either way |
Bootstrap\MigrationsFactory, Bootstrap\DoctrineConfiguration |
Doctrine Migrations over the application's migrations block (bin/doctrine and the guard of the migrations both use it), and the name of the migrations' own table, which the schema tool is told to leave alone (ignoreMigrationsTable()) |
Testing\IntegrationTestCase |
ampf's ApplicationTestCase on a disposable MariaDB (below) |
Testing\SelectCountingMiddleware, Testing\SelectCounter |
The count of the SELECT statements that every connection of the tests sends: the test's own and a request's (countSelects() reads it) |
Testing\Guard\… |
Conventions as tests that an application points at its own files and at its running self (below) |
config/default.php is the package's doctrine block, which an application lists after ampf's two files: every datetime in
UTC and a database's enum read as a string (ampf's own entries: datetime and datetimetz as its mutable UTCDateTimeType),
datetime_immutable and datetimetz_immutable as UtcDateTimeImmutableType, and guid as UuidType, which
doctrine.mappingOverrides reads back as guid. No class is final.
How an application wires it
Entry points. public/index.php, bin/index.php and bin/doctrine list the package's config/default.php after
ampf's two files and before the application's own:
$config = ApplicationContext::boot([ $root . '/vendor/amp-framework/ampf/config/default.php', $root . '/vendor/amp-framework/ampf/config/http.php', // or cli.php $root . '/vendor/amp-framework/kit/config/default.php', $root . '/config/default.php', $root . '/config/http.php', // or cli.php $root . '/config/local.php', ]);
The files merge one level deep: an application that sets doctrine.typeOverrides or doctrine.mappingOverrides itself
replaces the package's whole block and repeats its entries beside its own (DoctrineSettingsGuard holds it to that).
The database. MariaDB 10.10 or later (the UUID type, the UCA 14 collations), a connection in UTC (SET time_zone = 'UTC'). config/local.php builds the ORM configuration with ampf's DoctrineConfiguration::create() (the attribute
mapping and native lazy objects; no cache directory on a development machine). An application's entities extend BaseEntity
and put their tables on BaseEntity::TABLE_OPTIONS:
#[ORM\Entity(repositoryClass: NoteRepo::class)] #[ORM\Table(name: 'notes', options: self::TABLE_OPTIONS)] class NoteEntity extends BaseEntity {}
The datetimes. An entity maps an immutable datetime as it would without the package, and the database holds UTC with nothing converted by hand:
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)] private DateTimeImmutable $createdAt;
A value in any time zone is written as its UTC time (the value itself is left as it is), and a value read is a
DateTimeImmutable in UTC, whatever time zone PHP has as its default. The column is DBAL's DATETIME, which holds whole
seconds: the fraction of a second of a value written is cut off, not rounded, and a column that a migration made with
fractions of a second is refused when it is read. A value that is no datetime is refused with DBAL's InvalidType, a text
that is no Y-m-d H:i:s with its InvalidFormat. datetime stays ampf's UTCDateTimeType, a mutable DateTime in UTC.
The migrations. The application's migrations block lists its migrations; MigrationsFactory::create($config, $entityManager) reads it (bin/doctrine and MigrationsGuard both call it):
'migrations' => [ 'migrations_paths' => ['acme\notes\Migration' => dirname(__DIR__) . '/src/Migration'], 'table_storage' => ['table_name' => DoctrineConfiguration::MIGRATIONS_TABLE], 'transactional' => false, ],
The tests. ampf\Kit\Testing\IntegrationTestCase is ampf's ApplicationTestCase with the package's config/default.php
after ampf's files and a disposable MariaDB: a subclass names the project root (projectRoot()), and
tests/Support/config/integration.php of the project, which boots in place of config/local.php, names the database. It
refuses a database whose name does not end in _test or _test_<n> (disposableDatabasePattern(), which an application may
tighten), makes the schema from the mapping once per process, and starts every test with empty tables. $em is the test's own
entity manager, which the beans of ownBean() share; a request's is closed when the next request starts. dbText(),
dbTexts(), countSelects() and rebuildSchema() are the helpers. countSelects($work) says how many SELECT statements the
work sent on every connection, the test's own and those of the requests and commands it runs, each of which has one of its
own: the test case puts SelectCountingMiddleware into the Doctrine configuration of the tests, so that a page or a service
that must cost a fixed number of queries can be held to it (and, as a number, above zero). It extends PHPUnit's TestCase: an
application that uses it has PHPUnit as a development dependency.
The guards. ampf\Kit\Testing\Guard\ holds conventions as tests of an application's own files, in the
pattern of ampf's guards (ampf\Testing\Guard\): a guard is an abstract TestCase that carries its tests and data providers,
and the application extends it in one small class of its tests/Unit/ (those that read files) or its tests/Integration/
(those that boot the application) that names its project root (projectRoot()) and whatever else is its own; the hooks have
these conventions as their defaults.
final class TemplateTextTest extends TemplateTextGuard { protected static function projectRoot(): string { return dirname(__DIR__, 2); } protected static function allowedLiterals(): array { return [...parent::allowedLiterals(), 'Note', 'Book']; } }
A failure says what is wrong and names the file (from the project's root), the key, the section, the table or the class at
fault; one that finds nothing to look at (a directory without a template, a list of words that is empty) fails too.
SensitiveParametersGuard is the one that does not: a source with no parameter named for a password has nothing unmarked, so it
passes with one data set that says so, and is never skipped. Those that read the configuration merge it as the entry points do:
ampf's files, the package's config/default.php, the application's.
| Guard | Holds | The application names | Hooks (default) |
|---|---|---|---|
TemplateTextGuard |
A template has no text of its own: the text of an element (also the content of a <template> element) and the attributes a person reads (alt, title, label, aria-label, aria-description, aria-placeholder, aria-roledescription, aria-valuetext, placeholder, value, a meta description) come from PHP |
allowedLiterals() (·, |, / and *: add the wordmark's pieces), templateDirectory() (views/http) |
|
TemplateMarkupGuard |
No inline script, style or on…= (the Content Security Policy refuses them); an error alert (any element whose class lists the error class, among others and in any order) takes the focus |
errorAlertClass() (alert alert--error: its last word is the one looked for), templatesWithoutAlertFocus() (none: name the templates whose page puts the focus on a field, the login's say), templateDirectory() |
|
TranslationFilesGuard |
A file for every language and a language for every file; keys in dotted groups, sorted, the same in each; the same arguments and ICU arguments; texts safe to print; ICU messages their language reads | languages(): the codes, the base language first |
translationDirectory() (config/translations), baseLanguage() (the first) |
TranslationKeyUsageGuard |
A string written like a key of a group the base language has is a key of it, and every key is named in the code | languages() |
scannedDirectories() (src, views), translationDirectory(), baseLanguage() |
SensitiveParametersGuard |
A parameter named for a password is #[SensitiveParameter]; a source with none passes |
sourceNamespace() |
sensitiveNames() (password: add token, a key), sourceDirectory() |
EntityConventionsGuard |
Every entity below Doctrine/Entity (a file *Entity.php, or any file that declares #[ORM\Entity] or #[ORM\MappedSuperclass]) extends BaseEntity, names a repository of ampf's kind and a table on BaseEntity::TABLE_OPTIONS (a mapped superclass has neither) |
sourceNamespace() |
sourceDirectory() (src), entityDirectory() (Doctrine/Entity) |
DoctrineSettingsGuard |
An application's own doctrine.typeOverrides or mappingOverrides keeps every entry of ampf's and the package's |
transports() (http, cli) |
The guards that boot the application are ampf\Kit\Testing\IntegrationTestCases (AbstractApplicationGuard): they run in the
application's integration suite, over its tests/Support/config/integration.php and the disposable database.
| Guard | Holds | Hooks (default) |
|---|---|---|
SchemaGuard |
MariaDB 10.10 or later, a connection in UTC, a mapping the schema tool accepts, every table InnoDB and every table and string column on the collation of BaseEntity::TABLE_OPTIONS, every id a UUID |
serverVersion() |
MigrationsGuard |
Every migration says what it does; run on an empty database the migrations build exactly the mapped schema, on that collation (the migrations' table included); the way back undoes them all and a second run has nothing left |
A guard is tested against an application that abides and applications that break each rule (tests/Fixtures/<Guard>/,
AGENTS.md); tests/Unit/Testing/Guard/Abiding/ and tests/Integration/Testing/Guard/Abiding/ run them as an application
does.
Working on the package
PHP runs only in Docker; the host needs Docker with Compose and nothing else. The scripts are those of the applications
(sh docker/tooling, sh docker/ci, sh docker/test-integration, sh docker/mutation): a throwaway container of the
image ampf-kit-tooling, as the calling user, the checkout at /app, no network but for Composer. The disposable test
stack (docker/compose.test.yml, the Compose project ampf-kit-test) is a MariaDB in memory and a network with no way
out.
sh docker/tooling install # the dependencies from composer.lock sh docker/ci [static|unit|integration|mutation] # every gate, in order; the first failing step's exit status AMPF_KIT_AMPF_CHECKOUT=$PWD/../ampf sh docker/ci # against an unreleased checkout of ampf (docker/mounts.sh)
The gates are ampf's: PHPCS and PHP-CS-Fixer with ampf's own rules, PHPStan at the maximum level, both suites, and
mutation testing at 100 %. The code is ampf\Kit\ in src/; the tests are ampf\Kit\Tests\ in tests/, and the ones
that need a booted application run the fixture application in tests/Fixtures/App/ (AGENTS.md).