Merge branch 'develop' into 'master'

State after Workshop at TYPO3 Camp Rhein Ruhr 2018

See merge request internal/events/trainings/typo3-extension-workshop!1
This commit is contained in:
Daniel Siepmann 2018-11-05 09:37:54 +01:00
commit a99c42257c
40 changed files with 6554 additions and 0 deletions

4
CodeExamples/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/public
/vendor
/app/*
!/app/typo3conf/sites/default/config.yaml

View file

@ -0,0 +1,39 @@
rootPageId: 1
base: /
baseVariants: {}
languages:
-
title: English
enabled: true
languageId: '0'
base: /
typo3Language: default
locale: en_US.utf8
iso-639-1: en
navigationTitle: English
hreflang: en-US
direction: ''
flag: global
errorHandling: {}
routes: {}
routeEnhancers:
ExamplePlugin:
type: Extbase
extension: ExampleExtension
plugin: Address
defaultController: 'Address::index'
routes:
-
routePath: '/edit/{address}'
_controller: 'Address::edit'
_arguments:
'address': 'address'
-
routePath: '/update'
_controller: 'Address::update'
aspects:
address:
type: PersistedPatternMapper
tableName: 'tx_exampleextension_domain_model_address'
routeFieldPattern: '^(?P<company_name>.+)-(?P<uid>\d+)$'
routeFieldResult: '{company_name}-{uid}'

View file

@ -0,0 +1,44 @@
{
"repositories": [
{
"type": "path",
"url": "localPackages/*"
},
{
"type": "composer",
"url": "https://composer.typo3.org/"
}
],
"name": "website/typo3-extension-workshop",
"description": "Example TYPO3 installation for workshop",
"license": "GPL-2.0-or-later",
"require": {
"helhum/typo3-console": "^5.5.5",
"helhum/typo3-secure-web": "^0.2.7",
"typo3-console/composer-auto-commands": "^0.2.0",
"typo3/cms-core": "^9.5.0",
"typo3/cms-about": "*",
"typo3/cms-belog": "*",
"typo3/cms-beuser": "*",
"typo3/cms-fluid-styled-content": "*",
"typo3/cms-info": "*",
"typo3/cms-info-pagetsconfig": "*",
"typo3/cms-rte-ckeditor": "*",
"typo3/cms-setup": "*",
"typo3/cms-t3editor": "*",
"typo3/cms-tstemplate": "*",
"workshop/example-extension": "@dev"
},
"require-dev": {
"typo3/cms-lowlevel": "*"
},
"extra": {
"typo3/cms": {
"app-dir": "app",
"root-dir": "app",
"web-dir": "public"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

4082
CodeExamples/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,67 @@
<?php
namespace Workshop\ExampleExtension\Controller;
/*
* Copyright (C) 2018 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use Workshop\ExampleExtension\Domain\Model\Address;
use Workshop\ExampleExtension\Domain\Repository\AddressRepository;
class AddressController extends ActionController
{
/**
* @var AddressRepository
*/
protected $addressRepository;
public function __construct(AddressRepository $addressRepository)
{
$this->addressRepository = $addressRepository;
}
public function indexAction()
{
$this->view->assign('addresses', $this->addressRepository->findAll());
}
/**
* @ignorevalidation $address
*/
public function editAction(Address $address)
{
$this->view->assign('address', $address);
}
public function updateAction(Address $address)
{
$this->addressRepository->update($address);
$this->addFlashMessage(
LocalizationUtility::translate('flashSuccess', 'ExampleExtension', [
'companyName' => $address->getCompanyName(),
'street' => $address->getStreet(),
]),
'Update successfully'
);
$this->redirect('index');
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Workshop\ExampleExtension\Controller;
/*
* Copyright (C) 2018 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class ExampleController extends ActionController
{
public function exampleAction()
{
// Use the code below, to output the string.
// Comment the code out, to use fluid template from
// "Resources/Private/Templates/Example/Example.html"
return 'Hello world!';
}
}

View file

@ -0,0 +1,119 @@
<?php
namespace Workshop\ExampleExtension\Domain\Model;
/*
* Copyright (C) 2018 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
class Address extends AbstractEntity
{
/**
* @var string
* @validate NotEmpty
*/
protected $companyName;
/**
* @var string
*/
protected $street;
/**
* @var string
*/
protected $houseNumber;
/**
* @var string
* @validate RegularExpression(regularExpression = '/^[0-9]{5}$/')
*/
protected $zip;
/**
* @var string
*/
protected $city;
/**
* @var string
*/
protected $country;
public function setCompanyName(string $companyName)
{
$this->companyName = $companyName;
}
public function getCompanyName(): string
{
return $this->companyName;
}
public function setStreet(string $street)
{
$this->street = $street;
}
public function getStreet(): string
{
return $this->street;
}
public function setHouseNumber(string $houseNumber)
{
$this->houseNumber = $houseNumber;
}
public function getHouseNumber(): string
{
return $this->houseNumber;
}
public function setZip(string $zip)
{
$this->zip = $zip;
}
public function getZip(): string
{
return $this->zip;
}
public function setCity(string $city)
{
$this->city = $city;
}
public function getCity(): string
{
return $this->city;
}
public function setCountry(string $country)
{
$this->country = $country;
}
public function getCountry(): string
{
return $this->country;
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Workshop\ExampleExtension\Domain\Repository;
/*
* Copyright (C) 2018 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use TYPO3\CMS\Extbase\Persistence\Repository;
class AddressRepository extends Repository
{
}

View file

@ -0,0 +1,15 @@
<?php
(function () {
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
'Workshop.ExampleExtension',
'pluginkey',
'Example Plugin'
);
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
'Workshop.ExampleExtension',
'Address',
'Address Plugin'
);
})();

View file

@ -0,0 +1,101 @@
<?php
return (function (
$extensionKey = 'example_extension',
$tableName = 'tx_workshopexampleextension_domain_model_address'
) {
$extensionLanguagePrefix = 'LLL:EXT:example_extension/Resources/Private/Language/locallang_tca.xlf:';
$coreLanguagePrefix = 'LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:';
return [
'ctrl' => [
'label' => 'company_name',
'default_sortby' => 'company_name',
'tstamp' => 'tstamp',
'crdate' => 'crdate',
'cruser_id' => 'cruser_id',
'title' => $extensionLanguagePrefix . 'address',
'delete' => 'deleted',
'enablecolumns' => [
'disabled' => 'hidden',
'starttime' => 'starttime',
'endtime' => 'endtime'
],
'searchFields' => 'company_name, street, city'
],
'interface' => [
'showRecordFieldList' => 'company_name, street, house_number, zip, city, country'
],
'palettes' => [
'address' => [
'showitem' => implode(',', [
'street, house_number',
'--linebreak--',
'zip, city',
'--linebreak--',
'country',
]),
],
],
'types' => [
'0' => [
'showitem' => implode(',', [
'--div--;' . $coreLanguagePrefix . 'general',
'company_name;;address',
]),
],
],
'columns' => [
'company_name' => [
'label' => $extensionLanguagePrefix . 'company_name',
'config' => [
'type' => 'input',
'max' => 255,
'eval' => 'trim,required',
],
],
'street' => [
'label' => $extensionLanguagePrefix . 'street',
'config' => [
'type' => 'input',
'max' => 255,
'eval' => 'trim,required',
],
],
'house_number' => [
'label' => $extensionLanguagePrefix . 'house_number',
'config' => [
'type' => 'input',
'size' => 10,
'max' => 255,
'eval' => 'trim,required',
],
],
'zip' => [
'label' => $extensionLanguagePrefix . 'zip',
'config' => [
'type' => 'input',
'size' => 10,
'max' => 255,
'eval' => 'trim,required',
],
],
'city' => [
'label' => $extensionLanguagePrefix . 'city',
'config' => [
'type' => 'input',
'max' => 255,
'eval' => 'trim,required',
],
],
'country' => [
'label' => $extensionLanguagePrefix . 'country',
'config' => [
'type' => 'input',
'max' => 255,
'eval' => 'trim,required',
],
],
],
];
})();

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="messages" date="2018-10-01T11:16:33Z" product-name="CodeExamples">
<header/>
<body>
<!-- Form Labels -->
<trans-unit id="labels.companyName" xml:space="preserve">
<source>Company name</source>
</trans-unit>
<trans-unit id="labels.street" xml:space="preserve">
<source>Street</source>
</trans-unit>
<trans-unit id="labels.houseNumber" xml:space="preserve">
<source>House number</source>
</trans-unit>
<trans-unit id="labels.zip" xml:space="preserve">
<source>Zip</source>
</trans-unit>
<trans-unit id="labels.city" xml:space="preserve">
<source>City</source>
</trans-unit>
<trans-unit id="labels.country" xml:space="preserve">
<source>Country</source>
</trans-unit>
<!-- Form Validation -->
<trans-unit id="error.address.companyName.1221560718" xml:space="preserve">
<source>Please provide a company name.</source>
</trans-unit>
<trans-unit id="error.address.zip.1221565130" xml:space="preserve">
<source>Please provide a valid ZIP consisting of 5 digits.</source>
</trans-unit>
<trans-unit id="flashSuccess" xml:space="preserve">
<source>Update des Datensatzes %1$s war Erfolgreich.</source>
</trans-unit>
</body>
</file>
</xliff>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="messages" date="2018-09-05T08:51:01Z" product-name="CodeExamples">
<header/>
<body>
<trans-unit id="address" xml:space="preserve">
<source>Address</source>
</trans-unit>
<trans-unit id="company_name" xml:space="preserve">
<source>Company name</source>
</trans-unit>
<trans-unit id="street" xml:space="preserve">
<source>Street</source>
</trans-unit>
<trans-unit id="house_number" xml:space="preserve">
<source>House number</source>
</trans-unit>
<trans-unit id="zip" xml:space="preserve">
<source>Zip</source>
</trans-unit>
<trans-unit id="city" xml:space="preserve">
<source>City</source>
</trans-unit>
<trans-unit id="country" xml:space="preserve">
<source>Country</source>
</trans-unit>
</body>
</file>
</xliff>

View file

@ -0,0 +1,41 @@
<h3>Editing: {address.companyName}</h3>
<f:form action="update" object="{address}" name="address">
<f:for each="{
0: 'companyName',
1: 'street',
2: 'houseNumber',
3: 'zip',
4: 'city',
5: 'country'
}" as="propertyName">
{f:render(section: 'Field', arguments: {
propertyName: propertyName
})}
</f:for>
<br>
<f:form.submit value="Update" />
</f:form>
<f:section name="Field">
<p>
<label for="{propertyName}">{f:translate(id: 'labels.{propertyName}')}</label>
<br>
<f:form.textfield property="{propertyName}" id="{propertyName}" />
{f:render(section: 'FieldErrors', arguments: {
propertyPath: 'address.{propertyName}'
})}
<br>
</p>
</f:section>
<f:section name="FieldErrors">
<f:form.validationResults for="{propertyPath}">
<f:for each="{validationResults.flattenedErrors}" as="errors">
<f:for each="{errors}" as="error">
{f:translate(id: 'error.{propertyPath}.{error.code}', default: error.code)}
</f:for>
</f:for>
</f:form.validationResults>
</f:section>

View file

@ -0,0 +1,11 @@
<f:flashMessages />
<f:for each="{addresses}" as="address">
<h3>{address.companyName}</h3>
<address>
{address.street} {address.houseNumber}
{address.zip} {address.city}
{address.country}
</address>
<f:link.action action="edit" arguments="{address: address}">Edit</f:link.action>
</f:for>

View file

@ -0,0 +1,21 @@
{
"name": "workshop/example-extension",
"description": "Example for TYPO3 Extension Workshop.",
"type": "typo3-cms-extension",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "Daniel Siepmann",
"email": "coding@daniel-siepmann.de"
}
],
"autoload": {
"psr-4": {
"Workshop\\ExampleExtension\\": "Classes"
}
},
"require": {
"php": ">=7.2.0",
"typo3/cms-core": "*"
}
}

View file

@ -0,0 +1,23 @@
<?php
$EM_CONF['example_extension'] = [
'title' => 'Example extension',
'description' => 'Example for TYPO3 Extension Workshop.',
'category' => 'example',
'version' => '1.0.0',
'state' => 'stable',
'author' => 'Daniel Siepmann',
'author_email' => 'coding@daniel-siepmann.de',
'author_company' => 'Codappix',
'constraints' => [
'depends' => [
'php' => '7.2.0-7.2.999',
'typo3' => '8.7.0-9.5.999',
],
],
'autoload' => [
'psr-4' => [
'Workshop\\ExampleExtension\\' => 'Classes',
]
],
];

View file

@ -0,0 +1,46 @@
<?php
(function () {
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
'Workshop.ExampleExtension',
'pluginkey',
[
'Example' => 'example'
]
);
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
'Workshop.ExampleExtension',
'Address',
[
'Address' => 'index, edit, update'
],
[
'Address' => 'update'
]
);
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig('
mod {
wizards {
newContentElement {
wizardItems {
plugins {
elements {
exampleElement {
iconIdentifier = content-coffee
title = Example title
description = Example Description
tt_content_defValues {
CType = list
list_type = exampleextension_pluginkey
}
}
}
}
}
}
}
}
');
})();

View file

@ -0,0 +1,8 @@
CREATE TABLE tx_exampleextension_domain_model_address (
company_name varchar(255) DEFAULT '' NOT NULL,
street varchar(255) DEFAULT '' NOT NULL,
house_number varchar(255) DEFAULT '' NOT NULL,
zip varchar(255) DEFAULT '' NOT NULL,
city varchar(255) DEFAULT '' NOT NULL,
country varchar(255) DEFAULT '' NOT NULL,
);

View file

@ -0,0 +1,2 @@
Sphinx>=1.3.0,<1.4.0
guzzle_sphinx_theme>=0.7.0,<0.8.0

View file

@ -0,0 +1,111 @@
.. _add-first-plugin:
Add first Plugin
================
Let's get dirty and start with some functionality.
Most extensions provide a *Plugin*, this is a functionality provided as content
element for TYPO3 CMS Frontend. Also *Modules* are available, that is functionality
in TYPO3 CMS Backend.
Also many more parts like *Signals and Slots* or *Hooks* are available. You can also
provide *HashAlgorithms* some *Logger* or *CacheBackends*, etc. TYPO3 CMS can be
extended in many areas.
Still Plugins are a very typical thing to bring in features to your website.
The Plugin
----------
As mentioned, we will start with a simple example plugin to display "Hello World".
Register Plugin in Backend
--------------------------
.. admonition:: Task
Register a plugin in TYPO3 backend.
We first need to register the Plugin in the backend. This way it will become
available as a new option for the content element *Insert Plugin*.
This is done with the following code in file
:file:`Configuration/TCA/Overrides/tt_content.php`:
.. code-block:: php
(function () {
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
'Workshop.ExampleExtension',
'pluginkey',
'Example Plugin'
);
})();
Configure Plugin for Frontend
-----------------------------
.. admonition:: Task
Configure a plugin for TYPO3 frontend.
To actually call some PHP Code when the content element is rendered, we need to
configure the plugin in :file:`ext_localconf.php`:
.. code-block:: php
(function () {
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
'Workshop.ExampleExtension',
'pluginkey',
[
'Example' => 'example'
]
);
})();
Write necessary Code
--------------------
.. admonition:: Task
Remove all PHP Errors, step by step.
If we insert the plugin as content element and open the site, we should see an error
message. This message is not helpful, so we will switch to development context within
the installation / maintenance tool. Also we will add the following TypoScript setup
for our local development instance:
.. code-block:: typoscript
config.contentObjectExceptionHandler = 0
Afterwards we should see the following error message:
Could not analyse class: "Workshop\\ExampleExtension\\Controller\\ExampleController" maybe not loaded or no autoloader? Class Workshop\\ExampleExtension\\Controller\\ExampleController does not exist
This tells us that everything so far has worked as expected. TYPO3 tries to call our
*ExampleController*, which just does not exist yet.
So let's create the controller with the following code in
:file:`Classes/Controller/ExampleController.php`:
.. literalinclude:: ../../CodeExamples/localPackages/example_extension/Classes/Controller/ExampleController.php
:language: php
:lines: 1-27,36
The error message should change to:
An action "exampleAction" does not exist in controller "Workshop\\ExampleExtension\\Controller\\ExampleController".
Yeah, we fixed the error to get the next one. Even if our class exists, the
configured default action does not exist yet, so let's create it.
.. literalinclude:: ../../CodeExamples/localPackages/example_extension/Classes/Controller/ExampleController.php
:language: php
:lines: 26-29,34-
We now should see "Hello world!" in our frontend.
We just created our first plugin.

View file

@ -0,0 +1,231 @@
.. _configuration:
Configuration
=============
There are many different ways and places in TYPO3 for configuration. We will cover
the different places and types of configuration, including when to use a certain way.
By following the API you also make sure modern approaches like configuration loaders
will work with your extensions.
Places of configuration
-----------------------
The following places exist to configure a TYPO3 Extensions:
PHP
^^^
PHP via :file:`LocalConfiguration.php` and :file:`AdditionalConfiguration.php`.
Some extensions allow to place configuration in custom files, e.g. EXT:realurl. I
would call that a bad practice as you have arbitrary places to check for certain
configurations.
Instead use existing places like the two mentioned files above. This is done by
either using:
.. code-block:: php
return [
'EXTCONF' => [
'extkey' => [
// options
],
],
];
This way you can access the configuration via ``$GLOBALS['EXTCONF']['extkey']``.
Or by providing a :file:`ext_conf_template.txt` in the root of your Extension.
The content is TypoScript as documented at :ref:`t3coreapi:extension-options`.
Afterwards you can access the options through an API.
TypoScript
^^^^^^^^^^
TypoScript and Flexforms are merged by Extbase, so you do not have to do any
additional work and combine both. They are available as Variable ``{settings}`` in
all templates. Also inside the Controller they are available as array in
``$this->settings`` out of the box.
.. admonition:: Task
Add some settings and print them in template and controller.
The configuration via TypoScript has to be located at a specific path:
.. code-block:: typoscript
:linenos:
// For all frontend plugins of the extension
plugin {
tx_exampleextension {
settings {
// The configuration goes here
}
}
}
// For a specific frontend plugin of the extension
plugin {
tx_exampleextension_pluginkey {
settings {
// The configuration goes here
}
}
}
// For Backend Modules
module {
tx_exampleextension {
settings {
// The configuration goes here
}
}
}
Extbase itself already has some configuration options available via TypoScript, some
are mentioned at :ref:`t3extbasebook:typoscript_configuration` section of Extbase
book.
.. tip::
The whole ``settings`` array is passed into all templates, layouts and partials.
This way it's possible for integrators to provide arbitary information.
E.g. introduce a new namespace ``codappix`` with project specific settings::
plugin {
tx_exampleextension {
settings {
codappix {
pageUids {
detail = {$pageUids.exampleextension.detail}
}
}
}
}
}
Also it's possible to insert a plugin via TypoScript. In that case the settings can
be provided only for that instance:
.. code-block:: typoscript
:linenos:
lib.instance = USER
lib.instance {
userFunc = TYPO3\CMS\Extbase\Core\Bootstrap->run
extensionName = ExampleExtension
pluginName = pluginkey
vendorName = Workshop
settings {
testKey = testValue
}
}
Flexforms
^^^^^^^^^
Flexforms are like TCA, which will be covered at :ref:`custom-records-tca` section of
:ref:`custom-records`. The format is XML instead of PHP and saved inside the database
field ``pi_flexform`` of the ``tt_content`` record. This way editors are able to
adjust provided settings within a plugin record.
Custom
^^^^^^
Do whatever you want, e.g. use yaml or TypoScript by calling the parser for contents
from anywhere.
When to use which
-----------------
The Flexform approach provides the best UX as it uses the known UI of TYPO3 inside a
record. It should be used if the setting is plugin instance related.
The TypoScript provided the best UX when integrators have to deploy configuration or
configuration is necessary on multiple pages. Also if the plugin is inserted directly
via TypoScript.
The PHP approach is best suited for instance wide configuration, which nearly never
exists. Things like API Keys might depend on the current Domain or Website, and there
can be multiple in a single TYPO3 instance.
.. _configuration-content-wizard:
Adding Content Wizard for our Plugin
------------------------------------
So far we do not have any configuration. But we can use PageTSConfig to make live
easier for editors.
Right now an editor has to insert a new "Insert Plugin" content record and choose our
plugin. We can provide a Configuration to make our Plugin available via the "Create
new content element" wizard.
.. admonition:: Task
Add the new Plugin to content element wizard.
Within :file:`ext_localconf.php` we add the following::
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig('
mod {
wizards {
newContentElement {
wizardItems {
plugins {
elements {
exampleElement {
iconIdentifier = content-coffee
title = Example title
description = Example Description
tt_content_defValues {
CType = list
list_type = exampleextension_pluginkey
}
}
}
}
}
}
}
}
');
This will add the given PageTS as default to global configuration. Within the TS we
define a new element ``exampleElement`` with icon, title and description. The important
part is, that we can define default values (``defValues``) for the creation. This way
we can pre select the plugin.
See: https://docs.typo3.org/typo3cms/TSconfigReference/PageTsconfig/Mod.html#wizards
Available TYPO3 Icons can be found here: https://typo3.github.io/TYPO3.Icons/
.. _configuration-view-paths:
Adjusting view paths
--------------------
In :ref:`views` we covered the conventions for paths. However sometimes you need to
change these paths. E.g. if you exchange a Partial or template.
This can be done via TypoScript, the same way as for ``FLUIDTEMPLATE`` cObject:
.. code-block:: typoscript
:linenos:
plugin {
tx_exampleextension {
view {
templateRootPaths {
10 = EXT:sitepackage/Resources/Plugins/ExampleExtension/Templates/
}
}
}
}
See: https://docs.typo3.org/typo3cms/TyposcriptReference/ContentObjects/Fluidtemplate/Index.html

View file

@ -0,0 +1,248 @@
.. _custom-records:
Custom records
==============
The basics are behind us, now let's get deeper into the system and create a new
record type, like ``tt_address`` which can be displayed through our plugin.
.. _custom-records-tca:
TCA
---
.. admonition:: Task
Create necessary TCA for our new record.
Before we can do anything with Extbase, we need to configure TYPO3. The TCA (=Table
Configuration Array) contains configuration for each database table. TYPO3 will
generate the list view and edit forms within the Backend from this configuration.
Extbase uses this configuration for mapping and database queries, together with
relation handling.
TYPO3 provides a rich documentation about the TCA at
https://docs.typo3.org/typo3cms/TCAReference/. That's why this section is empty, all
information are available there.
One thing to notice is that Extbase uses "Convention over Configuration". While we
can configure Extbase to map a Model to a specific database table, we can auto match
them. For a Model ``\Workshop\ExampleExtension\Domain\Model\Address``, the database
table would be ``tx_exampleextension_domain_model_address``. So this will be
our database table name for our example. Also TYPO3 uses convention over
configuration, so the TCA for this table is placed within
:file:`Configuration/TCA/tx_exampleextension_domain_model_address.php` within our
Extension.
Also each property within the model is written lowerCamelCase, while the database
columns are written snake_case.
Our new record will be an address record with the following fields:
* Company Name
* Street
* House number
* Zip
* City
* Country
Once we finished the TCA, we already can create new records. Only saving them does
not work, as we didn't setup the database yet.
.. note::
By default new records are only allowed on pages of type "Folder".
ext_tables.sql
--------------
.. admonition:: Task
Create necessary sql for our new record.
.. admonition:: Task
Create some records, edit them, play around.
Once the TCA is provided, we need to create the table in our Database.
Each extension can provide a :file:`ext_tables.sql` in the root directory. Within the
admin tools and TYPO3 Console, you can update the database schema to match the
current necessary structure of all extensions.
If multiple extensions adjust the same field, the last one in load order is used.
The example :file:`ext_tables.sql` is:
.. literalinclude:: ../../CodeExamples/localPackages/example_extension/ext_tables.sql
:language: sql
:linenos:
All further TYPO3 specific fields, like ``uid`` and ``pid`` are added by TYPO3 CMS since v9.
Before v9, the file would look like:
.. code-block:: sql
:linenos:
CREATE TABLE tx_exampleextension_domain_model_address (
uid int(11) unsigned NOT NULL auto_increment,
pid int(11) unsigned DEFAULT '0' NOT NULL,
crdate int(11) unsigned DEFAULT '0' NOT NULL,
cruser_id int(11) unsigned DEFAULT '0' NOT NULL,
tstamp int(11) unsigned DEFAULT '0' NOT NULL,
hidden tinyint(3) unsigned DEFAULT '0' NOT NULL,
deleted tinyint(3) unsigned DEFAULT '0' NOT NULL,
starttime int(11) unsigned DEFAULT '0' NOT NULL,
endtime int(11) unsigned DEFAULT '0' NOT NULL,
company_name varchar(255) DEFAULT '' NOT NULL,
street varchar(255) DEFAULT '' NOT NULL,
house_number varchar(255) DEFAULT '' NOT NULL,
zip varchar(255) DEFAULT '' NOT NULL,
city varchar(255) DEFAULT '' NOT NULL,
country varchar(255) DEFAULT '' NOT NULL,
PRIMARY KEY (uid),
KEY parent (pid)
);
We should now be able to create and save new records within the TYPO3 backend. Also
existing records should be listed, searchable and editable.
Model
-----
.. admonition:: Task
Create Model representing our records.
Once we are able to create and edit records in TYPO3 backend, we are ready to go with
Extbase. First we need a representation of our Data. This is done with a Model, in
our case this has to match the table name and is called
``Workshop\ExampleExtension\Domain\Model\Address`` and located at
:file:`Classes/Domain/Model/Address.php`.
Each model is a PHP class, structured like:
.. literalinclude:: ../../CodeExamples/localPackages/example_extension/Classes/Domain/Model/Address.php
:language: php
:linenos:
:lines: 1-4,24-29,31-32,64-68,119
Repository
----------
.. admonition:: Task
Create Repository to access records.
In order to get, update or delete our records, we need a repository. This will return
the models for us. The repository is another class which can be completely empty:
The file is located at :file:`Classes/Domain/Repository/AddressRepository.php`:
.. literalinclude:: ../../CodeExamples/localPackages/example_extension/Classes/Domain/Repository/AddressRepository.php
:language: php
:linenos:
:lines: 1-4, 24-
The parent class already provides all necessary methods for daily use cases.
Controller
----------
.. admonition:: Task
Provide available records to template.
In order to provide records in form of models to our template, we first need an
instance of our repository:
.. literalinclude:: ../../CodeExamples/localPackages/example_extension/Classes/Controller/AddressController.php
:language: php
:linenos:
:lines: 1-4, 24-38,66
With the above code we only can create instances of the controller if an instance of
the Repository is provided.
Extbase itself will analyse dependencies inside ``__construct`` and will provide
instances. This is called Dependency Injection and works in three different ways with
Extbase. The above one is the preferred as this is not Extbase specific but will also
work in other PHP Frameworks and without any Dependency Injection at all.
We then can call the accordingly method to fetch records, which then can be assigned
to the view:
.. literalinclude:: ../../CodeExamples/localPackages/example_extension/Classes/Controller/AddressController.php
:language: php
:linenos:
:lines: 28-29,40-43,66
The ``AddressRepository`` extends the base ``Repository`` class and inherits some
methods, e.g. ``findAll()``.
Template
--------
With our records in our template, we can iterate over them to display them.
:file:`Resources/Private/Templates/Address/Index.html`:
.. literalinclude:: ../../CodeExamples/localPackages/example_extension/Resources/Private/Templates/Address/Index.html
:language: html
:linenos:
:lines: 1-7,10
Configure storage pid
---------------------
We should not see any addresses yet, thats due to the generated database query by
Extbase. If no storage pid is configured, Extbase will fetch records from pid 0.
Within the content element we can select arbitrary "Record Storage Page" entries to
use for records.
We could also configure the pid via TypoScript:
.. code-block:: typoscript
:linenos:
plugin {
tx_exampleextension {
persistence {
storagePid = 2
}
}
}
Add new plugin
--------------
You might have noticed that the above controller is not the same as in our first
example. We therefore can add the controller to the existing plugin or add a new
plugin for this controller.
I would recommend to create a new plugin, to separate things. The process is not
explained again. If you struggle, take a look at :ref:`add-first-plugin` again.
Check everything
----------------
Once everything is set up, the following should be possible:
* Create, edit, search and delete records within TYPO3 Backend.
* List available records via Frontend Plugin.
Sounds like a lot of work for a small benefit? Right. If all you have to achieve is
this, you should not use Extbase but "pure" TYPO3. But we will extend the Extension
in the next step. Also this is about a first simple Extbase Extension, not how to use
TYPO3 the right way.

View file

@ -0,0 +1,74 @@
Extension
=========
First of all we have to understand what an extension is, in the context of TYPO3 CMS.
What is an extension?
---------------------
See :ref:`t3coreapi:extension-architecture` in TYPO3 Core API Reference.
TYPO3 is built only with extensions, there is no framework below. All features are
assigned to a specific extension. This way it's possible to build the TYPO3 that fits
the project needs.
An extension is something like ``frontend`` or ``backend``, which provides the TYPO3
frontend or backend. It can also be ``extbase`` which works as an Framework
to build further extensions or ``fluid`` an template engine.
Nowadays most installations also have a ``site_`` or ``sitepackage`` extensions, which
encapsulates the systems configuration and resources like assets and templates. Thus
an TYPO3 extension is the same as an composer package.
In this workshop we will concentrate on a "typical" extension that will provide a
plugin and custom record types. This can be installed into any compatible TYPO3
installation. A new record type will be added, which can be edited in the TYPO3
backend. Also a new plugin will be added which can be added as a content element and
displayed in frontend.
Structure of an extension
-------------------------
.. code-block:: plain
extension_key
├── Classes
│   ├── Command
│   │   └── ExampleCommandController.php
│   ├── Controller
│   │   └── ExampleController.php
│   └── Domain
│      └── Model
│         └── Example.php
├── composer.json
├── Configuration
│   ├── TCA
│   │   └── Overrides
│   │   └── tt_content.php
│   └── TypoScript
│   ├── constants.typoscript
│   └── setup.typoscript
├── Documentation
├── ext_conf_template.txt
├── ext_emconf.php
├── ext_localconf.php
├── ext_tables.php
├── readme.rst
└── Resources
   └── Private
   └── Templates
   └── Search
   └── Search.html
See :ref:`t3coreapi:extension-files-locations` in TYPO3 Core API Reference.
Further resources
-----------------
* https://extensions.typo3.org/about-extension-repository/what-are-extensions/
* :ref:`t3coreapi:extension-architecture` in TYPO3 Core API Reference.
* :ref:`t3coreapi:extension-files-locations` in TYPO3 Core API Reference.
* https://docs.typo3.org/typo3cms/ExtbaseFluidBook/Index.html

View file

@ -0,0 +1,17 @@
Outlook
=======
If we have some time left, we can take a deeper look at some of these topics.
.. toctree::
:maxdepth: 2
:numbered:
Outlook/Routing
Outlook/Dependency-Injection
Outlook/Automated-Testing
Outlook/Property-Mapper
Outlook/Command-Controllers
Outlook/Backend-Modules
Outlook/Fluid/Custom-ViewHelper
Outlook/Configuration/Mapping-DB

View file

@ -0,0 +1,6 @@
Automated Testing
=================
See:
* https://github.com/DanielSiepmann/testing-talk/

View file

@ -0,0 +1,6 @@
Backend Modules
===============
See:
* https://docs.typo3.org/typo3cms/ExtbaseFluidBook/10-Outlook/2-Backend-modules.html

View file

@ -0,0 +1,6 @@
Command Controllers
===================
See:
* https://docs.typo3.org/typo3cms/ExtbaseFluidBook/10-Outlook/3-Command-controllers.html

View file

@ -0,0 +1,6 @@
Configuration Mapping DB
========================
See:
* https://docs.typo3.org/typo3cms/ExtbaseFluidBook/b-ExtbaseReference/Index.html#persistence

View file

@ -0,0 +1,8 @@
Dependency Injection
====================
See:
* https://daniel-siepmann.de/Posts/2017/2017-08-17-typo3-injection.html
* https://daniel-siepmann.de/Posts/Migrated/2015-10-20-extbase-inject-settings.html

View file

@ -0,0 +1,6 @@
Fluid - Custom ViewHelper
=========================
See:
* https://docs.typo3.org/typo3cms/ExtbaseFluidBook/8-Fluid/8-developing-a-custom-viewhelper.html

View file

@ -0,0 +1,8 @@
Property Mapper
===============
See:
* https://docs.typo3.org/typo3cms/ExtbaseFluidBook/10-Outlook/4-Property-mapping.html
* https://github.com/DanielSiepmann/extbase-the-good-parts/blob/master/Tests/Unit/PropertyMappingTest.php

View file

@ -0,0 +1,38 @@
Routing
=======
This section is related to TYPO3 v9.5.0 LTS, which introduces routing into the TYPO3
core.
See: https://docs.typo3.org/typo3cms/extensions/core/latest/Changelog/9.5/Feature-86365-RoutingEnhancersAndAspects.html
An example for the example extension looks like:
.. code-block:: yaml
:linenos:
routeEnhancers:
ExamplePlugin:
type: Extbase
extension: ExampleExtension
plugin: Address
defaultController: 'Address::index'
routes:
-
routePath: '/edit/{address}'
_controller: 'Address::edit'
_arguments:
'address': 'address'
-
routePath: '/update'
_controller: 'Address::update'
aspects:
address:
type: PersistedPatternMapper
tableName: 'tx_exampleextension_domain_model_address'
routeFieldPattern: '^(?P<company_name>.+)-(?P<uid>\d+)$'
routeFieldResult: '{company_name}-{uid}'
This defines two routed, one for edit and one for update. Also there is no cHash in
URls due to the configuration. The address is replaced by company name and uid within
the url.

View file

@ -0,0 +1,85 @@
Start new extension
===================
We will start with the simplest example ``Hello World``. Once we understood the
basics, we will create an "address" extension to manage a custom record.
Necessary files
---------------
Extensions consists of a folder and the single necessary file, which is
:file:`ext_emconf.php`. This configures the *Extension Manager*. Without this file,
the Extension Manager would not recognize the extension and would prevent
installation.
.. literalinclude:: ../../CodeExamples/localPackages/example_extension/ext_emconf.php
:language: php
See :ref:`t3coreapi:extension-declaration` in TYPO3 Core API Reference.
.. admonition:: Task
So let's create a new folder and add the file within the folder.
In this example I'll use :file:`example_extension` as folder name.
Install extension
-----------------
Once we have created the first extension, we need to install the extension. There are
two ways for a local extension. Either placing the extension inside the installation,
or via composer.
.. admonition:: Task
Install the new extension.
Oldschool
^^^^^^^^^
Copy the extension to :file:`typo3conf/ext/`, and head over to *Extension Manager* to
activate the extension.
Via Composer
^^^^^^^^^^^^
The following project setup is suggested:
.. code-block:: text
.
├── composer.json
└── localPackages
   └── example_extension
:file:`composer.json`:
.. literalinclude:: ../../CodeExamples/composer.json
:language: json
In this case, we also need a :file:`composer.json` inside our extension, to make the
extension an composer package and allow the installation:
:file:`composer.json`:
.. literalinclude:: ../../CodeExamples/localPackages/example_extension/composer.json
:language: json
Thanks due ``typo3-console/composer-auto-commands`` our extension is activated already.
Autoloading
-----------
Using composer, TYPO3 does not do any special. The autoloading is provided by
composer and can be configured as documented by composer.
If you are not using composer, you should provide autoloading information inside
:file:`ext_emconf.php`:
.. literalinclude:: ../../CodeExamples/localPackages/example_extension/ext_emconf.php
:language: php
:lines: 1-3,18-
There you can follow the composer autoloading configuration.
You can find the composer documentation about autoloading at https://getcomposer.org/doc/04-schema.md#autoload .

View file

@ -0,0 +1,54 @@
Understand TYPO3 Plugins
========================
What happened until now?
We created an extension and a very basic plugin.
The plugin is created with two API calls and a Controller class with a single action
method.
What exactly are the API calls doing? And what does the PHP code in our Controller do
so far? Let's understand TYPO3!
TYPO3 Backend
-------------
The TYPO3 Backend needs to provide a new Plugin to the editor, this is done within
:file:`Configuration/TCA/Overrides/tt_content.php`.
The API configures the TCA (=Table Configuration Array) for ``tt_content``. And adds
the new plugin as ``list_type``.
We can go further on the content element during the Topic :ref:`configuration`,
especially :ref:`configuration-content-wizard`.
TYPO3 Frontend rendering
------------------------
Also we need to configure the handling of our plugin during the rendering of TYPO3
frontend. This is done within :file:`ext_localconf.php`.
The API configures TypoScript ``tt_content.list.20.<pluginSignature>`` to define
rendering of the new registered ``list_type``.
Extbase bootstrap will be started with extension and plugin name.
Also the configuration of callable controller actions and caching is stored in
``$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions']``.
The controller
--------------
TypoScript will call Extbase, which will figure out to call the ``exampleAction``
method of our ``ExampleController``, thanks to our frontend rendering configuration.
The default action is always the first action of the first controller in the
configuration. Multiple actions will be shown later.
The Controller should extend the ``ActionController`` to work as expected out of the
box. Also all "actions" must have the suffix ``Action`` and need to be public.
As soon as an action returns a string, this will be the output. If nothing is
returned, which is ``null`` Extbase will try to find and render a template matching
our current controller and action. This is done at
``\TYPO3\CMS\Extbase\Mvc\Controller\ActionController::callActionMethod``.

View file

@ -0,0 +1,300 @@
.. highlight:: php
Updating records
================
For everything we have done so far, you do not need a plugin at all. Custom records
only need :file:`ext_tables.sql` the TCA and TypoScript for rendering.
Extbase is needed if you provide interaction to the user, e.g. updating or adding
records.
Even that can nowadays be achieved using the system extension "Form". Still we will
cover how to update a record next.
We need a form where we can adjust the values of the record, e.g. change the
company name.
Therefore we will add a new ``editAction()``. This will receive a single ``Address``
for editing and provides the form. We also add an ``updateAction()`` which receives a
single ``Address``. This action is the target of the submit form and will update the
database record.
To start editing an address, we will add an link from :file:`index.html` to
``editAction()``.
Link to another action
----------------------
.. admonition:: Task
Create a link to the ``editAction()`` providing the "current" Address record.
TYPO3 provides ViewHelpers to create links to different actions. To insert a Link to
edit a record, you could use the following:
.. literalinclude:: ../../CodeExamples/localPackages/example_extension/Resources/Private/Templates/Address/Index.html
:language: html
:linenos:
:dedent: 4
:lines: 9
The ViewHelper generates a link, thanks to the current plugin context, all arguments
are prefixed with the plugin namespace.
Creating Forms
--------------
.. admonition:: Task
Create a form which will show the current values of a single ``Address`` to the
user.
TYPO3 also provides ViewHelpers to create forms. Of course you could create forms with
pure HTML, but TYPO3 / Fluid adds some security aspects and makes things easier.
E.g. a proper validation that forms were not manipulated are added out of the box.
Also you do not need to take care of proper names of the inputs to allow Extbase to
map incoming inputs.
A basic form looks like:
.. code-block:: html
:linenos:
<f:form action="update" object="{address}" name="address">
<f:form.textfield property="companyName" />
<f:form.submit value="Update" />
</f:form>
Persistence
-----------
.. admonition:: Task
Save changes to database.
Once the user submits the form, the ``updateAction()`` method within our controller is
called. We therefore have to implement this method and to persist the changes that
were submitted.
We already have an instance of the accordingly repository within our controller. We
also receive the modified object as an argument within our action. All we have to do
is to update the record within the repository::
public function updateAction(Address $address)
{
$this->addressRepository->update($address);
}
At the end of the "request", Extbase will cleanup everything and persist the updates
to the backend, which in our case is MySQL.
Redirect
--------
.. admonition:: Task
Redirect back to index.
All our changes are already saved to database, but the user receives an error that no
template could be found. We actually do not need any template for our
``updateAction()``. Instead we will redirect the user back to the ``indexAction()``.
This way he can check whether the change has effected the output and works as
expected.
.. note::
Following REST, an update returns the updated resource, which is the uri to the
resource, in our case the redirect.
As Browsers do not support ``PATCH``, which would be the request method, we use
``POST``, see: https://en.wikipedia.org/wiki/Representational_state_transfer#Relationship_between_URL_and_HTTP_methods
As we extend ``ActionController`` we can use the following line to redirect to
another action::
$this->redirect('index');
Cache clearing
--------------
Even if the user is redirected, he does not see any difference. That's due to TYPO3
caching.
.. admonition:: Task
Make changes visible in index action.
Extbase, by default, clears the cache for all updated records. Therefore the page
cache for the pages holding the records is cleared. As our plugin resists on a
different page, we have to configure TYPO3.
Same is true for plain TYPO3 Backend. As soon as a record is edited, the page cache
is cleared. If this record is displayed on another page, the caching has to be
configured, so this is not Extbase specific.
The caching can be configured using Page TS Config:
.. code-block:: typoscript
TCEMAIN {
clearCacheCmd = 1
}
See: https://docs.typo3.org/typo3cms/TSconfigReference/PageTsconfig/TceMain.html#clearcachecmd
Flash message
-------------
.. admonition:: Task
Inform user about what happened.
We now have a fully working process. Still in a long list of records, the user might
not notice a difference. Also if he leaves the computer and comes back, he will not
know what was done before.
Extbase has a feature called "Flashmessages" which are also used within TYPO3
Backend. They inform a user on next page about some thing that happened during
the last request. We could use that to add a message about which record was updated.
This is also just one line within a controller::
$this->addFlashMessage(
$address->getCompanyName() . ' was updated.',
'Update successfully'
);
Adding alone would not work, so we have to display thus messages. This is done within
the View with an ViewHelper:
.. code-block:: html
<f:flashMessages />
Validation
----------
.. admonition:: Task
Prevent invalid data.
Up till now, the user could provide any text into any property. There was no
validation whether a zip is actual valid.
Adding this is done within either the model, or the controller.
Within Controller
^^^^^^^^^^^^^^^^^
Sparely used, this makes sense if you do not use models at all or use the same model
with different validation rules.
The process is the same as within a model, just the annotations are added to the
PHPDoc of the corresponding action.
We will not cover this, instead we use validation within model.
Within Model
^^^^^^^^^^^^
Each property already has a type annotation::
/**
* @var string
*/
By adding one, or multiple, ``@validate`` annotations, these properties get
validated::
/**
* @var string
* @validate NotEmpty
*/
Extbase provides some validators our of the box, all available within ``typo3/sysext/extbase/Classes/Validation/Validator``:
* AlphanumericValidator
* EmailAddressValidator
* NotEmptyValidator
* NumberRangeValidator
* RawValidator
* RegularExpressionValidator
* StringLengthValidator
* TextValidator
Also validators for PHP Type like ``String`` or ``DateTime`` are provided which are
auto added based on ``@var`` annotation.
Let's say a zip only consists of integers and is exactly 5 integers long, like in
Germany. One or more leading 0 are allowed, we therefore will not use the PHP type
``integer`` but ``string``. A possible validation might look like::
/**
* @var string
* @validate RegularExpression(regularExpression = '/^[0-9]{5}$/')
*/
protected $zip;
Also see: https://docs.typo3.org/typo3cms/extensions/core/Changelog/9.3/Feature-83167-ReplaceValidateWithTYPO3CMSExtbaseAnnotationValidate.html
Display validation errors
-------------------------
Nearly finished, we can no longer save invalid records. Still the user does not get
any information about what's wrong. Fluid by default will add the css class
``f3-form-error`` to all inputs with an error. So one could style this css class:
.. code-block:: css
.f3-form-error {
border: solid 5px #cd2323;
}
This way at least it's clear which fields fail, but not why. We therefore use another
ViewHelper to add the validation errors to each field. As we have to add the same
markup for each field, we will put it into a section for re-use. On larger projects
this might be a Partial:
.. code-block:: html
:linenos:
<f:section name="FieldErrors">
<f:form.validationResults for="{propertyPath}">
<f:for each="{validationResults.flattenedErrors}" as="errors">
<f:for each="{errors}" as="error">
<li>{error.code}: {error}</li>
</f:for>
</f:for>
</f:form.validationResults>
</f:section>
This section can be used like:
.. code-block:: html
:linenos:
<f:form.textfield property="companyName" />
{f:render(section: 'FieldErrors', arguments: {
propertyPath: 'address.companyName'
})}
Handling existing invalid records
---------------------------------
In some circumstances your system might have an invalid record. Right now it's not
possible to edit this record with ``editAction()`` as Extbase will validate the
record.
Therefore the ``@ignorevalidation`` annotation can be added to the action::
/**
* @ignorevalidation $address
*/
public function editAction(Address $address)
{
$this->view->assign('address', $address);
}
This way Extbase will ignore raised validation issues and we are ready to go to edit
the record.

View file

@ -0,0 +1,223 @@
.. _views:
Views
=====
So far we only have our "Hello world!" output hardcoded in our PHP Code.
That's not helpful, so let's introduce Views with Fluid.
Fluid
-----
Fluid is the TYPO3 Template engine. Nowadays it's standalone, but was developed for
and by TYPO3.
It follows XML / HTML which should make it easier to get started.
Fluid templates are valid HTML and custom tags, called ViewHelpers, are introduced to
bring in logic.
Convention over configuration
-----------------------------
Extbase follows the principle "Convention over configuration", which we already saw
with our Controller. We didn't configure the path or class name, it just follows a
convention.
Same is true for the output of plugins. If "something" is returned, this will be the
output. If nothing is returned, Extbase will call Fluid to render a Fluid template.
Paths
-----
The path to the template of an controller action is
:file:`example_extension/Resources/Private/Templates/ControllerName/ActionName.html`,
which in our example would be: :file:`example_extension/Resources/Private/Templates/Example/Example.html`,
.. admonition:: Task
Move the output to a fluid template, following Extbase conventions.
So let's create the file and move the "Hello world!" to this file. We should make a
small change, otherwise we will not see whether our change has worked. E.g. make the
"w" uppercase "W".
Do not forget to remote the ``return 'Hello world!';`` from our controller.
We should now see our "Hello World!".
Configuration
-------------
Awesome, we now do no longer need to touch PHP code to change the output, we can use
Fluid and an Integrator is able to change something.
But they should be able to change templates in their own extension, e.g. a
"sitepackage". We will see how to do this in next chapter :ref:`configuration`,
especially :ref:`configuration-view-paths`.
Sections
--------
If templates grow in size, we need to add some structure. One way is to use sections
inside a single template. A section is like a PHP method or function and can be
called with arguments:
.. code-block:: html
:linenos:
Normal output
{f:render(
section: 'FirstSection',
arguments: {
arg1: var1,
arg2: var2,
arg3: 'string'
}
)}
<f:section name="FirstSection">
Some output + {arg1}.
</f:section>
We have our default output "Normal output" and call a ViewHelper ``f:render`` with
some arguments to render a specific section with some arguments. The ViewHelper will
be replaced with the rendered result of the section.
This way it's possible to structure templates like Controllers. They control the
output flow and call different sections with arguments where more specific logic
happens.
Variables
---------
Variables are assigned via PHP:
.. code-block:: php
:linenos:
// Inside a controller action do:
$this->view->assign('var1', $variable1);
$this->view->assign('var2', $variable2);
// Or to assign multiple variables at once:
$this->view->assignMultiple([
'var1' => $variable1
'var2' => $variable2
]);
Assigned variables can be accessed inside Fluid with curly braces:
.. code-block:: html
:linenos:
Hello {userInput}!
ViewHelper
----------
To make templates more flexible, ViewHelpers are available. They are custom HTML-Tags
available inside the template engine.
TYPO3 and Fluid already ship some ViewHelpers, but you can provide own ViewHelpers.
ViewHelpers always live in a Namespace, e.g. ``\TYPO3\CMS\Fluid\ViewHelpers`` or
``\Workshop\ExampleExtension\ViewHelpers``.
You can either register these namespaces globally, or inside the templates via
``{namespace wee=Workshop\ExampleExtension\ViewHelpers}``.
The ``f`` namespace for ``Fluid`` is always registered globally.
Once ViewHelpers are available available, you can use them:
.. code-block:: html
<f:format.crop maxCharacters="5">Hello World!</f:format.crop>
The above should output "Hello ...", as the string is cropped to 5 characters, the
"..." can be configured via another argument of the ViewHelper:
.. code-block:: html
<f:format.crop maxCharacters="5" append="">Hello World!</f:format.crop>
Beside the tag based kind of inserting ViewHelpers, you can also use the "inline
notation":
.. code-block:: html
:linenos:
{f:format.date(date: 'midnight')}
It's also possible to chain ViewHelpers in both ways:
.. code-block:: html
:linenos:
{f:format.date(date: 'midnight') -> f:format.raw()}
<f:format.raw>
{f:format.date(date: 'midnight')}
</f:format.raw>
<f:format.raw>
<f:format.date date="midnight" />
</f:format.raw>
<f:format.raw>
<f:format.date>midnight</f:format.date>
</f:format.raw>
Partials and Layouts
--------------------
We already saw sections to make a single template easier to manage.
For re-using parts between multiple templates there are Partials.
Partials are like templates and can be rendered via:
.. code-block:: html
:linenos:
Normal output
{f:render(
partial: 'Path/To/Partial',
arguments: {
arg1: var1,
arg2: var2,
arg3: 'string'
}
)}
Also each template can be embedded into a Layout via:
.. code-block:: html
:linenos:
<f:layout name="Layout/Path/AndName" />
This way wrapping code, e.g. for HTML E-Mails or content elements can be moved to a
layout and all templates can inherit this layout.
Further resources
-----------------
.. hint::
Use ViewHelpers for output logic, not to get data into your View.
Use Controller and DataProcessing to prepare data.
* Available ViewHelpers can be found at:
* :file:`typo3/sysext/fluid/Classes/ViewHelpers/`
* :file:`vendor/typo3fluid/src/ViewHelpers/`
* https://github.com/TYPO3/Fluid
* https://docs.typo3.org/typo3cms/TyposcriptReference/ContentObjects/Fluidtemplate/Index.html
* https://docs.typo3.org/typo3cms/ExtbaseFluidBook/Index.html

View file

@ -0,0 +1,3 @@
.admonition-task {
border-left-color: #ad7526;
}

View file

@ -0,0 +1,323 @@
# -*- coding: utf-8 -*-
#
# TYPO3 Extension Workshop documentation build configuration file, created by
# sphinx-quickstart on Mon Aug 13 12:04:20 2018.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.githubpages',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'TYPO3 Extension Workshop'
copyright = u'2018, Daniel Siepmann'
author = u'Daniel Siepmann'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u'1.0.0'
# The full version, including alpha/beta/rc tags.
release = u'1.0.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = []
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# -- Options for HTML output ----------------------------------------------
import guzzle_sphinx_theme
html_theme_path = guzzle_sphinx_theme.html_theme_path()
html_theme = 'guzzle_sphinx_theme'
# Register the theme as an extension to generate a sitemap.xml
extensions.append("guzzle_sphinx_theme")
# Guzzle theme options (see theme.conf for more information)
html_theme_options = {
# Set the name of the project to appear in the sidebar
"project_nav_name": "TYPO3 Extension Workshop",
}
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
# html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents.
# "<project> v<release> documentation" by default.
#html_title = u'TYPO3 Extension Workshop v1.0.0'
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
#html_last_updated_fmt = None
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
#html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# 'ja' uses this config value.
# 'zh' user can custom change `jieba` dictionary path.
#html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'TYPO3ExtensionWorkshopdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'TYPO3ExtensionWorkshop.tex', u'TYPO3 Extension Workshop Documentation',
u'Daniel Siepmann', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'typo3extensionworkshop', u'TYPO3 Extension Workshop Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'TYPO3ExtensionWorkshop', u'TYPO3 Extension Workshop Documentation',
author, 'TYPO3ExtensionWorkshop', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
't3coreapi': ('https://docs.typo3.org/typo3cms/CoreApiReference/', None),
't3extbasebook': ('https://docs.typo3.org/typo3cms/ExtbaseFluidBook/', None),
}
# Allow inline PHP highlighting
# See: https://github.com/guzzle/guzzle/blob/master/docs/conf.py
from sphinx.highlighting import lexers
from pygments.lexers.web import PhpLexer
lexers['php'] = PhpLexer(startinline=True, linenos=1)
# Add custom css
def setup(app):
app.add_stylesheet('css/custom.css')

View file

@ -0,0 +1,44 @@
Welcome to TYPO3 Extension Workshop
===================================
This workshop is about the basics of TYPO3 extensions, using Extbase and Fluid.
Some "rules" for the workshop
-----------------------------
* You will write all the code yourself. The repository is available though, if you need
help or are to lazy… .
* We will not call APIs without checking out their code. Always understand what your
own code does.
* I'm using the latest TYPO3 CMS 9 LTS, most of the parts are so basic, they should
work from 4.5 onwards.
* Ask questions as soon as possible. This way we have the context.
* Tell me if you want less details.
* All materials are available on Github: https://github.com/DanielSiepmann/typo3-extension-workshop
* When should we make breaks? Any Smokers here?
Topics
------
We will cover the following topics during the workshop, to build a foundation to
build custom extensions:
.. toctree::
:maxdepth: 2
:numbered:
Extension
StartNewExtension
AddFirstPlugin
UnderstandTypo3Plugins
Views
Configuration
CustomRecords
UpdatingRecords
Outlook