Showing posts with label TYPO3. Show all posts
Showing posts with label TYPO3. Show all posts

Thursday, June 23, 2022

Using TYPO3 FluidEmail in CLI context

Since TYPO3 10.4 it is possible to use TYPO3\CMS\Core\Mail\FluidEmail to send emails with body content rendered by Fluid. This all works fine as long as you work in frontend or backend context, but when you use FluidEmail in CLI context (e.g. in a symfony console command), you will run into some problems. Since no real server request object is available, TYPO3 is not able to construct URIs using Fluid ViewHelpers nor will the Image ViewHelper render working absolute image links. Especially the last problem (non working website logo in rendered FluidEmail default template) motivated me to dig deeper into the topic. 

Below follows a summary of things you should consider when working with FluidEmail (or Fluid StandaloneView) in CLI context.

All Fluid ViewHelpers in the namespace f:uri or f:link do not work by default

The ViewHelpers depend on either a TypoScriptFrontendController object or the global TYPO3_REQUEST variable being available. Both are not in CLI context. 

One workaround for this problem is to create an instance of $GLOBALS['TYPO3_REQUEST'] manually in your symfony console command as shown below:


$site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId(1);
$request = (new ServerRequest())
    ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
    ->withAttribute('site', $site);
$GLOBALS['TYPO3_REQUEST'] = $request;

Note, that the site object is fetched by the uid of page 1. This must be kept in mind, when you have a TYPO3 website with multiple sites defined.

Link created with f:link.action must always contain the extensionName

If the previous workaround is used, it is mandatory to set the "extensionName" argument in the f:link.action or f:uri.action ViewHelper.

Create links manually

An alternative to the previous example, where the $GLOBALS['TYPO3_REQUEST'] object is created manually, is to create all links manually used in the FluidEmail template in the symfony console command. This can done as shown below:


$arguments = [
    'tx_sfeventmgt_pieventdetail' => [
        'action' => 'detail',
        'controller' => 'Event',
        'event' => 25,
    ],
];

$site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId(1);
$eventLink = (string)$site->getRouter()->generateUri(22, $arguments);

Note again, that the site object is fetched by the uid of page 1. The variable $eventLink can then be passed to the view and be used as href attribute for a-tags.

Image ViewHelper creates broken URLs

When you try to render an image with an absolute path using the f:image ViewHelper, the resulting link will be broken. Example:


<f:image absolute="1" src="EXT:sf_event_mgt/Resources/Public/Icons/Extension.svg" width="100" />

In CLI context this will create an img tag as shown below


<img src="http://./typo3/sysext/core/bin/typo3conf/ext/sf_event_mgt/Resources/Public/Icons/Extension.svg" width="100" height="104" alt="">

In order to create a workaround for the problem, I found 2 different approaches.

Workaround 1

FluidEmail has the function setRequest() which allows to set a ServerRequest object for the view. 


$site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId(1);

$normalizedParams = new NormalizedParams(
    [
        'HTTP_HOST' => $site->getBase()->getHost(),
        'HTTPS' => $site->getBase()->getScheme() === 'https' ? 'on' : 'off',
    ],
    $systemConfiguration ?? $GLOBALS['TYPO3_CONF_VARS']['SYS'],
    '',
    ''
);

$request = (new ServerRequest())
    ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
    ->withAttribute('normalizedParams', $normalizedParams)
    ->withAttribute('site', $site);
    
$email->setRequest($request);

By assigning the request object to the view, the variable {normalizedParams.siteUrl} is now also available in CLI context resulting in the TYPO3 logo being shown again in the default email layout. 

Workaround 2

The f:image ViewHelper will create relative links to images when the argument "absolute" is not used. So basically just the baseUri is missing. In the previous workaround {normalizedParams.siteUrl} was used to add the baseUri, but instead it is also possible to assign the sites baseUri as Fluid variable as shown below:


$site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId(1);
$email->assign('baseUri', (string)$site->getBase());

In the template the full path to the image can now be created as following:


<img src="{baseUri}{f:uri.image(src: 'EXT:sf_event_mgt/Resources/Public/Icons/Extension.svg', width: '100')}" width="100" />

Ensure to set the correct application context in CLI

Whenever the site object is fetched using the SiteFinder, the resulting base is resolved respecting TYPO3s application context. By default this is "production". If you e.g. have a testing or staging environment and use one of the described workarounds, ensure to set the application context as environment variable as shown below:

export TYPO3_CONTEXT=Development


Friday, January 21, 2022

How to use multiple SMTP accounts in one TYPO3 installation

When TYPO3 is used to serve multiple websites in one installation, it may sometimes be required to configure multiple SMTP accounts in order to send emails from TYPO3 (e.g. mailforms or notifications) to different recipients. This may especially be important, when the recipient mailserver has a strict spam filter or when the domain uses a SPF, DKIM or DMARC and the mailserver only accepts emails from thrusted sources.

In TYPO3 you can configure one global SMTP server in LocalConfiguration.php by using the following settings:

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport'] = 'smtp';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_server'] = 'your.mailserver.tld';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_encrypt'] = true;
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_username'] = 'username';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_password'] = 'password';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] = 'email@your.mailserver.tld';

This setting however reflects to any hosted website in your TYPO3 installation and the email-server for typo3-website1.tld may possible not accept emails with a sender from the domain typo3-website2.tld.

In order to provide multiple SMTP servers for different websites in a TYPO3 installation, I configure different SMTP servers in AdditionalConfiguration.php 


if (($_SERVER['SERVER_NAME'] ?? '') === 'typo3-website1.tld') {
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_server'] = 'mail.typo3-website1.tld';
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_encrypt'] = true;
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_username'] = 'username';
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_password'] = 'password';
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] = 'email@typo3-website1.tld';
}

if (($_SERVER['SERVER_NAME'] ?? '') === 'typo3-website2.tld') {
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_server'] = 'mail.typo3-website2.tld';
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_encrypt'] = true;
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_username'] = 'username';
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_password'] = 'password';
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] = 'email@typo3-website2.tld';
}

Since AdditionalConfiguration.php is evaluated on every request, TYPO3 will conditionally use the email settings depending on the $_SERVER['SERVER_NAME'] variable.

Note, that this solution only applies to web requests and does not work in CLI context (e.g. scheduler task).

Wednesday, January 19, 2022

TYPO3 - Multiple dynamic parameters for a typolink using a custom userFunc

I often use the TYPO3 linkHandler to enable the possibility for editors to create direct links to records from within the CKEditor in TYPO3 backend. This is all well documented and easy to configure using the RecordLinkHandler, as long as the resulting link only contains one dynamic parameter. But sometimes it may be required to have multiple dynamic parameters for the resulting link. In this case you may need to create a userFunc for the typolink function in order to create a custom configuration which uses multiple dynamic parameters.

Requirement

Let us assume, you have an event which has multiple event registrations. Registrations are listed in the detail view of an event and each registration is shown as an accordion item with a unique ID in markup. Now you want to create a link to an event and set a link anchor to a specific registration. The resulting URL should be as shown below:

https://www.cool-events.tld/events/my-first-event#registration-1

Calling the URL will open the event detail page and scroll down the the HTML element with the ID "registration-1".

Note: This is just an example, which also can be achieved without a custom userFunc. Goal of this article is to demonstrate how to use a userFunc for typolink.

Solution

In order to archive the requirement, first a linkHandler Page TSConfig must be created as shown below:


TCEMAIN.linkHandler {
    event {
        handler = TYPO3\CMS\Recordlist\LinkHandler\RecordLinkHandler
        label = Event Registration
        configuration {
            table = tx_sfeventmgt_domain_model_registration
        }
    }
}

Next, the TypoScript for the link generation is added. 


config {
  recordLinks {
    registration {
      typolink {
        parameter = 1
        userFunc = DERHANSEN\SfEventMgt\UserFunc\TypoLink->createEventLink
        userFunc {
          eventUid = TEXT
          eventUid.data = field:event
          registrationUid = TEXT
          registrationUid.data = field:uid
        }
      }
    }
  }
}

Finally a custom userFunc needs to be created which renders the A-tag for the link.


<?php

declare(strict_types=1);

namespace DERHANSEN\SfEventMgt\UserFunc;

use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;

class TypoLink
{
    private const EVENT_DETAILPID = 22;

    public ContentObjectRenderer $cObj;

    public function createEventLink(array $content, array $config): string
    {
        $eventUid = $this->cObj->cObjGetSingle($config['eventUid'], $config['eventUid.']);
        $registrationUid = $this->cObj->cObjGetSingle($config['registrationUid'], $config['registrationUid.']);

        // Link parameters (can also contain multiple dynamic parameters)
        $parameters = [
            'tx_sfeventmgt_pieventdetail' => [
                'controller' => 'Event',
                'action' => 'detail',
                'event' => $eventUid,
            ]
        ];

        $link = $this->cObj->typoLink($this->cObj->lastTypoLinkResult->getLinkText(), [
            'parameter' => self::EVENT_DETAILPID,
            'additionalParams' => '&' . http_build_query($parameters),
            'section' => 'registration-' . $registrationUid,
            'returnLast' => 'url',
        ]);

        return '<a href="' . $link . '">';
    }
}

The most important part is, that the custom userFunc must only return the opening A-tag. In the userFunc, it is basically possible to construct the resulting link however you want. In the example above, 2 dynamic parameters are used in the function ($eventUid and $registrationUid). It is of course also possible to e.g. do dynamic database lookups in the function to fetch other dynamic parameters required for link construction.


Wednesday, December 29, 2021

How to manually create the default crop variant string for an imported image in TYPO3 CMS

When data in TYPO3 is created automatically (e.g. through a custom API or by an import script), it is very common, that also new files (especially images) are imported. TYPO3 has the well documented FAL (File Abstraction Layer), which provides an API for common tasks. 

One typical task is to import an image to FAL and next creating a file reference for the imported image to a record (e.g. event record of ext:sf_event_mgt). Such a task is easy to implement in a custom extension when you follow the example documentation from the TYPO3 FAL API. This all works fine as described, as long as the referenced image does not use Crop Variants. For image fields, where crop variants are configured, no crop variants will be created for imported images and as a result, TYPO3 will always use the aspect ratio of the imported image no matter which crop variant is configured for output.

TYPO3 internals

Available crop variants for image fields are defined in TCA. When an editor adds an image to a record in the TYPO3 backend, TYPO3 will automatically calculate the default crop variants. The result is saved to the table sys_file_reference in the field crop as a JSON encoded string like the shown example crop variant string below:

{"heroimage":{"cropArea":{"x":0,"y":0.23,"width":1,"height":0.54},"selectedRatio":"16:9","focusArea":null},"teaserimage":{"cropArea":{"x":0,"y":0.14,"width":1,"height":0.72},"selectedRatio":"4:3","focusArea":null}}

This process if performed in ImageManipulationElement. When an editor for example edits a record with an imported image and opens an image inline element, then the ImageManipulationElement is rendered and the default crop variant string is calculated for the imported image and saved, as soon as the editor saves the record.

Manually calculating the default crop variant string

In order to manually calculate the default crop variant string in an image import process, it is required to extract 2 code snippets from the ImageManipulationElement, since both are not public available:

  1. The default crop configuration - see code
  2. The function populateConfiguration - see code
Both is extracted to a class named CropVariantUtility and an additional public function is added which returns the crop variant string for a given file as shown below:

/**
 * Returns a crop variant string to be used in sys_file_reference field "crop" for the given file and table/fieldname
 *
 * @param File $file
 * @param string $tableName
 * @param string $fieldname
 * @return string
 * @throws InvalidConfigurationException
 */
public static function getCropVariantString(File $file, string $tableName, string $fieldname): string
{
    $config = $GLOBALS['TCA'][$tableName]['columns'][$fieldname]['config']['overrideChildTca']['columns']['crop']['config'];
    $cropVariants = self::populateConfiguration($config);
    $cropVariantCollection = CropVariantCollection::create('', $cropVariants['cropVariants']);
    if (!empty($file->getProperty('width'))) {
        $cropVariantCollection = $cropVariantCollection->applyRatioRestrictionToSelectedCropArea($file);
    }

    return (string)$cropVariantCollection;
}

The whole class is available in this gist.

Finally the new CropVariantUtility can be used in a file import routine as shown in the example below:


protected function addFileToEvent(File $file, int $eventUid): void
{
    $eventRecord = BackendUtility::getRecord(self::EVENT_TABLE, $eventUid);

    $fileReferenceUid = StringUtility::getUniqueId('NEW');

    $dataMap = [];
    $dataMap['sys_file_reference'][$fileReferenceUid] = [
        'table_local' => 'sys_file',
        'tablenames' => self::EVENT_TABLE,
        'uid_foreign' => $eventUid,
        'uid_local' => $file->getUid(),
        'fieldname' => 'image',
        'pid' => $eventRecord['pid'],
        'show_in_views' => 0,
        'crop' => CropVariantUtility::getCropVariantString($file, self::EVENT_TABLE, 'image'),
    ];

    $dataMap[self::EVENT_TABLE][$eventUid] = [
        'image' => $fileReferenceUid
    ];

    $this->dataHandler->start($dataMap, []);
    $this->dataHandler->process_datamap();
}

The example code will create a new file reference for the given file object for the table  self::EVENT_TABLE (tx_sfeventmgt_domain_model_event in this case) and the given event UID. The usage of the new CropVariantUtility ensures, that the new file relation has a default crop variant string and configured crop variants can directly be used for imported images.

Sunday, August 15, 2021

"Unterminated nested statement!" using TYPO3 rector

TYPO3 rector is a really helpful application when it comes to TYPO3 major updates. It helps you to identify and refactor TYPO3 deprecations in custom extensions and can save hours of manual refactoring. I use TYPO3 rector quite a lot and stumbled across the following error recently.

"Unterminated nested statement!"







This message is not really helpful, so I digged deeper into the problem. The "Parser.php" throwing the exception is located in "helmich/typo3-typoscript-parser" package, so I first thought that there was a problem with the TypoScript in the desired extension, but after checking ever line manually, I could not find any error. 

It came out, that the extension I wanted to process by TYPO3 rector had a "node_modules" folder, which contained a lot of Typescript (not TypoScript) files. Those files where obviously parsed by the TypoScript parser resulting in the shown error message. After removing (excluding should also work) the "node_modules" folder, everything worked as expected.

If you like rector and/or TYPO3 rector, please consider to support the authors.
 

Friday, June 4, 2021

How to use constructor dependency injection in a XCLASSed TYPO3 class

Some time ago in needed to extend an Extbase controller in TYPO3 10.4 which used dependency injection through constructor injection. So I used XCLASS to extend the original controller and added an own constructor which added an additional dependency, but this obviously did not work out properly, since the constructor was always called with the amount of arguments from the original class.

Later I created this issue on TYPO3 forge in order to find out if this is a bug/missing feature or if I missed something in my code. In order to demonstrate the problem, I created this small demo extension which basically just extended a TYPO3 core class using XCLASS and just days later, a solution for the issue was provided.

The solution is pretty simple and you just have to ensure to add a reference to the extended class in the Services.yaml file of the extending extension.

Example:


  TYPO3\CMS\Belog\Controller\BackendLogController: '@Derhansen\XclassDi\Controller\ExtendedBackendLogController'

The complete Services.yaml file can be found here.

Thanks a lot to Lukas Niestroj, who pointed out the solution to the problem.


Monday, November 16, 2020

How to extend existing FlexForm select options of a TYPO3 plugin using Page TSconfig

Sometimes existing options of a TYPO3 plugin may not fully suite the project requirements. As an example, I refer to my TYPO3 extension "Event Management and Registration" (sf_event_mgt). The extension allows to select the ordering of records by a specific field in the FlexForm plugin options as shown on the screenshot below.


The 3 options shown are configured in the Flexform options for the field "settings.orderField".

In a project it was required to order by a custom field which was not part of the main extension. So I added the custom field named "slot" to the extension using an extending extension for sf_event_mgt.

In order to allow the new field as sorting field, the field "slot" needs to be added to the allowed ordering fields using TypoScript (note, this step is only specific to the extension sf_event_mgt).


plugin.tx_sfeventmgt {
  settings {
    orderFieldAllowed = uid,title,startdate,enddate,slot
  }
}

Finally the FlexForm of the plugin needs to be extended, so the new field appears in the "Sort by" select field. In order to do so, the following Page TSconfig has been added:


TCEFORM.tt_content.pi_flexform.sfeventmgt_pievent.sDEF.settings\.orderField {
  addItems.slot = slot
  altLabels.slot = Slot
}

You might notice the backslash before the dot in "settings\.orderField". This is required to escape the dot of the fieldname "settings.orderField", since Page TSconfig also uses dots to separate between configuration options/levels.

After adding the Page TSconfig, the plugin now shows the new field.

Pretty cool and not a single line of PHP code required :-) 

Reference: TYPO3 TCEFORM

Tuesday, May 5, 2020

Unit-, Functional- and Acceptance-Tests for a TYPO3 Extension with GitHub Actions

Back in 2017 at TYPO3 Camp Munich I held a talk about Unit-, Functional- and Acceptance-Tests for a TYPO3 Extension with GitLab CI. I never really used that setup for my Open Source Extensions, since they all are hosted on GitHub. But since november 2019 GitHub Actions are available, so I finally took some time to migrate my GitLab CI Build Pipeline to GitHub Actions. The results of this migration process is available on GitHub and summarized in this blogpost.

To keep things simple, I created a little demo Extension for TYPO3 to make the setup as easy and understandable as possible.

All in all, the results are very satisfying and the build process is really fast without the requirement to use additional docker images (e.g. MySQL or Selenium Standalone). GitHub has really done a great job by providing preconfigured hosted runners with lots of useful tools 👍




The GitHub Repository with all sources and the GitHub Actions workflow is available at https://github.com/derhansen/gha_demo.

During creation of the setup, I ran into some issues, that took me some time to figure out. All issues are easy to resolve and I summarized them in the "Good to know"-section at the end of this article.

TYPO3 demo extension "gha_demo"


The repository includes a very simple TYPO3 extension that basically does nothing special. It has a simple domain model with just one field and a simple plugin that shows all records of the domain model. The extension has the following tests

  • Unit Tests for the domain model
  • Functional Tests for the repository
  • Acceptance Tests (based on codeception) for the plugin output

Before I created the GitHub Actions workflow, I ensured that all tests execute properly in my local development environment.

GitHub-hosted virtual environments


GitHub hosted runners are preconfigured runners that already contain a lot of available software (e.g. composer, PHP in various versions, Google Chrome, Selenium) that can be used to test an application. No need to puzzle around with building or pulling docker images that contain requirements and no waste of build time to install required packages.

For the gha_demo TYPO3 extension I use the Ubuntu 18.04 LTS runner only without any other docker images.

Workflow YAML file


It is very easy to enable GitHub Actions for a repository. You create a directory called .github/workflows and add a YAML file with your workflow configuration that must follow the Workflow Syntax for GitHub Actions.

The workflow YAML file I created for this article is (hopefully) self explaining:
https://github.com/derhansen/gha_demo/blob/master/.github/workflows/ci.yml

The workflow uses 3 GitHub actions. The first action "actions/checkout" just checks out the GitHub repository.

The second action "actions/cache" ensures, that Composer cache files are shared for builds. You just have to configure a unique key for the cache and I choose to use the hash of the composer.json as a key, so every time dependencies change, the cache is rebuilt. To ensure, that the cache is working you should see "loading from cache" in the output of the composer command.



What helps when you want to debug your workflow is to save build artifacts. For this I use the third action "actions/upload-artifact" which uploads the log of the PHP server and the Codeception output if the build failed.

All other steps in the workflow are based on commands that are executed on the runner (e.g. start MySQL Server, Update Composer, ...).

You may note, that the workflow contains 2 "sleep" commands. Both are required so previous commands have enough time to finish execution (start PHP Server and start Selenium).

Another thing you may note is, that I added many TYPO3 packages to the require-dev section of my composer.json file. This is not a requirement and can be moved to an additional build step (e.g. composer require typo3/cms-filelist ....).

Acceptance Tests with Codeception


In order to execute the Codeception Acceptance Tests, it is required to setup a fully working TYPO3 website including a preconfigured database dump with e.g. pages and records to test. For the Acceptance Tests I included the following files/folders in Tests/Acceptance/_data

  • config/ 
    TYPO3 Sites configuration
  • typo3conf/LocalConfiguration.php
    Preconfigured LocalConfiguration PHP that matches the environment and settings (e.g. DB Credentials) for GitHub Actions
  • typo3.sql
    Full Database dump of my local test TYPO3 website

To separate between Acceptance Test environments (local and GitHub) there are configuration settings for both in Tests/Acceptance/_env

At this point I would like to thank Daniel Siepmann for sharing his GitLab CI configuration about Acceptance Tests. I adapted some parts of his examples to my current setup.

Update 29.05.2020: If you want to test an extension against multiple TYPO3 versions, you can use a build matrix as shown in this example from Sebastian Fischer.

Good to know


#1 - Composer dependencies are not cached during builds

Update 15.05.2020: Not required any more, since the issue is fixed.

Due to a misconfiguration in the Ubuntu 18.04 runner (that has already been fixed), the .composer directory is owned by root:root with 775 rights. This makes it impossible for the runner user to write into that directory. To fix this, make sure to remove the the directory recursive as shown below in a build step before composer is executed.


- name: Delete .composer directory
  run: |
    sudo rm -rf ~/.composer


#2 - PHP server with "php -S" is obviously not starting


I used "php -S 0.0.0.0:8888 -t .Build/public/ &> php.log.txt &" to start a PHP server that serves my application for Acceptance Tests. Somehow the acceptance tests step was not able to connect to the given port and always showed "Failed to connect to x.x.x.x port 8888: Connection refused”

To solve this issue, I forced the workflow to stop for 2 seconds (just added "sleep 2;" right after the PHP -S line) so PHP has enough time to server the application.


#3 - MySQL credentials not accepted / MySQL "Connection refused"


Setting up a build step that uses MySQL I ran into problems connecting to the MySQL server that comes with the default Ubuntu 18.04 runner. The solution to this problem was really simple, since you just have to start the MySQL service.


- name: Start MySQL
  run: sudo /etc/init.d/mysql start

The default credentials for the MySQL are root:root

Wednesday, April 29, 2020

How to add a replacement for the removed TCA Option "setToDefaultOnCopy" in TYPO3 10.4

The TYPO3 TCA Option "setToDefaultOnCopy" has been removed in TYPO3 10 in order to reduce the amount of checks in DataHandler and the amount of available options in TCA. The documentation says, that "This option was only there for resetting some `sys_action` values to default, which can easily be achieved by a hook if needed. If an extension author uses this setting,
this should be achieved with proper DataHandler hooks."

I use this option in one of my extensions. Basically, I have one "main" record, that has one Inline field with various "subrecords". Those "subrecords" are user generated and should not be copied, when the main record is copied, so I had to find out which DataHandler hooks should be used to get the removed functionality back for the TYPO3 10 compatible version of my extension.

After some hours with several breakpoints in the TYPO3 DataHandler I came to the conclusion, that this may not be as "easy" as described, since there is no Hook, where you can unset certain field values during the copy (or localize) process. And if there was, then another problem would have shown up, since relation fields are processed different (basically the relation is resolved using TCA) on copy or translation commands in DataHandler.

Knowing the last about TCA however makes it possible to hook into the process. At a very early stage in DataHandler, I use processCmdmap_preProcess to set the TCA type for affected fields to "none" as shown below:

public function processCmdmap_preProcess($command, $table, $id, $value, $pObj, $pasteUpdate)
{
    if (in_array($command, ['copy', 'localize']) && $table === 'tx_extension_table') {
        $GLOBALS['TCA']['tx_extension_table']['columns']['fieldname1']['config']['type'] = 'none';
        $GLOBALS['TCA']['tx_extension_table']['columns']['fieldname2']['config']['type'] = 'none';
    }
}

With this configuration in TCA, the affected fields are completely ignored by the copy/localize command in DataHandler. It is now just important to change the field types back after the command is finished in processCmdmap_postProcess hook as shown below:

public function processCmdmap_postProcess($command, $table, $id, $value, $pObj, $pasteUpdate, $pasteDatamap)
{
    if (in_array($command, ['copy', 'localize']) && $table === 'tx_extension_table') {
        $GLOBALS['TCA']['tx_extension_table']['columns']['fieldname1']['config']['type'] = 'text';
        $GLOBALS['TCA']['tx_extension_table']['columns']['fieldname2']['config']['type'] = 'inline';
    }
}

Hard to say, if this is a good approach to get the functionality back. It feels not really right to change existing TCA at runtime as shown, but at least, I could not find any downsides in the solution and it works fine for me.

Thursday, April 2, 2020

How to limit the TYPO3 category tree to a subset of categories for extension records

In many TYPO3 projects I've been working in, the TYPO3 category system is used to structure content by one or multiple categories. A typical category tree I often see is build up as shown in the example below:

Full TYPO3 category tree

This is a very plain way to create a category tree and the structure in the example is limited to 3 independent main categories (Events, Products, Staff). 

Quite often, the shown example category tree is used system wide in TYPO3 and all main categories are shown for all record types. This can be confusing for editors, since when you for example want to assign categories for e.g. event records, why should one see and be able to select the categories "Products" and "Staff" including all subcategories?

Fortunately TYPO3 can be configured to limit the category tree for tables to a given root category. As an example, I limit the shown categories for event records to only "Event" categories. I assume that the category "Events" has the sys_category UID 1.

PageTS example

TCEFORM.tx_sfeventmgt_domain_model_event.category.config.treeConfig.rootUid = 1

In PageTS such configuration options can be set for any record as long as the following configuration path is met: TCEFORM.[tableName].[fieldName].[propertyName]

The fieldName is usually "categories" or "category", but this can also be different depending on how categories are implemented in 3rd party extensions.

The PageTS setting can also be set in TCA as shown below.

TCA example

$GLOBALS['TCA']['tx_sfeventmgt_domain_model_event']['columns']['category']['config']['treeConfig']['rootUid'] = 1;

As a result, the category tree for event records is now limited to the category "Events" and all subcategories.

TYPO3 category tree limited to a subcategories of one main category

I think this is way more clear for an editor than it was before. In general, this can be configured for every table in the TYPO3 TCA (e.g. pages, files, extensions, ...)

The configuration only allows to define one UID as root for the category tree. If more flexibility is needed to limiting the category tree, then TCEFORM.[tableName].[fieldName].[config][foreign_table_where] may be the place to add own custom conditions.

Friday, November 22, 2019

TYPO3 Extension "Plain FAQ" released

Today I released the first public version of my latest TYPO3 Extension called "Plain FAQ". The name already says it - the extension is a very simple solution to manage Frequently Asked Questions in TYPO3. Below follow some of the facts about the Extension:
  • Compatible with TYPO3 8.7 and 9.5
  • Based on Extbase and Fluid
  • Covered with unit and functional tests
  • Easy usage for editors
  • Uses TYPO3 system categories to structure FAQs by category
  • Field for media and files
  • Possibility to add related FAQs
  • Configurable template layouts for the views
  • Automatic cache cleanup when a FAQ article has been updated in backend
  • Signal slots to extend the extension with own functionality
  • Symfony Console commands to migrate from ext:irfaq
The extension is available on TER and packagist.

Migration from "Modern FAQ (irfaq)"

If you currently use the TYPO3 Extension "Modern FAQ (irfaq)" you may have noticed, that the extension is not compatible to TYPO3 9.5 (last Extension-Update in March 2018)  and the architecture is quite old (AbstractPlugin, Marker Based Templates and TypoScript configuration for wraps). 

For users where "Modern FAQ (irfaq)" is a blocker for an upcoming TYPO3 9.5 Update, it is possible to migrate to "Plain FAQ" using the Symfony Console Commands included in "Plain FAQ". The migration is as easy as the usage of the extension:

1. Migrate existing Categories to sys_category
2. Migrate existing FAQs to "plain_faq" records
3. Migrate existing Plugins including Plugin settings

The migration may not cover all possible scenarios (e.g. Ratings, Question asked by, irfaq Plugin settings set by TypoScript), but is for sure a good starting point in order to migrate existing records. I guess, for most websites the included migration will suite without further work on migrated data.

You can find details about the migration process in the Extension Manual.

Thanks for sponsoring


I would like to thank Julius-Maximilians-Universität Würzburg for sponsoring the initial development of the TYPO3 extension. Thanks for supporting TYPO3 and open source software!







Tuesday, July 10, 2018

TYPO3 usage statistics for july 2018

TR;DR - I analyzed 48.146.633 websites for TYPO3 usage - the results with aggregated charts can be found here.

I'm proud to introduce my latest side project called t3versions. It is a Python (Django) web application to identify, if a website is running TYPO3 and if so, which major version is being used. As a result, TYPO3 websites are saved to the database in order to create an aggregated overview with several charts.

Beside the live check in the frontend part, the application also contains a task queue, which can analyze multiple websites in the background an on multiple servers. I imported a list with 48.146.633 domains of european websites to the task queue and after 21 days, all websites were analyzed.

As a result, t3versions did find 292.629 websites using TYPO3. Those websites are located on servers with 61.269 individual IP Addresses. The archived results including 6 charts (Version overview, Supported version, TLDs, Webservers, Countries and SSL) can be found here.

I will reschedule the background check on a regular basis. If someone can provide me a list with e.g. Asian or US domains, feel free to contact me.

Sunday, April 15, 2018

TYPO3 MySQL database import fails with "Index column size too large. The maximum column size is 767 bytes"

I recently migrated a TYPO3 7.6 Website to TYPO3 8.7 and while importing the migrated TYPO3 database on the production server, the import failed with the following MySQL error:

ERROR 1709 (HY000) at line 2060: Index column size too large. The maximum column size is 767 bytes.

The error occurred for the import of the TYPO3 table sys_refindex. After some research and local debugging I found out, that I locally was using MySQL 5.7 and the production server was using MySQL 5.6, but settings in regard to innodb_large_prefix and innodb_file_format were equal.

In order to import the dump from my MySQL 5.7 server to the production MySQL 5.6 Server, I executed the following SQL query before creating the MySQL dump:  

ALTER TABLE sys_refindex ROW_FORMAT=DYNAMIC;

After setting the ROW_FORMAT to DYNAMIC, the database dump from MySQL 5.7 could finally be imported without errors on the MySQL 5.6 production server.

Wednesday, April 11, 2018

TYPO3 extension sf_event_mgt version 3.0 released

Today I finally released the new version 3.0 of my TYPO3 extension sf_event_mgt - Event management and registration. The new version comes with tons of new features, bugfixes and improvements and also contains 2 breaking changes, so make sure to read the release notes.

Thanks to everyone, who contributed to the extension over the last few months. Also a special thanks to Alex Kellner for his extension powermail, from which I adapted ideas and some code for the registration fields feature.


New features 

Below follows some of the new features of sf_event_mgt 3.0.

Registration fields

Im order to make the extension more easy to use for non-programmers, I added the possibility for editors add additional registration fields to the default registration form on event basis.

Registration fields
Creating of registration fields works basically as in powermail. The user can add registrations fields in a new tab as shown in the screenshot above and choose one of the following field types: input, textarea, radio and checkbox.  When a participant registers to an event, all filled out registration fields are saved to the registration record in the TYPO3 backend.



allowLanguageSynchronization for TYPO3 8.7

The TCA settings allowLanguageSynchronization has been introduced in TYPO3 8.6 and is the successor for "l10n_mode=mergeIfNotBlank". Note, that sf_event_mgt did not use "l10n_mode=mergeIfNotBlank", so the language synchronization feature is only available in TYPO3 8.7



Signals

Finally, I also added signals in various actions, so it is now easily possible to modify/extend variables given to the desired views.



Monday, February 5, 2018

TYPO3 - How to render a Fluid standalone view multiple times in different languages

Back in 2015, I wrote 2 blogposts (first and second) about rendering a localized Fluid standalone view in a scheduler task (commandController). The main problem was to render a Fluid standalone view multiple times within the same request but with different languages. Back then, my solution was to create an own ViewHelper and a modified version of the TYPO3 LocalizationUtility which were responsible for handling the localization changes during the rendering request.

Meanwhile, I got feedback from readers of my blog pointing me to a more simple solution for the problem.

The main problem with changing the TYPO3 backend language during one request is the language cache, which is only initialized once for the current language. So when you switch the backend language multiple times, the cached language files for the previous language will still be used.

The solution is to unset the language cache for the extension you are rendering your Fluid standalone view from. In order to do so, you have to extend the TYPO3 LocalizationUtility with a new function as shown below.


class LocalizationUtility extends \TYPO3\CMS\Extbase\Utility\LocalizationUtility
{
    /**
     * Resets the language cache for the given extension key
     *
     * @param string $extensionName
     */
    public static function resetLocalizationCache($extensionName)
    {
        unset(static::$LOCAL_LANG[$extensionName]);
    }
}

The example code below shows, how to use the resetLocalizationCache method before rendering a Fluid standalone view in a given language.


/**
 * Renders a Fluid StandaloneView respecting the given language
 *
 * @param string $language The language (e.g. de, dk or se)
 * @return string
 */
public function renderStandaloneView($language = '')
{
    // Set the extensionKey
    $extensionKey = GeneralUtility::underscoredToUpperCamelCase('standaloneview');

    if ($language !== '') {
        // Temporary set Language of current BE user to given language
        $GLOBALS['BE_USER']->uc['lang'] = $language;
        LocalizationUtility::resetLocalizationCache($extensionKey);
    }

    /** @var \TYPO3\CMS\Fluid\View\StandaloneView $view */
    $view = $this->objectManager->get(StandaloneView::class);
    $view->setFormat('html');
    $template = GeneralUtility::getFileAbsFileName(
        'EXT:standaloneview/Resources/Private/Templates/StandaloneView.html'
    );
    $view->setTemplatePathAndFilename($template);

    // Set Extension name, so localizations for extension get respected
    $view->getRequest()->setControllerExtensionName($extensionKey);

    return $view->render();
}

So with a small extension of the LocalizationUtility it is now easily possible to render a Fluid standalone view in a language of choice and it is also possible to switch the language within the same request (e.g. sending out localized e-mails to multiple recipients).

I updated the Demo-Extension, which contains a backend module and a command controller for demonstration purposes.

I would like to thank Johannes Rebhan for giving me the hint about the localization cache and also Ulrich Fischer for showing me a different approach, which requires more code, but lead to the same result.


Monday, June 26, 2017

TYPO3 Extbase - Manual validation of a domain model

When you create an Extbase extension and allow a website user to submit form data that will be saved to the TYPO3 database, you usually work with validators in your domain model to ensure, that the given data will match certain criteria (e.g. properties have the right data type or expected data format). Extbase offers an easy way to add validation rules to domain model properties, by just adding the @validate annotation followed by one or multiple validators as shown in the following example:

/**
 * Description
 *
 * @var string
 * @validate NotEmpty, StringLength(minimum=10, maximum=50)
 */
protected $description = '';

Extbase will take care of the domain model validation, when the given form data is converted to an object of the type TYPO3\CMS\Extbase\Mvc\Controller\Argument.

Manual validation


If you manually create a domain model object and want to make sure, that your Extbase validation rules are meet, you can trigger the domain model validation manually as shown below:

/** @var Data $dataModel */
$dataModel = $this->objectManager->get(Data::class);
$dataModel->setDescription('too short');

/* @var $validator \TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator */
$validator = $this->objectManager->get(ValidatorResolver::class)->getBaseValidatorConjunction(Data::class);
$validationResults = $validator->validate($dataModel);

if ($validationResults->hasErrors()) {
    // @todo cycle through errors in $validationResults->getFlattenedErrors()
}

By creating the domain model object manually, you must take into account, that this will create a new object, where all properties are initialized with the default values defined in the domain model.

Practical use case


As an example for a practical use case for manual domain model validation, lets assume you have a REST webservice and need to import some data to TYPO3. You typically fetch the data from the webservice and add the data to the database. Instead of checking the content of the incoming record/field manually using if-statements, you can use the Extbase property mapper in combination with manual domain model validation.

Below follows some example code for the described use case:

/** @var PropertyMapper $propertyMapper */
$propertyMapper = $this->objectManager->get(PropertyMapper::class);

// Get some data - could for example be some data from a REST webservice
$data = $this->getApiData();

foreach ($data as $importRecord) {
    $dataModel = $propertyMapper->convert($importRecord, Data::class);
    // Note: The propertyMapper will set domain model default values for all all non-mappable values

    /* @var $validator \TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator */
    $validator = $this->objectManager->get(ValidatorResolver::class)->getBaseValidatorConjunction(Data::class);
    $validationResults = $validator->validate($dataModel);

    if ($validationResults->hasErrors()) {
        // Record could not be imported, collect error messages for each field in $errorMessages array

        /** @var Error $error */
        foreach ($validationResults->getFlattenedErrors() as $field => $errors) {
            $errorMessages = [];
            foreach ($errors as $error) {
                $errorMessages[] = $error->getMessage();
            }
        }
    } else {
        // Import record to repository...
    }
}

By using the Extbase property mapper to create domain model objects, you do not need to check and assign each field individually. You just have to make sure, that the array given passed to the "convert" function use the same field naming as the domain model do like shown below.


'title' => 'a title',
'email' => 'torben@derhansen.com',
'description' => 'a description',
'year' => 2017,
'amount' => 19.99,
'paid' => true

Note, that the resulting object from the property mapper will contain the default values for each property, that can't be mapped properly.

In the code example above, the resulting domain model object will manually be validated and if no validation errors occur, the object can be added to the repository.

I created a small demo extension with a command controller, which contains 2 example commands that show validation results for some dummy data.

Wednesday, January 25, 2017

TYPO3 - Adding direct mail fields to femanager

In a project I needed to add the direct mail fields "Activate Newsletter" and "Subscribe to categories" to the TYPO3 femanager (thanks to Alex Kellner for this great extension), so frontend users are able to subscribe to a newsletter and select newsletter categories.

Since femanager is created with Extbase, it is easily extendable and I created a small TYPO3 extension named femanager_dmail_subscribe, that automatically adds "Newsletter subscription", "Newsletter category" and "HTML newsletter" direct mail fields to femanager, so editors can select the direct mail fields a frontend user should be able to edit.

femanager plugin field settings

In order to display direct mail categories in the frontend, it is required to add the sysfolder with the direct mail categories to the femanager record storage page.

femanager plugin record storage page
After installing and configuring the extension as shown above, frontend users can edit the selected fields (see screenshot below)

Frontend user can edit direct mail fields

I uploaded 2 versions of the extension to TER. Version 1.0.0 is compatible with TYPO3 6.2 and femanager 1.5.2 and version 2.0.0 is compatible with TYPO3 7.6 and femanager 2.x



Tuesday, October 4, 2016

Testing and deploying TYPO3 using GitLab, GitLab CI, Capistrano and TYPO3 Console

In this article, I'll show you how to setup automated testing and continuous deployment of a TYPO3 website using GitLab, GitLab CI, Capistrano and TYPO3 Console. The main purpose of the article is to give you a starting point for automated testing and continuous deployment.

With the setup described in this article, I want to make sure, that
  • tests are executed automatically on every commit
  • pushes to master branch will automatically be deployed to the production server
  • deployment will only execute, when tests pass

I tried to keep write the article as simple as possible, so please note, that some features / processes of real life automated deployment scenarios are not covered.

Prerequisites:
  • A composer based TYPO3 website with a basic site package
  • At least one TYPO3 extension with tests
  • A working GitLab server (I'm using an own instance of the Community Edition)
  • A GitLab CI runner
  • A SSH account on the webspace, where you want the TYPO3 website to be deployed
  • Ruby version 1.9.3 or newer on your local machine (required for capistrano)

GitLab.com offers free private repositories and also access to GitLab CI runners, so I'm pretty sure, that the setup I described here may even work using the GitLab.com infrastructure.

1. Structure of the composer based TYPO3 website


The TYPO3 website used for this blogpost is a really simple composer based TYPO3 website. It contains a sitepackage with TypoScript and a Fluid template and it contains the TYPO3 extension sf_yubikey, which is used in this example to execute some unit tests. Also the TYPO3 website contains the TYPO3 extension typo3_console, which will be used in the deployment process. 

Unit tests can be executed on CLI with the command shown below:
./vendor/bin/phpunit -c web/typo3conf/ext/sf_yubikey/Tests/Build/UnitTests.xml

My composer.json file contains the following contents:
{
 "repositories": [
  { "type": "composer", "url": "https://composer.typo3.org/" }
 ],
 "name": "typo3/cms-base-distribution",
 "description" : "TYPO3 CMS Base Distribution",
 "license": "GPL-2.0+",
 "require": {
  "typo3/cms": "^7.6",
  "typo3-ter/sf-yubikey": "*",
  "helhum/typo3-console": "^3.3"
 },
 "require-dev": {
   "mikey179/vfsStream": "1.4.*@dev",
   "phpunit/phpunit": "~4.7.0"
 },
 "extra": {
  "typo3/cms": {
   "cms-package-dir": "{$vendor-dir}/typo3/cms",
   "web-dir": "web"
  },
  "helhum/typo3-console": {
   "install-binary": false
  }
 }
}

Note, that I use helhum/typo3-console (not from TER), which on install makes the typo3cms command available in the vendor/bin directory.

The TYPO3 installation also contains a backend user named _cli_lowlevel, which is required to execute CLI commands.

2. Move all sensitive data and local settings out of the LocalConfiguration.php


You should never commit any sensitive data (like passwords or API keys) to a Git repository, so I move those settings out of from LocalConfiguration.php to the file AdditionalConfiguration.php, which also is located in the typo3conf/ folder.

The local AdditionalConfiguration.php file used in this blogpost looks like shown below:

<?php
$GLOBALS['TYPO3_CONF_VARS']['DB']['database'] = 'typo3db';
$GLOBALS['TYPO3_CONF_VARS']['DB']['host']     = 'localhost';
$GLOBALS['TYPO3_CONF_VARS']['DB']['username'] = 'dbusername';
$GLOBALS['TYPO3_CONF_VARS']['DB']['password'] = 'dbpassword';

$GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword'] = 'hashvalue';

$GLOBALS['TYPO3_CONF_VARS']['GFX']['im_path'] = '/opt/local/bin/';
$GLOBALS['TYPO3_CONF_VARS']['GFX']['im_path_lzw'] = '/opt/local/bin/';

Besides all sensitive data from the LocalConfiguration.php, the AdditionalConfiguration.php file should include all settings, that should not be shared across deployment stages.

3. Modify the default .gitignore, so some TYPO3 directories will not be included in the git repository


For the TYPO3 website in this blogpost, I extended the default .gitignore file, so contents of some TYPO3 directories and files do not get committed to the git repository (e.g. fileadmin, uploads or all extensions installed by composer)

#############################
# TYPO3 CMS Base Distribution
#############################
.DS_Store
.idea
index.php
nbproject
node_modules
vendor
typo3
typo3_src
web/index.php
web/typo3

# Ignore fileadmin, uploads and typo3temp
web/fileadmin
web/uploads
web/typo3temp

# Ignore some files in typo3conf
web/typo3conf/ENABLE_INSTALL_TOOL
web/typo3conf/AdditionalConfiguration.php
web/typo3conf/*.log

# Ignore language files (fetched by TYPO3 console)
web/typo3conf/l10n

# Ignore all extensions (loaded by composer)
web/typo3conf/ext/*

# But include the sitepackage, which is not composer based
!web/typo3conf/ext/sitepackage

# Ignore capistrano deployment logs
log

4. Commit the TYPO3 website to a git repository


The .gitignore file is ready, so now I create a new git repository and add / commit all files to it. Finally, I create a new project on the GitLab server and add the repository as a remote for the local git repository and finally push the repository to the remote.

git remote add origin git@gitlab.com:derhansen/typo3_ci_cd.git
git push -u origin master


5. Configure deployment of TYPO3 website with capistrano


5.1 SSH setup for deployment to webserver

First, I add my public SSH RSA key to the .ssh/authorized_keys file in the home-directory of the SSH user on the webserver, so I'm able to login by using SSH key authentication. This step is not required, but makes deployment from my local machine to the webserver easier.

Next I create a new SSH RSA key on the webserver (like shown here), so the SSH user will be able to pull from the remote git repository on the GitLab server I created in step 4. This SSH RSA Key will be the deployment key and must not contain a password.

The new SSH public key must now be added to the git repository on the GitLab server. In order to do so, I add it as Deployment Key for the project (see screenshot below)


5.2 Initialize and configure capistrano

I have chosen capistrano as the deployment tool for the TYPO3 website, because it is well documented, has a large user base and is easy to use/extend. If you do not like capistrano, it should be easy to replace capistrano with the deployment tool of your choice (e.g. TYPO3 surf, Deployer, Magallanes, ...)

In the project root of my local composer based TYPO3 website, I now initialize a new capistrano config with the following command:

cap install

First of all, I remove everything in the file config/deploy/production.rb and add the following content:

server 'typo3-ci-cd.domain.tld',
  user: 'typo3cicd',
  roles: %w{app db web}

Next, I create a really basic deployment in config/deploy.rb, which mainly executes composer install and executes some TYPO3 tasks using TYPO3 console.

# config valid only for current version of Capistrano
lock '3.6.1'

set :application, 'typo3_ci_cd'
set :repo_url, 'git@path-to-my-gitlab:torben/typo3_ci_cd.git'
set :deploy_to, '/var/www/typo3-ci-cd/httpdocs'
set :scm, :git
set :keep_releases, 5
set :linked_dirs, %w{web/fileadmin web/uploads}
set :linked_files, %w{web/typo3conf/AdditionalConfiguration.php}

namespace :composer do
    desc "Runs composer."
    task :install do
        on roles(:web) do
            within release_path do
                execute "composer", "install", "--no-dev", "--no-interaction", "--prefer-dist"
            end
        end
    end
end

namespace :typo3 do

    desc "Clear TYPO3 Cache"
    task :cache_flush do
        on roles(:all) do
            within release_path do
                execute "#{release_path}/vendor/bin/typo3cms cache:flush"
            end
        end
    end

    desc "Update language"
    task :language_update do
        on roles(:all) do
            within release_path do
                execute "#{release_path}/vendor/bin/typo3cms language:update"
            end
        end
    end

end

namespace :deploy do
    after :publishing, "composer:install"
    after :finishing,  "typo3:cache_flush"
    after :finishing,  "typo3:language_update"
end

You may note the file AdditionalConfiguration.php in the linked_files section of the deployment configuration. This is required in order to get TYPO3 running, when I'm doing a deployment from my local machine.

The linked_dirs section contains the folders web/fileadmin and web/uploads, which I want to share across deployments, since users will upload files to it.

5.3 Manual steps before first deployment

Before the first deployment can be executed, there are some steps, that need to be done manually on the webserver.

5.3.1 Initial shared folder setup

I copy the contents of the directories fileadmin/ and uploads/ from my local TYPO3 website to the directory shared/web in the deployment path (see "deploy_to" in the deploy.rb file). The final directory structure is as shown below (Note: .htaccess files are not shown).

shared/
└── web
    ├── fileadmin
    │   ├── _temp_
    │   │   └── index.html
    │   └── user_upload
    │       ├── index.html
    │       └── _temp_
    │           ├── importexport
    │           │   └── index.html
    │           └── index.html
    ├── typo3conf
    │   └── AdditionalConfiguration.php
    └── uploads
        ├── index.html
        ├── media
        │   └── index.html
        ├── pics
        │   └── index.html
        └── tx_felogin

The shared folder does also contains the typo3conf/ directory, where I add the AdditionalConfiguration.php and modify the settings (e.g. database credentials, GraphicsMagick path, ...) to match the server configuration.

5.3.2 Initial database setup

Since there is no initial TYPO3 database on the webserver, I create a local dump of the TYPO3 database and restore it to the remote database on the webserver.

5.4 Initial deployment

Now it is time to do the first deployment form my local machine, so I execute the deployment with the following command

cap production deploy

Capistrano now executes the deployment and after finishing, I have a fully working copy of my local TYPO3 website on the remote webserver.

Capistrano creates the directories current/, releases/, repo/ and shared/. The current version of the deployed TYPO3 website is available in the current/ directory, so I adjusted the webserver to load the website from that directory.

5.5 Commit deployment configuration

Finally I commit the deployment configuration to the git repository, so it later can be used for the automated deployment.

6. Configure automated test execution on GitLab server


In an automated deployment process, you need to make sure, that only working versions of your website will be deployed to the production server. In this example, I simply run the unit tests of a TYPO3 extension to demonstrate, how automatic test execution can be a part of a deployment process. In real life scenarios, you can for example use unit, functional or acceptance tests to make sure, that your website will run smoothly after deployment.

In my local project, I create the file .gitlab-ci.yml and add the following contents to it:

stages:
  - tests

phpunit:php7.0:
  stage: tests
  image: php:7.0
  before_script:
    - apt-get update -yqq
    - apt-get install git libcurl4-gnutls-dev libicu-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libpq-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev -yqq
    - docker-php-ext-install mbstring zip
    - curl -sS https://getcomposer.org/installer | php
  script:
    - php composer.phar install
    - php vendor/bin/phpunit --colors -c web/typo3conf/ext/sf_yubikey/Tests/Build/UnitTests.xml

I add and commit the file to the git repository and push the changes to the remote. Now GitLab CI will start a PHP 7.0 docker container, install all dependencies and finally run the configured unit tests.

Nice, the build passed....



...and unit tests have been executed on the GitLab CI runner.


You may notice, that the build took 3:16 minutes to finish, which is pretty long just for unit test execution. To reduce the build time, I suggest to create your own docker image with all dependencies included.

If you want to receive e-mail notifications on builds (or only for failed builds), you can enable this feature in the service section of the GitLab project.

7. Configure automated deployment on GitLab server


As a final step, I want to automate the deployment of the website, when commits are made to the master branch and if tests passed. Since the GitLab CI runner will do the deployment, it must be able to SSH to the production website.

I create a new SSH RSA key (with no password), which will be used for the deployment from the GitLab CI runner to the production webserver. After adding the SSH public key to the ssh/authorized_keys file of the SSH user on the webserver, I create a new variable in the GitLab project and add the new private key to it.


Next, I extend the .gitlab-ci.yml to use the SSH_PRIVATE_KEY variable in the docker container, that will do the deployment. Detailed instructions can be found in the GitLab CI documentation. The final configuration file looks like shown below:

stages:
  - test
  - deploy

phpunit:php7.0:
  stage: test
  image: php:7.0
  before_script:
    - apt-get update -yqq
    - apt-get install git libcurl4-gnutls-dev libicu-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libpq-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev -yqq
    - docker-php-ext-install mbstring zip
    - curl -sS https://getcomposer.org/installer | php
  script:
    - php composer.phar install
    - php vendor/bin/phpunit --colors -c web/typo3conf/ext/sf_yubikey/Tests/Build/UnitTests.xml

deployment:
  stage: deploy
  image: ruby:2.3
  before_script:
    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
    - eval $(ssh-agent -s)
    - ssh-add <(echo "$SSH_PRIVATE_KEY")
    - mkdir -p ~/.ssh
    - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
  script:
    - gem install capistrano
    - cap production deploy
  only:
    - master

Since capistrano is a ruby application, I use a docker container with ruby for the deployment.

Finally, after committing the changes to the repository, the GitLab CI runner runs the build and deploys the TYPO3 website to the production server.



And that's it, the automated testing and continuous deployment setup for TYPO3 is up and running.

Outlook and conclusion


There are for sure some things that can be improved in the process I described in this post. For example, you could also add the AdditionalConfiguration.php file as a secret variable to the GitLab project and use the GitLab CI Mutli-Runner to deploy it to the production server. Also you can automate TYPO3 related tasks using TYPO3 console (e.g. database updates on new extension installation with typo3cms database:updateschema, update reference index, ...).

I hope the post will give you a good point of how/where to start with automating tests and continuous deployment of a TYPO3 website. The shown technique is just one way of how to setup and automate the process - feel free to build your own configuration with the tools of your choice and please share your knowledge :-)

Sunday, January 17, 2016

TYPO3 ExtBase - Hidden and deleted property of domain models

TYPO3 ExtBase repositories do by default only return non-hidden and non-deleted records. You can easily override this behavior by modifying the default query settings for the given repository. If you for example want to return all FrontendUser (including hidden and deleted) from a TYPO3 website, you can set the query settings as shown below.

Now all available FrontendUser records are returned. When you now loop over the queryResult, you will see, that the FrontendUser Objects returned do not contain any information about if the record actually is hidden or deleted.


Note that the example above shows the assignment of the defaultQuerySettings directly in an action. Typically you define this in the initializeObject method or directly in the repository.

In order to obtain those information, you need to extend the existing FrontendUser domain model with two fields. First, you need to create an own domain model with two properties as shown below (must be located in a extension - usually your own).
Note that I called the property which indicates if a record is hidden or not "disable" and not "hidden". I could not get the example to work with a property called "hidden".

Next you need to add the new fields to the TCA by adding the file fe_users.php with the following content to the folder Configuration/TCA/Overrides.

Finally you must add you new domain model as a subclass for the original FrontendUser domain modal and map the two new properties. Add the file ext_typoscript_setup.txt to the root folder of your extension with the following content.

Note, that the key in line 6 (in this example "0") must be used in line 12 as recordType.

After clearing all caches, ExtBase now returns all FrontendUsers containing the two new properties "deleted" and "disable"

Sunday, November 15, 2015

TYPO3 - Using Fluid StandaloneView to render localized templates in a scheduler task (part 2)

Today I found, that the technique I described in my blogpost about rendering localized Fluid templates in a scheduler task does not work as expected. As long as you want to switch the language used to render the templates only one time, then you're fine. But as soon as you want to switch the language several times (e.g. sending multiple localized e-mails in one request), then you experience that only the first language switch is respected.

The root cause for this is the TYPO3 LocalizationUtility, which includes the static translate() method that is used return translated language labels from XLF/XML language files. The LocalizationUtility is not designed to handle multiple language switches in one request, so at this point I'm stuck.

I order to keep things simple for the integrator (use one e-mail template with language labels to send out localized e-mails in a scheduler task), I decided to create an own viewHelper which uses a modified version of the LocalizationUtility. The modified version of the LocalizationUtility does not contain any static variables or methods and can be used with dependency injection. You can find the code in this GitHub repository.

Usage

In my Fluid StandaloneView templates I now use my own translate viewHelper as shown below.

The viewHelper uses the LocalizationService (which is the TYPO3 LocalizationUtility with some small modifications - e.g. removed all "static" declarations). As a result of this, all functionality of the original viewHelper / TranslationUtility are remained (e.g. overwriting language labels with TypoScript)

I extended the original demo extension from my first blogpost so it makes use of the new viewHelper / LocalizationService. The Extension now includes a form, which renders multiple Fluid StandaloneViews in one request and the language is switched for each individual StandaloneView (see result-section in the screenshot below)

The demo extension also includes a command controller, which includes a command that also renders multiple standaloneViews in one request (see screenshot below)




If I don't find any major problems, I will make use of this technique to send out multilingual e-mails in a scheduler task in my Event Management Extension.

Final notice

The technique shown should only be used in the backend context of TYPO3 when you want to render multilingual Fluid StandaloneViews in one request. I'm not very happy with the approach of "just" taking some code from the TYPO3 core and adapting it to my needs, since this is not always a clean solution and it may include some drawbacks.