Extbase quick start for experienced developers 

You know TYPO3, you know PHP, you just want the steps. This page gets a minimal but fully working Extbase extension in front of you as quickly as possible — a list and detail view for a custom record type, registered as a frontend plugin.

For the reasoning behind each step, follow the links into the relevant chapters.

Step 1: Scaffold the extension 

Use the FriendsOfTYPO3 kickstarter package to generate the extension skeleton:

composer require friendsoftypo3/kickstarter --dev
vendor/bin/typo3 make:extension
Copied!

Answer the prompts (vendor name, extension key, etc.). The kickstarter generates the directory structure, composer.json and the boilerplate files you need. Since TYPO3 v14 you do not need ext_emconf.php unless you plan to publish your extension to the TYPO3 Extension Repository.

Step 2: Create the domain model 

Add a class extending \TYPO3\CMS\Extbase\DomainObject\AbstractEntity to Classes/Domain/Model/. Properties map to database columns by name.

EXT:my_extension/Classes/Domain/Model/Event.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Event extends AbstractEntity
{
    protected string $title = '';
    protected string $description = '';
    protected ?\DateTimeImmutable $eventDate = null;

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getDescription(): string
    {
        return $this->description;
    }

    public function getEventDate(): ?\DateTimeImmutable
    {
        return $this->eventDate;
    }
}
Copied!

Key points:

  • Properties must be protected, not public — Extbase uses getters and setters to access them.
  • Do not set default values or initialise properties in the constructor. Extbase bypasses the constructor when hydrating objects from the database.
  • Use typed properties. Extbase reads the type declarations to map values correctly.

Step 3: Create the repository 

For a basic repository, extending \TYPO3\CMS\Extbase\Persistence\Repository is all you need. The naming convention is mandatory: a model named Event must have a repository named EventRepository in the \Domain\Repository namespace.

EXT:my_extension/Classes/Domain/Repository/EventRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

class EventRepository extends \TYPO3\CMS\Extbase\Persistence\Repository {}
Copied!

The base class provides findAll(), findByUid(), findBy(array $criteria), and findOneBy(array $criteria) out of the box. The old magic findBy[PropertyName]() methods were removed in TYPO3 v14.

Step 4: Define the database table (TCA) 

Create Configuration/TCA/tx_myextension_domain_model_event.php with the column definitions matching your model properties.

Since TYPO3 v13, database columns are auto-created from TCA definitions — you no longer need to define every field in ext_tables.sql. Check the database analyser after installation to confirm the generated schema matches your expectations. If a column needs a non-default type or index, declare it explicitly in ext_tables.sql and it will take precedence.

The TCA column names must match the property names of your model (camelCase properties map to snake_case columns by default — for example $eventDate maps to event_date).

Step 5: Create the controller 

Controllers live in Classes/Controller/ and extend \TYPO3\CMS\Extbase\Mvc\Controller\ActionController . Each public method ending in Action is automatically available as a plugin action. Inject dependencies via the constructor.

EXT:my_extension/Classes/Controller/EventController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Model\Event;
use MyVendor\MyExtension\Domain\Repository\EventRepository;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class EventController extends ActionController
{
    public function __construct(
        private readonly EventRepository $eventRepository,
    ) {}

    public function listAction(): ResponseInterface
    {
        $this->view->assign('events', $this->eventRepository->findAll());
        return $this->htmlResponse();
    }

    public function showAction(
        Event $event,
    ): ResponseInterface {
        $this->view->assign('event', $event);
        return $this->htmlResponse();
    }
}
Copied!
  • Assign variables to the view with $this->view->assign().
  • Return $this->htmlResponse() to render the Fluid template.
  • Typed action arguments are automatically resolved from the request — passing an int UID in the URL results in a hydrated Event object in the action.

Step 6: Add Fluid templates 

Create the template files Extbase expects by convention:

  • EXT:my_extension/Resources/Private/

    • Templates/

      • Event/

        • List.fluid.html
        • Show.fluid.html
    • Layouts/

      • Default.fluid.html
    • Partials/

The template name matches the action name — for example listAction() maps to List.fluid.html. Variables assigned in the controller are available directly in the template.

EXT:my_extension/Resources/Private/Templates/Event/List.fluid.html
<f:for each="{events}" as="event">
    <h2>{event.title}</h2>
    <p>{event.eventDate -> f:format.date(format: 'd.m.Y')}</p>
    <f:link.action action="show" arguments="{event: event}">
        Read more
    </f:link.action>
</f:for>
Copied!

Step 7: Register the plugin 

Two calls are required — one in ext_localconf.php, one in Configuration/TCA/Overrides/tt_content.php.

Since TYPO3 v14, plugins are registered as dedicated content types ( CType) rather than as subtypes of the old "General Plugin" element. The old list_type approach is gone.

ext_localconf.php tells Extbase which controller actions the plugin may call:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Controller\EventController;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

defined('TYPO3') or die();

ExtensionUtility::configurePlugin(
    'MyExtension',
    'EventList',
    [
        EventController::class => ['list', 'show'],
    ],
);
Copied!

Configuration/TCA/Overrides/tt_content.php registers the plugin as a content element in the backend:

EXT:my_extension/Configuration/TCA/Overrides/tt_content.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

defined('TYPO3') or die();

ExtensionUtility::registerPlugin(
    'MyExtension',
    'EventList',
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:plugin.eventlist.title',
    'EXT:my_extension/Resources/Public/Icons/Extension.svg',
);
Copied!

What next? 

You have a working extension. From here: