Developer corner 

How a submission is processed 

  1. An entry point (admin panel, backend toolbar) renders a trigger and the dialog configuration, built by \Vkemeter\Opinion\Dialog\DialogConfigurationBuilder .
  2. The JavaScript dialog posts the form to the backend AJAX route opinion_submit.
  3. \Vkemeter\Opinion\Controller\SubmitController checks access, builds and validates the submission and validates the context with the matching context provider.
  4. \Vkemeter\Opinion\Processing\SubmissionProcessor stores the record and the screenshot, then runs every delivery channel and records the result. A failing channel never loses the submission.

Add an entry point 

Build the dialog configuration on the server, after checking access:

use Vkemeter\Opinion\Access\AccessChecker;
use Vkemeter\Opinion\Dialog\DialogConfigurationBuilder;
use Vkemeter\Opinion\Domain\Source;

if ($this->accessChecker->hasAccess($backendUser)) {
    $configuration = $this->dialogConfigurationBuilder->build(
        $backendUser,
        Source::Frontend,
        ['pageUid' => 12],
    );
}
Copied!

Pass it to the page, load the dialog CSS EXT:opinion/Resources/Public/Css/opinion-dialog.css and open the dialog with the JavaScript module:

import { openOpinionDialog } from '@vkemeter/opinion/opinion-dialog.js';

openOpinionDialog(configuration);
Copied!

The import map entry @vkemeter/opinion/ is available in the backend. The context is validated again on submit by the context provider of the source; keys that are switched off in the extension configuration are dropped.

Add a delivery channel 

Implement \Vkemeter\Opinion\Delivery\DeliveryChannelInterface , for example to create a ticket. Implementations are tagged automatically and run next to the email channel for every stored submission. Report failures through the result instead of throwing, they end up in the delivery log.

EXT:my_sitepackage/Classes/Opinion/TicketDeliveryChannel.php
namespace MyVendor\MySitepackage\Opinion;

use TYPO3\CMS\Core\Resource\File;
use Vkemeter\Opinion\Configuration\OpinionConfiguration;
use Vkemeter\Opinion\Delivery\DeliveryChannelInterface;
use Vkemeter\Opinion\Delivery\DeliveryResult;
use Vkemeter\Opinion\Domain\Submission;

final readonly class TicketDeliveryChannel implements DeliveryChannelInterface
{
    public function getIdentifier(): string
    {
        return 'ticket';
    }

    public function deliver(Submission $submission, ?File $screenshot, OpinionConfiguration $configuration): DeliveryResult
    {
        // Create the ticket from $submission->message, $submission->category, ...
        return DeliveryResult::success($this->getIdentifier(), 'Ticket created');
    }
}
Copied!

Validate the context of an entry point 

The context an entry point sends along is untrusted. Implement \Vkemeter\Opinion\Context\ContextProviderInterface to validate it; supports() decides by the source (frontend, backend). Context of a source without a provider is dropped. The extension ships providers for both sources.

Replace the configuration source 

The configuration is read through \Vkemeter\Opinion\Configuration\ConfigurationProviderInterface ; the default implementation reads the extension configuration. To read it from somewhere else, implement the interface and point the alias to your class in a package that depends on vkemeter/opinion:

EXT:my_sitepackage/Configuration/Services.yaml
services:
  Vkemeter\Opinion\Configuration\ConfigurationProviderInterface:
    alias: MyVendor\MySitepackage\Opinion\ConfigurationProvider
Copied!

Submit endpoint 

POST to the backend AJAX route opinion_submit as multipart/form-data. The route URL contains the request token and is part of the dialog configuration (endpoint).

Field Content
message 10 to 5000 characters
category bug, content, idea or other
severity low, normal or high
source frontend or backend
clientInfo JSON, details collected by the browser
context JSON, context of the entry point
screenshot Optional, PNG or JPEG, at most 10 MB
Status Meaning
200 Stored; delivered tells whether all channels succeeded
401 No backend session (answered by the core)
403 No access or extension disabled
422 Invalid input, errors holds a message per field
503 Screenshot storage unavailable

Running the tests 

The Makefile bundles all tasks, make help lists them:

make install                       # Composer and Node dependencies (Playwright, ESLint)
make test                          # unit, functional and browser tests
make test-functional DB=mariadb    # DB=sqlite|mariadb|mysql|postgres, PHP=8.4|8.5
make check                         # PHP lint, ESLint, coding guidelines, PHPStan, documentation
make fix                           # fix the coding guidelines
make ci                            # everything the CI runs
Copied!

The targets wrap the following commands. The PHP suites run in ghcr.io/typo3/core-testing-* containers with docker or podman, no local PHP or Composer needed:

Build/Scripts/runTests.sh -s composerUpdate
Build/Scripts/runTests.sh -s unit
Build/Scripts/runTests.sh -s functional -d mariadb
Build/Scripts/runTests.sh -s phpstan
Build/Scripts/runTests.sh -s cgl -n
Build/Scripts/runTests.sh -h
Copied!

The browser test runs the dialog against a mocked endpoint in headless Chromium:

npm --prefix Build ci
npx --prefix Build playwright install chromium
node Tests/Browser/dialog.test.mjs
Copied!

ESLint checks the JavaScript of the extension and the browser test, with the configuration Build/eslint.config.mjs:

npm --prefix Build run lint:js
Copied!

Render this documentation locally:

docker run --rm --pull always -v $(pwd):/project -it \
    ghcr.io/typo3-documentation/render-guides:latest --config=Documentation
Copied!

The result is in Documentation-GENERATED-temp/Index.html.