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.
On this page
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
Answer the prompts (vendor name, extension key, etc.). The kickstarter generates
the directory structure, composer. and the boilerplate files you need.
Since TYPO3 v14 you do not need ext_ unless you plan to publish
your extension to the TYPO3 Extension Repository.
See also
Creating a new extension from scratch covers the full scaffolding process, including manual setup without the kickstarter.
Step 2: Create the domain model
Add a class extending
\TYPO3\ to
Classes/. Properties map to database columns by name.
<?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;
}
}
Key points:
- Properties must be
protected, notpublic— 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.
See also
Step 3: Create the repository
For a basic repository, extending
\TYPO3\ is all you need.
The naming convention is mandatory: a model named
Event must have a
repository named
Event in the
\Domain\
namespace.
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Domain\Repository;
class EventRepository extends \TYPO3\CMS\Extbase\Persistence\Repository {}
The base class provides
find,
find,
find, and
find out of the
box. The old magic
find methods were removed in TYPO3 v14.
See also
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
$event maps to
event_).
Tip
The kickstarter generates TCA and SQL for you if you define your model
properties during scaffolding. Use
vendor/ to
add a model to an existing extension.
See also
TCA Reference for the full TCA reference.
Step 5: Create the controller
Controllers live in Classes/ and extend
\TYPO3\. Each public method
ending in
Action is automatically available as a plugin action.
Inject dependencies via the constructor.
<?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();
}
}
- Assign variables to the view with
$this->view->assign.() - Return
$this->htmlto render the Fluid template.Response () - Typed action arguments are automatically resolved from the request —
passing an
intUID in the URL results in a hydratedEventobject in the action.
Step 6: Add Fluid templates
Create the template files Extbase expects by convention:
-
-
-
-
List.fluid.html
-
Show.fluid.html
-
-
-
-
Default.fluid.html
-
-
Partials/
-
The template name matches the action name — for example
list maps
to List.. Variables assigned in the controller are available
directly in the template.
<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>
See also
Fluid — the full Fluid templating reference, including all built-in ViewHelpers.
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_ approach is gone.
ext_localconf.php tells Extbase which controller actions the plugin
may call:
<?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'],
],
);
Configuration/TCA/Overrides/tt_content.php registers the plugin as a
content element in the backend:
<?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',
);
See also
Step 8: Configure routing (optional but recommended)
Without a route enhancer, URLs contain the raw plugin namespace parameters
(for example: ?tx_).
Add an Extbase route enhancer to your site configuration to get
clean URLs like /events/.
routeEnhancers:
EventList:
type: Extbase
extension: MyExtension
plugin: EventList
defaultController: 'Event::list'
routes:
- routePath: '/events'
_controller: 'Event::list'
- routePath: '/events/{event}'
_controller: 'Event::show'
aspects:
event:
type: PersistedAliasMapper
tableName: tx_myextension_domain_model_event
routeFieldName: slug
See also
Routing for Extbase plugins — the full routing chapter with detailed examples and common mistakes.
What next?
You have a working extension. From here:
- Core concepts of Extbase — understand the MVC and ORM patterns underlying everything above.
- Querying the database with Extbase — write custom repository queries with ordering, filtering, and limits.
- Validation in Extbase — validate model properties and action arguments automatically.
- Caching for Extbase plugins — understand how caching works for your plugin and what your responsibilities are.