---
title: "Developer corner"
manual: "Opinion"
version: "main"
permalink: "https://docs.typo3.org/permalink/vkemeter/opinion:developer@main"
source: "Developer/Index.rst"
rendered: "2026-10-23T14:21:05+00:00"
---

# Developer corner {#developer}

## How a submission is processed {#developer-architecture}

1.  An entry point (admin panel, backend toolbar) renders a trigger and the
    dialog configuration, built by
    `\Vkemeter\Opinion\Dialog\DialogConfigurationBuilder`.
1.  The JavaScript dialog posts the form to the backend AJAX route
    `opinion_submit`.
1.  `\Vkemeter\Opinion\Controller\SubmitController` checks access,
    builds and validates the submission and validates the context with the
    matching context provider.
1.  `\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 {#developer-entry-point}

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

```php
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],
    );
}
```

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:

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

openOpinionDialog(configuration);
```

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 {#developer-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**

```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');
    }
}
```

## Validate the context of an entry point {#developer-context-provider}

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 {#developer-configuration-provider}

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**

```yaml
services:
  Vkemeter\Opinion\Configuration\ConfigurationProviderInterface:
    alias: MyVendor\MySitepackage\Opinion\ConfigurationProvider
```

## Submit endpoint {#developer-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 {#developer-tests}

The `Makefile` bundles all tasks, `make help` lists them:

```bash
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
```

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:

```bash
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
```

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

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

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

```bash
npm --prefix Build run lint:js
```

Render this documentation locally:

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

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