
    
    
        
        
                
    
        
    
                
    
        
    
                
    
        
    
                
    
        
        
                
    
        
        
            
    
        
        
            
        
        
        
            
        
        
        
            
        
        
        
        
{"version":"https:\/\/jsonfeed.org\/version\/1","title":"Scripting-Base - Blog about TYPO3 and stuff","home_page_url":"https:\/\/www.scripting-base.de\/blog","feed_url":"https:\/\/www.scripting-base.de\/blog.json","description":"Scripting-Base - Blog about TYPO3 and stuff","author":{"name":"Andreas Fernandez"},"items":[{"title":"Configure Guzzle via Dependency Injection","date_published":"2022-04-03T07:00:00+02:00","id":"https:\/\/www.scripting-base.de\/blog\/configure-guzzle-via-dependency-injection","url":"https:\/\/www.scripting-base.de\/blog\/configure-guzzle-via-dependency-injection","content_html":"<p>With Guzzle v7, its class <code>GuzzleHttp\\Client<\/code> became annotated as <code>@final<\/code> as it will be a real <code>final<\/code> class in Guzzle\nv8. Extending Guzzle clients to enrich them with custom functionality or to pass configuration (e.g. API credentials) is\nnow discouraged and static code analysis tools like PHPStan may report this as an error. Depending on how\n<code>GuzzleHttp\\Client<\/code> is extended, migration may be cumbersome. I got your back, and I'll cover some common cases in this\nblog post.<\/p>\n\n<h2>Simple client with custom configuration<\/h2>\n<p>In a project, we heavily extended <code>GuzzleHttp\\Client<\/code> for all our cases as we wanted to make use of dependency injection\nvia the client's class names. Please see the example below how our clients were implemented.<\/p>\n<p>We defined our class <code>App\\Client\\GithubClient<\/code> that extends <code>GuzzleHttp\\Client<\/code> without any further logic:<\/p>\n<pre><code class=\"language-php\">\/\/ src\/Client\/GithubClient.php\nnamespace App\\Client;\n\nclass GithubClient extends \\GuzzleHttp\\Client\n{\n}<\/code><\/pre>\n<p>The client <code>App\\Client\\GithubClient<\/code> is injected into the service <code>App\\Service\\GithubService<\/code>:<\/p>\n<pre><code class=\"language-php\">\/\/ src\/Service\/GithubService.php\nnamespace App\\Service;\n\nclass GithubService\n{\n    public function __construct(\n        private readonly GithubClient $client\n    ) {\n    }\n}<\/code><\/pre>\n<p>The client <code>App\\Client\\GithubClient<\/code> is configured with the API key in the project's <code>services.yaml<\/code> file:<\/p>\n<pre><code class=\"language-yaml\"># src\/config\/services.yaml\nservices:\n  App\\Client\\GithubClient\n    class: App\\Client\\GithubClient\n    arguments:\n      $config:\n        headers:\n          Authorization: 'token %app.github.api_key%'<\/code><\/pre>\n<h3>Migration time<\/h3>\n<p>This is the most simple way a Guzzle client may be configured. A client just takes some configuration (like the\n<code>Authorization<\/code> header from the example) above, and it's ready to use. The whole class can be replaced by a simple\n<code>GuzzleHttp\\Client<\/code> instance, which is configured in our <code>services.yaml<\/code> file:<\/p>\n<pre><code class=\"language-yaml\">services:\n  guzzle.client.github\n    class: GuzzleHttp\\Client\n    arguments:\n      $config:\n        headers:\n          Authorization: 'token %app.github.api_key%'<\/code><\/pre>\n<div class=\"toast toast-error\">\n<p>This requires another change! Since we are using <code>GuzzleHttp\\Client<\/code> now, autowiring the correct Guzzle client via its class name does not work anymore.<\/p>\n<\/div>\n<p>Now, we have to configure our service to use the <code>guzzle.client.github<\/code> service explicitly:<\/p>\n<pre><code class=\"language-yaml\">services:\n  App\\Service\\GithubService:\n    class: App\\Service\\GithubService\n    arguments:\n      $client: '@guzzle.client.github'<\/code><\/pre>\n<p>Finally, we have to adjust the <code>constructor<\/code> of our service to expect an instance <code>\\GuzzleHttp\\Client<\/code> as argument:<\/p>\n<pre><code class=\"language-php\">\/\/ src\/Service\/GithubService.php\nnamespace App\\Service;\n\nclass GithubService\n{\n    public function __construct(\n        private readonly \\GuzzleHttp\\Client $client\n    ) {\n    }\n}<\/code><\/pre>\n<p>This migration is pretty easy, the key changes are:<\/p>\n<ul>\n<li>the service name is changed to <code>guzzle.client.github<\/code><\/li>\n<li>the class in the service definition is changed to <code>GuzzleHttp\\Client<\/code>, as our custom client <code>App\\Client\\GithubClient<\/code> became obsolete<\/li>\n<li><code>App\\Service\\GithubService<\/code> is configured to use <code>guzzle.client.github<\/code> explicitly<\/li>\n<\/ul>\n<h2>Create complex client via factory<\/h2>\n<p>In some cases, our clients are more complex and need custom logic besides the configuration. A common use-case is\npassing an API key via a query parameter in the request URL, for example as required by Google Maps.<\/p>\n<p>See an example of how our <code>GoogleMapsClient<\/code> was implemented before:<\/p>\n<pre><code class=\"language-php\">\/\/ src\/Client\/GoogleMapsClient.php\nnamespace App\\Client;\n\nclass GoogleMapsClient extends \\GuzzleHttp\\Client\n{\n    public function __construct(array $config, string $apiKey)\n    {\n        $handlerStack = \\GuzzleHttp\\HandlerStack::create($config['handler'] ?? null);\n        $config = array_merge($config, [\n            'base_uri' =&gt; rtrim($config['base_uri'] ?? '', '\/') . '\/',\n            'handler' =&gt; $handlerStack,\n        ]);\n        $handlerStack-&gt;unshift(\\GuzzleHttp\\Middleware::mapRequest(static function (\\Psr\\Http\\Message\\RequestInterface $request) use ($apiKey) {\n            return $request-&gt;withUri(\\GuzzleHttp\\Psr7\\Uri::withQueryValue($request-&gt;getUri(), 'key', $apiKey));\n        }));\n\n        parent::__construct($config);\n    }\n}<\/code><\/pre>\n<p>In this example, we create a <code>GuzzleHttp\\HandlerStack<\/code> and add a middleware to it. The middleware is responsible for adding the <code>key<\/code> query...<\/p>","date_modified":"2022-04-03T13:04:34+02:00","tags":["symfony","guzzle","dependency injection","tutorial"],"image":"\/user\/pages\/02.blog\/11.configure-guzzle-via-dependency-injection\/taylor-vick-M5tzZtFCOfs-unsplash.jpg"},{"title":"Improve test speed in Symfony with in-RAM database","date_published":"2021-05-27T10:00:00+02:00","id":"https:\/\/www.scripting-base.de\/blog\/improve-test-speed-in-symfony-with-in-ram-database","url":"https:\/\/www.scripting-base.de\/blog\/improve-test-speed-in-symfony-with-in-ram-database","content_html":"<p>When implementing the fundamentals of <a href=\"https:\/\/my.typo3.org\">my.typo3.org<\/a>, an API based on Symfony was built to feed data to applications in the TYPO3 universe, e.g. the aforementioned my.typo3.org or the  <a href=\"https:\/\/exam.typo3.com\/\">Certification Platform<\/a>. This API must be rock-solid, thus it has a decent test coverage for each single piece gluing the application together. With development going further, the amount of tests increased, including API endpoints that get their data from a database. In out test scenarios, we use a sqlite database as this needs no additional setup.<\/p>\n\n<p>At some point in the development process, executing tests became slower and slower as the amount of tests and the respective amount of fixtures increased:<\/p>\n<pre><code class=\"language-plaintext\">Time: 10:06.275, Memory: 916.00 MB\n\nOK (771 tests, 3120 assertions)<\/code><\/pre>\n<p>The full test run needs <strong>~10 minutes<\/strong> and consumes <strong>over 900 MB of RAM<\/strong>. Of course the whole test suite doesn't have to re-run when changing a single controller, but some changes are low-level and trial &amp; error by letting the CI do the job is not really feasible.<\/p>\n<p>Disclaimer: I'm developing on a 2019 Dell XPS 15 7590 with hexacore CPU Intel i7-9750H, 32 GB RAM, an M.2 NMVe SSD, Ubuntu 20.10 and every project runs with <a href=\"https:\/\/ddev.readthedocs.io\/en\/stable\/\">ddev<\/a>.<\/p>\n<h2>Use the RAM, Luke<\/h2>\n<p>Scraping data from and pushing data back to the SSD shouldn't be that time-consuming, but I'm not that deep into Symfony and sqlite internals to properly explain what's going on here. Luckily, Symfony allows to store the database in RAM very easily by setting the database URL to <code>sqlite:\/\/\/:memory:<\/code> instead. However, the first run didn't go well:<\/p>\n<pre><code class=\"language-plaintext\">ERRORS!\nTests: 771, Assertions: 1575, Errors: 39, Failures: 299.<\/code><\/pre>\n<p>All failures are caused by the exception <code>Doctrine\\DBAL\\Exception\\TableNotFoundException<\/code>, right after priming the database the tables are not available anymore after importing the fixtures. After some research I found out that Symfony keeps the database in RAM until its kernel gets shutdown, either on purpose or when a new kernel is created. This happened at three specific places:<\/p>\n<ul>\n<li>before priming the database<\/li>\n<li>after importing the fixtures<\/li>\n<li>booting a web client to call the API endpoints in the tests<\/li>\n<\/ul>\n<p>A typical <code>setUp()<\/code> looked like this:<\/p>\n<pre><code class=\"language-php\">protected function setUp(): void\n{\n    parent::setUp();\n    $this-&gt;prime(); \/\/ calls static::bootKernel() as well\n    $this-&gt;importFixture('path\/to\/fixture.php');\n    static::bootKernel();\n}<\/code><\/pre>\n<h3>Importing fixtures<\/h3>\n<p>The issue has been identified, let's start fixing it. The kernel is now booted at first in the test's <code>setUp()<\/code> method and the primer demands an already booted kernel. If the primer cannot find a kernel, a <code>\\LogicException<\/code> is thrown which reveals non-adopted test classes. The primer is a <code>trait<\/code> being imported in the test classes extending <code>\\Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase<\/code>, checking for a booted kernel is straight forward:<\/p>\n<pre><code class=\"language-php\">trait DatabasePrimer\n{\n    public function prime(): void\n    {\n        if (!self::$booted) {\n            throw new \\LogicException('Could not find a booted kernel');\n        }\n\n        \/\/ ...\n    }\n}<\/code><\/pre>\n<p>The fixtures are imported by Doctrine's <code>EntityManager<\/code>, calling its <code>persist()<\/code> and <code>flush()<\/code> methods. This revealed another issue: the records imported to the database could not be found. The reason is that Doctrine maintains an identity...<\/p>","date_modified":"2022-04-02T17:27:36+02:00","image":"\/user\/pages\/02.blog\/10.improve-test-speed-in-symfony-with-in-ram-database\/fabrizio-conti-k6GpdsPJSZw-unsplash.jpg"},{"title":"Icons are dead, long live broken icons!","date_published":"2020-11-25T21:13:00+01:00","id":"https:\/\/www.scripting-base.de\/blog\/icons-are-dead-long-live-broken-icons","url":"https:\/\/www.scripting-base.de\/blog\/icons-are-dead-long-live-broken-icons","content_html":"<p>In the recent release of TYPO3, namely 10.4.10 at the time of writing this blog post, a patch meant to improve the backend performance was merged: the introduction of SVG icon sprites. Unfortunately, this patch had unexpected consequences and lead to some new experiences.<\/p>\n\n<p><strong>tl;dr:<\/strong>\nThis blog post is a mix of \"how good things went wrong\" in terms of TYPO3 icons and personal drama based on recent observations.\nYou're still with me? Great, please read further.<\/p>\n<p>As always when patches for open source projects are written, everybody does it with best intentions to scratch own itches and those of other people, the same happens for TYPO3. Some changes have a little impact, others include major refactorings. <a href=\"https:\/\/github.com\/TYPO3\/TYPO3.CMS\/commit\/2d6ed648ab87185e34eaa0c61c958840396584a1\">This patch<\/a> introduces SVG icon sprites for an improved overall backend performance:<\/p>\n<ul>\n<li>Instead of hundreds of icons, only a few sprite files needs to be loaded<\/li>\n<li>Icons are rendered in shadow DOM which lowers the initial document size<\/li>\n<\/ul>\n<p>As written above each change has an impact, but sometimes the expected impact turns out to be wrong. The same thing happened with this very change. With an upgrade of the <a href=\"https:\/\/typo3.github.io\/TYPO3.Icons\/\">@typo3\/icons<\/a> package to version 2.0, some important restructurings were done which applies to the TYPO3 CMS as well, of course.<\/p>\n<h2>We have APIs<\/h2>\n<p>Previously, icons were stored in <code>sysext\/core\/Resources\/Public\/T3Icons<\/code>, which is considered being private API. Yes, <strong>private<\/strong>, albeit the icons are stored in the <code>Public\/<\/code> directory, simply because those icons are publicly callable assets.<\/p>\n<p>This is not a bad thing, as with TYPO3 7.5 an <a href=\"https:\/\/docs.typo3.org\/m\/typo3\/reference-coreapi\/master\/en-us\/ApiOverview\/Icon\/Index.html\">Icon API<\/a> was introduced, which is in place for <strong>five years<\/strong> already at the time of writing this blog post.<\/p>\n<h2>Impact<\/h2>\n<p>An attentive reader noticed the word \"previously\" in the upper paragraph. With switching to version 2 of the icon repository, the location of these icons changed, which isn't a big deal as there is a stable API in place, right? Wrong. It turned out <a href=\"https:\/\/github.com\/FriendsOfTYPO3\/frontend_editing\/commit\/d7f758198f3c933a1d4b5815cfe195ddf9990b2b\">extension<\/a> <a href=\"https:\/\/forge.typo3.org\/issues\/92689#note-13\">authors<\/a> out there rely on the absolute icon paths, either because of lack of knowledge, \"it's there\" or mistrust in the APIs.<\/p>\n<p>Altough some people allege it, this change was <strong>not meant<\/strong> to break existing extensions, otherwise it would have been marked as \"breaking\" and it would not have been backported to v10. On the other hand, relying on a specific, non-documented behavior makes every bugfix a breaking change.<\/p>\n<div class=\"md-spoiler\" data-label=\"Personal drama ahead, hover or tap to show\">\n<p class=\"md-spoiler__line\"><\/p>\n<p class=\"md-spoiler__line\">Some honest words here: under regular circumstances I'd say \"well yeah, you're out of luck\". Unfortunately, I noticed that the general tone became pretty harsh lately especially on social media. People sometimes demand things in free software (please read the GPL, especially the \"NO WARRANTY\" part, thanks!), or, in even worse cases, attacking the whole team who works hard on making TYPO3 better step-by-step.<\/p>\n<\/div>\n<p>Whatever the reason is, such cases make decisions even harder as everything must be considered being \"breaking\" in one way or another. Be assured that nobody \"hides\" a breaking change somewhere as a drive-by change willingly.<\/p>\n<p><small>Header photo by <a href=\"https:\/\/unsplash.com\/photos\/_zKxPsGOGKg\">Harpal Singh<\/a> on <a href=\"https:\/\/unsplash.com\">Unsplash<\/a>.<\/small><\/p>","date_modified":"2022-04-02T17:27:31+02:00","image":"\/user\/pages\/02.blog\/09.icons-are-dead-long-live-broken-icons\/harpal-singh-_zKxPsGOGKg-unsplash.jpg"},{"title":"Goodbye $.ajax - The clock is ticking","date_published":"2019-12-15T16:09:00+01:00","id":"https:\/\/www.scripting-base.de\/blog\/goodbye-jquery-ajax","url":"https:\/\/www.scripting-base.de\/blog\/goodbye-jquery-ajax","content_html":"<p>Back then, when each browser had its own set and understanding of \"supporting\" JavaScript features, one knight in shiny armor saved us maiden developers and allowed us to focus on our tasks: jQuery. There was no necessity to remember every browser quirk or buggy implementation, jQuery was there and covered us.<\/p>\n\n<p>But sometimes we have to let things go. The TYPO3 Core minimizes the usage of jQuery in an ongoing process. For example the <a href=\"https:\/\/developer.mozilla.org\/de\/docs\/Web\/API\/Document\/querySelector\">querySelector<\/a> allows to select a specific element by CSS selectors, similar to Sizzle, the selector engine developed and used by jQuery.<\/p>\n<p>But other parts of jQuery get replaced as well with modern, native APIs. During December's Review Friday the new <a href=\"https:\/\/docs.typo3.org\/c\/typo3\/cms-core\/master\/en-us\/Changelog\/master\/Feature-89738-ApiForAjaxRequests.html\">AJAX API<\/a> got merged, which implements the <a href=\"https:\/\/developer.mozilla.org\/de\/docs\/Web\/API\/Fetch_API\">Fetch API<\/a> under the hood. Fetch uses Promises under the hood which makes is very easy to chain success or error handlers to a request.<\/p>\n<p>The module is located at <code>TYPO3\/CMS\/Core\/Ajax\/AjaxRequest<\/code> and may be used with RequireJS. A very basic example looks like this:<\/p>\n<pre><code class=\"language-javascript\">require(['TYPO3\/CMS\/Core\/Ajax\/AjaxRequest'], function (AjaxRequest) {\n  new AjaxRequest('https:\/\/httpbin.org\/json').get().then(\n    async function (response) {\n      const data = await response.resolve();\n      console.log(data);\n    }\n  );\n});<\/code><\/pre>\n<p>But what happens here? We create an <code>AjaxRequest<\/code> object that only takes the target URL as argument. After that we're free in \"decorating\" the request. In this case, we send the request as <code>GET<\/code> and process the response. The response is of type <code>AjaxResponse<\/code> which exposes the methods <code>resolve()<\/code> and <code>raw()<\/code>. In this example we use <code>resolve()<\/code>, which checks whether the response contains a <code>Content-Type<\/code> header that matches to any JSON content. If this assumption is true, a JSON object is returned, otherwise we get a plaintext response that could contain any string.<\/p>\n<p>What's going on with these <code>async<\/code> and <code>await<\/code> keywords? Simplified speaking, this is a shortcut to handle Promises in a more comfortable way.<\/p>\n<h2>I know what I'm doing<\/h2>\n<p>For more advanced usage, <code>raw()<\/code> may be used instead which returns the original <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Response\">response object<\/a>:<\/p>\n<pre><code class=\"language-javascript\">require(['TYPO3\/CMS\/Core\/Ajax\/AjaxRequest'], function (AjaxRequest) {\n  new AjaxRequest('https:\/\/httpbin.org\/json').get().then(\n    async function (response) {\n      const response = response.raw();\n      if (raw.headers.get('Content-Type') !== 'application\/json') {\n        console.warn('We didn\\'t receive JSON, check your request.');\n        return;\n      }\n      console.log(await raw.json());\n    }\n  );\n});<\/code><\/pre>\n<h2>Error error on the wall<\/h2>\n<p>That's all nice, but how are errors handled? Errors may happen anytime, either if the client's network is down or if the requested endpoint is not available anymore. The original implementation of Fetch is a bit strange here: errors sent by the remote (e.g. HTTP status 500) are not handled as failure, but client errors are. Since this is not feasible and doesn't improve the developers experience, <code>AjaxRequest<\/code> always throws a <code>ResponseError<\/code>, which contains the original response:<\/p>\n<pre><code class=\"language-javascript\">require(['TYPO3\/CMS\/Core\/Ajax\/AjaxRequest'], function (AjaxRequest) {\n  new AjaxRequest('https:\/\/httpbin.org\/status\/500').get().then(\n    function (response) {\n      \/\/ Empty on purpose\n    }, function (error) {\n      console.error('Request failed because of error: ' + error.response.status + ' ' + error.response.statusText);\n    }\n  );\n});<\/code><\/pre>\n<h2>Attach query string on demand<\/h2>\n<p>But the API does more: if you need to add query arguments to the URL, call <code>withQueryArguments()<\/code> of the request object. The method...<\/p>","date_modified":"2022-04-02T17:27:26+02:00","image":"\/user\/pages\/02.blog\/08.goodbye-jquery-ajax\/alexandru-vicol-D9FQYwAclwQ-unsplash.jpg"},{"title":"The history of JavaScript in TYPO3","date_published":"2018-07-29T12:42:00+02:00","id":"https:\/\/www.scripting-base.de\/blog\/history-javascript-typo3","url":"https:\/\/www.scripting-base.de\/blog\/history-javascript-typo3","content_html":"<p>In TYPO3's history there have been a lot of ups and downs, in any regard. This blog post tells you something about JavaScript in TYPO3: how was it back then, what's happening now and what will maybe happen.<\/p>\n\n<h2>The dark past<\/h2>\n<p>In the past, the JavaScript implementations were some bunch of loose scripts that did some stuff. There was no clear separation of concerns and the scripts were loaded in a defined order and with some luck, everything worked well. The scripts had (sometimes hidden) dependencies to other scripts, so extending them or fixing bugs was always some kind of a challenge.<\/p>\n<p>In some cases, the JavaScript calls were not triggered by proper event handling, but by direct calls injected by PHP, what makes the whole situtation worse and more error-prone. Unfortunately, this is still the case, currently.<\/p>\n<p>Back then there were some JavaScript frameworks that were delivered with TYPO3: prototype, script.aculo.us and jQuery. The modules based on any of these frameworks were partialy a mess, as a module that was clearly written for one specific framework also used API of the other frameworks.<\/p>\n<p>With ExtJS, the TYPO3 Core received some kind of modularization, but that came with a high price: It's very complicated to extend the functionality of ExtJS for beginners, plus at some point the support of the used ExtJS version ended. Since parts of the TYPO3 backend highly depended on ExtJS components (Page tree, workspaces, extension manager, recycler and some more), this was the worst case, since the used ExtJS version had some critical bugs in regard of Internet Explorer and an upgrade was not possible without rewriting everything.<\/p>\n<h2>Renaissance<\/h2>\n<p>With the start of TYPO3 7 the core team decided to streamline the JavaScript modules and to get rid of any framework that blocks further development. Since prototype and script.aculo.us seemed to be abandoned (and actually they are, prototype got its last release in September 2015, script.aculo.us in December 2010!), all JavaScript was rewritten to either use Vanilla JS or jQuery.\nThat was also the time where <a href=\"https:\/\/requirejs.org\/\">RequireJS<\/a> was introduced (also see <a href=\"https:\/\/scripting-base.de\/blog\/requirejs-backend-1.html\">this blog post<\/a>). The goal of RequireJS is to have a proper dependency handling between JavaScript files to make sure every required module is loaded before the actual module. It's also possible to load another module \"on-the-fly\" when it's <em>really<\/em> required.<\/p>\n<p>In the same run, most of the ExtJS components were rewritten to RequireJS and jQuery, as we decided to get rid of ExtJS as well, for obvious reasons. But getting rid of ExtJS should take some more time.<\/p>\n<p>In TYPO3 v8, most of ExtJS got removed. The so-called \"viewport\" (the \"frameset\" of the backend with the top bar, module menu and content box) was <a href=\"https:\/\/docs.typo3.org\/typo3cms\/extensions\/core\/Changelog\/8.4\/Breaking-52877-RemoveExtJSViewport.html\">rewritten to plain HTML and jQuery<\/a>, which drastically reduced the processing time and the memory footprint of the backend. Only the page tree was left.<\/p>\n<p>With TYPO3 8.4, <a href=\"https:\/\/www.typescriptlang.org\/\">TypeScript<\/a>, a superset scripting language of JavaScript was introduced <a href=\"https:\/\/docs.typo3.org\/typo3cms\/extensions\/core\/Changelog\/8.4\/Feature-77900-IntroduceTypeScriptForTheCore.html\">into the Core<\/a>. But why?\nThe main feature of...<\/p>","date_modified":"2022-04-02T17:27:26+02:00","tags":["javascript","typescript","modules"],"image":"\/user\/pages\/02.blog\/07.history-javascript-typo3\/pankaj-patel-1IW4HQuauSU-unsplash.jpg"},{"title":"Welcome to the jungle, or: How I survived EXT:t3editor","date_published":"2017-08-27T20:32:00+02:00","id":"https:\/\/www.scripting-base.de\/blog\/welcome-to-the-t3editor","url":"https:\/\/www.scripting-base.de\/blog\/welcome-to-the-t3editor","content_html":"<p>Once upon a time, somewhere back in mid 2015 I dived into a big adventure that came to an end (for the time being) - the core extension <code>t3editor<\/code>.<\/p>\n\n<p>On my journey migrating a lot of stuff to jQuery and RequireJS during TYPO3 CMS 7 development, Benni Mack asked me to migrate the extension <code>t3editor<\/code> to jQuery as well. It sounded like an easy job - I've never been so wrong.\nThe extension was literally a hybrid of any existing JavaScript framework that was available back then when the extension was created: CodeMirror, prototype, script.aculo.us and ExtJS.<\/p>\n<p>The most challenging part was migrating our custom plugins for code completion to RequireJS, because of clean dependency handling. It also took some effort to get the layout of the code completion functional again.\nAfter two months (and a tiny patch in codemirror itself), the migration was done and <a href=\"https:\/\/review.typo3.org\/#\/c\/39635\/\">the patch<\/a> was merged. But, of course, there were still some glitches that were fixed in follow-up patches. Finally, the extension only uses CodeMirror, jQuery and RequireJS.<\/p>\n<p>But behold - if that's the end of the story I wouldn't put it into a blog post.<\/p>\n<p>The journey continues. We noticed that CodeMirror itself is really old with version 2.x, while version 5.x was out there in the wild, so the adventure continues. Several efforts were taken, all of them failed at a certain point, the latest one was after DevDays '16 in Nuremberg. After that, it went silent around t3editor. After taking weeks of thinking about how to refactor the whole codebase, I've started with one large pitfall that broke my neck before: Port everything that uses CodeMirror to FormEngine before doing any major overhaul.<\/p>\n<p>Two parts of the backend were affected by this change: The \"Template\" and the \"Filelist\" backend modules. The \"Template\" module had a custom, dedicated edit view for sys_template records. That view is dropped and EditDocumentController is now used for this purpose. That was the <a href=\"https:\/\/review.typo3.org\/52696\">easy patch<\/a>.\nThe more challenging part was the file handling stuff, as the forms had a custom structure and literally <em>any<\/em> file handling operation was affected by this. After 12 patch sets only, <a href=\"https:\/\/review.typo3.org\/#\/c\/53129\/\">that patch<\/a> was merged as well.\nBonus: Both patches removed some hooks that were barely used at all (probably only by t3editor itself).<\/p>\n<p>And now begins the fun stuff. The first thing I did was copying the most recent CodeMirror release into the core. After that, I removed <em>any<\/em> custom code and FormEngine hacks and, well, it wasn't so bad at all.\nThe main library was working and then I noticed: CodeMirror comes with a lot of so-called \"modes\" (syntax highlighting) and add-ons. I didn't want to include all available modes and add-ons, so I've implemented a configuration structure that allows to register additional modes and add-ons <a href=\"https:\/\/docs.typo3.org\/typo3cms\/extensions\/core\/latest\/Changelog\/master\/Feature-81901-ExtendT3editor.html\"><strong>on extension basis<\/strong><\/a>.\nAfter struggling with the add-ons's dependencies (even with RequireJS, you have to load any dependency by yourself), everything was fine so far. One of the last and most challenging...<\/p>","date_modified":"2022-03-29T20:56:29+02:00","tags":["story","extension"]},{"title":"Cleaning the hood: Record wizard","date_published":"2016-08-16T20:00:00+02:00","id":"https:\/\/www.scripting-base.de\/blog\/cleaning-the-hood-record-wizard","url":"https:\/\/www.scripting-base.de\/blog\/cleaning-the-hood-record-wizard","content_html":"<p>When you create a new content element by using the record wizard, you'll get a tab-based list of possible content elements to choose from. In the old days, those were PHP classes known as \"wizicons\" and automatically created by the kickstarter extension. You may guessed it already, it's about <code>$GLOBALS['TBE_MODULES_EXT']['xMOD_db_new_content_el']['addElClasses']<\/code>.<\/p>\n\n<p>This approach is not deprecated <strong>yet<\/strong>, but discouraged within the TYPO3 Core, as non of the shipped extensions use this mechanism.<\/p>\n<div class=\"toast toast-primary\">\n<p>As exception of the rule, <code>EXT:indexed_search<\/code> still uses this approach, in case <code>EXT:compatibility7<\/code> is installed.<\/p>\n<\/div>\n<p>In this article we're going to migrate the PHP-based classes to plain TypoScript. As example extension we use tt_news 7.6.3.<\/p>\n<p>In its <code>ext_tables.php<\/code> you may find the following code:<\/p>\n<pre><code class=\"language-php\">$TBE_MODULES_EXT['xMOD_db_new_content_el']['addElClasses']['tx_ttnews_wizicon'] = TYPO3\\CMS\\Core\\Utility\\ExtensionManagementUtility::extPath($_EXTKEY).'pi\/class.tx_ttnews_wizicon.php';<\/code><\/pre>\n<p>The registered class itself has a method called <code>proc()<\/code> that basically modifies an array only:<\/p>\n<pre><code class=\"language-php\">function proc($wizardItems) {\n    \/\/ ...\n\n    $wizardItems['plugins_tx_ttnews_pi'] = array(\n        'icon'=&gt;TYPO3\\CMS\\Core\\Utility\\ExtensionManagementUtility::extRelPath('tt_news').'pi\/ce_wiz.gif',\n        'title'=&gt;$LANG-&gt;getLLL('pi_title',$LL),\n        'description'=&gt;$LANG-&gt;getLLL('pi_plus_wiz_description',$LL),\n        'params'=&gt;'&amp;defVals[tt_content][CType]=list&amp;defVals[tt_content][list_type]=9'\n    );\n\n    return $wizardItems;\n}<\/code><\/pre>\n<h2>Migrate to TypoScript<\/h2>\n<p>Migrating this to TypoScript is a rather easy task. For beginning, a new file called <code>Configuration\/PageTS\/NewContentElementWizard.ts<\/code> is created within the extension directory:<\/p>\n<pre><code class=\"language-php\">mod.wizards {\n    newContentElement.wizardItems {\n        plugins {\n            elements {\n                plugins_tx_ttnews_pi {\n                    icon = EXT:tt_news\/pi\/ce_wiz.gif\n                    title = LLL:EXT:tt_news\/Resources\/Private\/Language\/locallang.xml:tt_news_title\n                    description = LLL:EXT:tt_news\/Resources\/Private\/Language\/locallang.xml:tt_news_description\n                    tt_content_defValues {\n                        CType = list\n                        list_type = 9\n                    }\n                }\n            }\n        }\n    }\n}<\/code><\/pre>\n<p>You probably noticed that the structure of the TypoScript is nearly the same as the PHP-based array, but there are a few minor differences:<\/p>\n<ul>\n<li>The extension name is prepended by <code>EXT:<\/code>, TYPO3 resolves this automatically<\/li>\n<li>The <code>params<\/code> key is replaced by <code>tt_content_defValues<\/code><\/li>\n<li>The parameters are now a key\/value array, not a concatenated parameter list for an URL<\/li>\n<\/ul>\n<p>Very important is that the configuration is wrapped within this scaffold, otherwise the TS won't work:<\/p>\n<pre><code class=\"language-php\">mod.wizards {\n    newContentElement.wizardItems {\n        plugins {\n            elements {\n                \/\/ YOUR CODE\n            }\n        }\n    }\n}<\/code><\/pre>\n<h2>Registration<\/h2>\n<p>Now, the TypoScript file must be registered. This is done within <code>ext_localconf.php<\/code>, <strong>NOT<\/strong> <code>ext_tables.php<\/code> (everytime you do this a unicorn dies, now do the math \ud83d\ude09):<\/p>\n<pre><code class=\"language-php\">\\TYPO3\\CMS\\Core\\Utility\\ExtensionManagementUtility::addPageTSConfig(\n    '&lt;INCLUDE_TYPOSCRIPT: source=\"FILE:EXT:tt_news\/Configuration\/PageTS\/NewContentElementWizard.ts\"&gt;'\n);<\/code><\/pre>\n<h2>Aftermath<\/h2>\n<p>We're nearly done. The last thing we have to do is removing any evidence of the old PHP-based approach. Means, you should remove the registration based on <code>$GLOBALS['TBE_MODULES_EXT']['xMOD_db_new_content_el']['addElClasses']<\/code> and clear the TYPO3 caches afterwards. If everything still works fine, it's sav to remove the wizicon PHP class.<\/p>","date_modified":"2022-03-29T20:56:29+02:00","tags":["extension","migration","cleaning the hood"]},{"title":"Cleaning the hood: TCA","date_published":"2016-08-09T17:52:00+02:00","id":"https:\/\/www.scripting-base.de\/blog\/cleaning-the-hood-tca","url":"https:\/\/www.scripting-base.de\/blog\/cleaning-the-hood-tca","content_html":"<p>Migrating an old extension can be a tough job, especially when the extension was developed before TYPO3 CMS 6.2 times and thus doesn't follow the current best practises. In this article I'll show you how to migrate the TCA placed in an <code>ext_tables.php<\/code> file of a <strong>real<\/strong> extension that was originally developed for TYPO3 4.5.<\/p>\n\n<p>In the old times, literally everything was placed in ext_tables.php: TCA, content element wizards, TypoScript includes and a lot more. In this series I'll guide you how to clean up that mess into a proper stucture. Today we handle the Table Configuration Array, or \"TCA\" for short.<\/p>\n<div class=\"toast toast-primary\">\n<p>As already teased, this is a <strong>real world extension<\/strong>. However, the extension name is anonymized. No further cleanups were made.<\/p>\n<\/div>\n<p>The TCA is probably split into two files: <code>ext_tables.php<\/code> and a dedicated <code>tca.php<\/code>.<\/p>\n<p>The part in ext_tables.php:<\/p>\n<pre><code class=\"language-php\">$TCA[\"my_extension_table\"] = Array (\n    \"ctrl\" =&gt; Array (\n        'title' =&gt; 'LLL:EXT:my_extension\/locallang_db.xml:my_extension_table',\n        'label' =&gt; 'title',\n        'tstamp' =&gt; 'tstamp',\n        'crdate' =&gt; 'crdate',\n        'cruser_id' =&gt; 'cruser_id',\n        'versioningWS' =&gt; TRUE,\n        'origUid' =&gt; 't3_origuid',\n        \"default_sortby\" =&gt; \"ORDER BY crdate DESC\",\n        \"delete\" =&gt; \"deleted\",\n        \"enablecolumns\" =&gt; Array (\n            \"disabled\" =&gt; \"hidden\",\n            \"starttime\" =&gt; \"starttime\",\n            \"endtime\" =&gt; \"endtime\",\n            \"fe_group\" =&gt; \"fe_group\",\n        ),\n        \"dynamicConfigFile\" =&gt; t3lib_extMgm::extPath($_EXTKEY).\"tca.php\",\n        \"iconfile\" =&gt; t3lib_extMgm::extRelPath($_EXTKEY).\"icon_tx_myextension_table.gif\",\n        \"dividers2tabs\"  =&gt; 1,\n        \"canNotCollapse\" =&gt; 1,\n    ),\n    \"feInterface\" =&gt; Array (\n        \"fe_admin_fieldList\" =&gt; \"hidden, starttime, endtime, fe_group, title, content\",\n    )\n);<\/code><\/pre>\n<p>The part in tca.php looks like this:<\/p>\n<pre><code class=\"language-php\">$TCA[\"tx_myextension_table\"] = Array (\n    \"ctrl\" =&gt; $TCA[\"tx_myextension_table\"][\"ctrl\"],\n    \"interface\" =&gt; Array (\n        \"showRecordFieldList\" =&gt; \"hidden,starttime,endtime,fe_group,title,content,info,version,edit_date,edit_user\"\n    ),\n    \"feInterface\" =&gt; $TCA[\"tx_myextension_table\"][\"feInterface\"],\n    \"columns\" =&gt; Array (\n        \"hidden\" =&gt; Array (\n            \"exclude\" =&gt; 1,\n            \"label\" =&gt; \"LLL:EXT:lang\/locallang_general.xml:LGL.hidden\",\n            \"config\" =&gt; Array (\n                \"type\" =&gt; \"check\",\n                \"default\" =&gt; \"0\"\n            )\n        ),\n        \"starttime\" =&gt; Array (\n            \"exclude\" =&gt; 1,\n            \"label\" =&gt; \"LLL:EXT:lang\/locallang_general.xml:LGL.starttime\",\n            \"config\" =&gt; Array (\n                \"type\" =&gt; \"input\",\n                \"size\" =&gt; \"8\",\n                \"max\" =&gt; \"20\",\n                \"eval\" =&gt; \"date\",\n                \"default\" =&gt; \"0\",\n                \"checkbox\" =&gt; \"0\"\n            )\n        ),\n        \"endtime\" =&gt; Array (\n            \"exclude\" =&gt; 1,\n            \"label\" =&gt; \"LLL:EXT:lang\/locallang_general.xml:LGL.endtime\",\n            \"config\" =&gt; Array (\n                \"type\" =&gt; \"input\",\n                \"size\" =&gt; \"8\",\n                \"max\" =&gt; \"20\",\n                \"eval\" =&gt; \"date\",\n                \"checkbox\" =&gt; \"0\",\n                \"default\" =&gt; \"0\",\n                \"range\" =&gt; Array (\n                    \"upper\" =&gt; mktime(0,0,0,12,31,2020),\n                    \"lower\" =&gt; mktime(0,0,0,date(\"m\")-1,date(\"d\"),date(\"Y\"))\n                )\n            )\n        ),\n        \"fe_group\" =&gt; Array (\n            \"exclude\" =&gt; 1,\n            \"l10n_mode\" =&gt; \"mergeIfNotBlank\",\n            \"label\" =&gt; \"LLL:EXT:lang\/locallang_general.xml:LGL.fe_group\",\n            \"config\" =&gt; Array (\n                \"type\" =&gt; \"select\",\n                \"size\" =&gt; 20,\n                \"maxitems\" =&gt; 20,\n                \"items\" =&gt; Array (\n                    Array(\"\", 0),\n                    Array(\"LLL:EXT:lang\/locallang_general.xml:LGL.hide_at_login\", -1),\n                    Array(\"LLL:EXT:lang\/locallang_general.xml:LGL.any_login\", -2),\n                    Array(\"LLL:EXT:lang\/locallang_general.xml:LGL.usergroups\", \"--div--\")\n                ),\n                \"foreign_table\" =&gt; \"fe_groups\",\n                \"foreign_table_where\" =&gt; \"ORDER BY title\",\n                \"itemListStyle\" =&gt; \"width:350px;\",\n                \"selectedListStyle\" =&gt; \"width:350px;\",\n            )\n        ),\n        \"title\" =&gt; Array (\n            \"exclude\" =&gt; 1,\n            \"label\" =&gt; \"LLL:EXT:my_extension\/locallang_db.xml:tx_myextension_table.title\",\n            \"config\" =&gt; Array (\n                \"type\" =&gt; \"input\",\n                \"size\" =&gt; \"50\",\n                \"max\"  =&gt; \"250\",\n                \"eval\" =&gt; \"required,trim\",\n            )\n        ),\n        \"content\" =&gt; Array (\n            \"exclude\" =&gt; 1,\n            \"label\" =&gt; \"LLL:EXT:my_extension\/locallang_db.xml:tx_myextension_table.content\",\n            \"config\" =&gt; Array (\n                \"type\" =&gt; \"text\",\n                \"cols\" =&gt; \"30\",\n                \"rows\" =&gt; \"5\",\n                \"wizards\" =&gt; Array(\n                    \"_PADDING\" =&gt; 2,\n                    \"RTE\" =&gt; Array(\n                        \"notNewRecords\" =&gt; 1,\n                        \"RTEonly\" =&gt; 1,\n                        \"type\" =&gt; \"script\",\n                        \"title\" =&gt; \"Full screen Rich Text Editing|Formatteret redigering i hele vinduet\",\n                        \"icon\" =&gt; \"wizard_rte2.gif\",\n                        \"script\" =&gt; \"wizard_rte.php\",\n                    ),\n                ),\n            ),\n            \"defaultExtras\" =&gt; \"richtext[]:rte_transform[mode=ts]\",...<\/code><\/pre>","summary":"Cleaning the hood: Migrate oldschool TCA into the current state-of-the-art.","date_modified":"2022-03-29T20:56:29+02:00","tags":["extension","migration","cleaning the hood"]},{"title":"AJAX with PSR-7","date_published":"2016-07-14T00:00:00+02:00","id":"https:\/\/www.scripting-base.de\/blog\/ajax-with-psr-7","url":"https:\/\/www.scripting-base.de\/blog\/ajax-with-psr-7","content_html":"<p>As stated in <a href=\"\/blog\/psr-7-for-backend-modules\">the previous article<\/a>, you may also use PSR-7 for AJAX requests. However, the implementation in the backend and the frontend is different. This article explains how you setup so-called \"routes\" for backend AJAX requests and how eIDs look like for the frontend.<\/p>\n\n<h2>Backend<\/h2>\n<p>In old days, AJAX handlers were registered in <code>ext_tables.php<\/code> via <code>\\TYPO3\\CMS\\Core\\Utility\\ExtensionManagementUtility::registerAjaxHandler()<\/code>. Although this still works, you should refrain from this as this is deprecated since TYPO3 v8. As an alternative you should use routes. To achieve this, create the file <code>Configuration\/Backend\/AjaxRoutes.php<\/code> in your extension and register your routes:<\/p>\n<pre><code class=\"language-php\">&lt;?php\n\nreturn [\n    'unique_identifier' =&gt; [\n        'path' =&gt; '\/unique\/identifier',\n        'target' =&gt; \\FooBar\\Baz\\Controller\\AjaxController::class . '::myAwesomeAction'\n    ],\n    'do_something' =&gt; [\n        'path' =&gt; '\/do\/something',\n        'target' =&gt; \\FooBar\\Baz\\Controller\\AjaxController::class . '::helloWorldAction'\n    ]\n];<\/code><\/pre>\n<p>Please be aware that the identifier and the path must be unique to avoid naming collisions.<\/p>\n<div class=\"toast toast-warning\">\n<p>After changing this file, the caches must be cleared.<\/p>\n<\/div>\n<p>After that, the routes are callable via AJAX. In an AMD module, you may call your action like this way:<\/p>\n<pre><code class=\"language-javascript\">$.ajax({\n    url: TYPO3.settings.ajaxUrls['unique_identifier'],\n    method: 'GET',\n    dataType: 'html',\n    success: function(response) {\n        console.log(response);\n    }\n});<\/code><\/pre>\n<p>In this example, we let the action return HTML code. As the default content type for AJAX actions with PSR-7 is <strong>application\/json<\/strong> in the backend, I'll show you how to change the content type:<\/p>\n<pre><code class=\"language-php\">public function helloWorldAction(\n    \\Psr\\Http\\Message\\ServerRequestInterface $request,\n    \\Psr\\Http\\Message\\ResponseInterface $response\n) {\n    $response-&gt;getBody()-&gt;write('&lt;b&gt;Hello &lt;i&gt;World&lt;\/i&gt;&lt;\/b&gt;');\n    $response = $response-&gt;withHeader('Content-Type', 'text\/html; charset=utf-8');\n    return $response;\n}<\/code><\/pre>\n<h2>Frontend<\/h2>\n<p>In the frontend you still use eID. Those may also run with PSR-7 now, but this is not mandatory <em>yet<\/em>. The registration itself slightly changes, instead of passing a file you also pass a controller::action combination:<\/p>\n<pre><code class=\"language-php\">$GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include']['fnord'] = \\FooBar\\Baz\\Controller\\FrontendAjaxController::class . '::fnordAction';<\/code><\/pre>\n<p>Thanks to the PSR-7 standard, the common \"layout\" of the action looks like the example from the backend:<\/p>\n<pre><code class=\"language-php\">public function fnordAction(\n    \\Psr\\Http\\Message\\ServerRequestInterface $request,\n    \\Psr\\Http\\Message\\ResponseInterface $response\n) {\n    $response-&gt;getBody()-&gt;write('I\\'m content fetched via AJAX.');\n    return $response;\n}<\/code><\/pre>\n<p>In contrast to the backend, the default content type for eID remains <strong>text\/html<\/strong>.<\/p>","summary":"Starting with TYPO3 v8, backend AJAX should be realized with PSR-7. This tutorial shows how AJAX calls for backend and frontend are handled now.","date_modified":"2022-03-29T20:56:29+02:00","tags":["psr-7","backend","tutorial","frontend"]},{"title":"PSR-7 for backend modules","date_published":"2016-07-04T00:00:00+02:00","id":"https:\/\/www.scripting-base.de\/blog\/psr-7-for-backend-modules","url":"https:\/\/www.scripting-base.de\/blog\/psr-7-for-backend-modules","content_html":"<p>Ah, you have a non-extbase extension whose content is cleanly structured into Classes, Configuration, Resources, etc. But then there are these pesky <code>modX\/index.php<\/code> files for your backend modules. That's not nice, but there is help: PSR-7. This tutorial shows how to setup your backend modules according to PSR-7.<\/p>\n\n<div class=\"toast toast-primary\">\n<p>The PSR-7 standard describes the communication with <a href=\"http:\/\/www.php-fig.org\/psr\/psr-7\/\">HTTP messages<\/a>.<\/p>\n<\/div>\n<p>You probably set up your backend modules this way in your ext_tables.php:<\/p>\n<pre><code class=\"language-php\">$extPath = \\TYPO3\\CMS\\Core\\Utility\\ExtensionManagementUtility::extPath($_EXTKEY);\n\n\\TYPO3\\CMS\\Core\\Utility\\ExtensionManagementUtility::addModule(\n    'web', 'txmoduleM1', '', $extPath . 'mod1\/', [\n        'access' =&gt; 'group,user',\n        'name' =&gt; 'web_txmoduleM1',\n        'labels' =&gt; [\n            'tabs_images' =&gt; [\n                'tab' =&gt; 'moduleicon.gif',\n            ],\n            'll_ref' =&gt; 'LLL:EXT:huselpusel\/mod1\/locallang.xlf',\n        ]\n    ]\n);<\/code><\/pre>\n<p>The hard-coded extension path looks not so nice and even triggers an entry in the deprecation log:<\/p>\n<div class=\"toast toast-warning\">\n<p>Registered \"web_txmoduleM1\" as a script-based module. Script-based modules are deprecated since TYPO3 CMS 7. Support will be removed with TYPO3 CMS 8, use the \"routeTarget\" option or dispatched modules instead.<\/p>\n<\/div>\n<p>To achieve this, the first step is creating a new controller, let's call it <code>Classes\/Controller\/BackendModuleController.php<\/code>:<\/p>\n<pre><code class=\"language-php\">&lt;?php\nnamespace Foobar\\Huselpusel\\Controller;\n\nuse TYPO3\\CMS\\Backend\\Module\\BaseScriptClass;\n\nclass BackendModuleController extends BaseScriptClass\n{\n}<\/code><\/pre>\n<p>The next step is inserting a public method that is called for dispatching the module. Let's call it <code>mainAction<\/code>:<\/p>\n<pre><code class=\"language-php\">public function mainAction(\n    \\Psr\\Http\\Message\\ServerRequestInterface $request,\n    \\Psr\\Http\\Message\\ResponseInterface $response\n) {\n    \/\/ Logic goes here...\n}<\/code><\/pre>\n<p><code>mainAction<\/code> is the entry point to the backend module which takes the <code>ServerRequestInterface<\/code> and <code>ResponseInterface<\/code> of PSR-7 as parameters. This method handles the logic of your backend module. There is one important thing that behaves completely different: content is not <code>echo<\/code>ed anymore (most likely by the method <code>printContent()<\/code>), you must use the <code>ResponseInterface<\/code> object.<\/p>\n<p>You may get your GET and POST parameters with the request object:<\/p>\n<pre><code class=\"language-php\">$get = $request-&gt;getQueryParams();\n$post = $request-&gt;getParsedBody();<\/code><\/pre>\n<p>To return the content, you may write into response's body and return the object:<\/p>\n<pre><code class=\"language-php\">$response-&gt;getBody()-&gt;write($this-&gt;doStuff());\nreturn $response;<\/code><\/pre>\n<p>You may also define a HTTP status code or set the content type:<\/p>\n<pre><code class=\"language-php\">if ($FAIL) {\n    $response = $response-&gt;withStatus(500);\n} else {\n    $response-&gt;getBody()-&gt;write($this-&gt;doStuff());\n    $response = $response-&gt;withHeader('Content-Type', 'text\/html; charset=utf-8');\n}<\/code><\/pre>\n<p>The content type for backend modules is by default <code>text\/html<\/code>, changing this is more important for AJAX calls based on PSR-7, but this will be explained in another blog post (hint hint).<\/p>\n<p>So far, so good. Let's get back ext_tables.php where the module is registered. The fourth parameter of <code>ExtensionManagementUtility::addModule()<\/code> requires the path to the module. Drop this, it must be an empty string now. As a replacement, adjust the configuration array by the new key <code>routeTarget<\/code>:<\/p>\n<pre><code class=\"language-php\">[\n    'routeTarget' =&gt; \\Foobar\\Huselpusel\\Controller\\BackendModuleController::class . '::mainAction',\n    'access' =&gt; 'group,user',\n    'name' =&gt; 'web_txmoduleM1',\n    \/\/...\n]<\/code><\/pre>\n<p>After clearing the caches of TYPO3, the backend module is now called by the PSR-7 way and there should be no new entry in the deprecation log. You may copy the whole logic of your former <code>modX\/index.php<\/code> which one exception:<\/p>\n<pre><code class=\"language-php\">$SOBE = GeneralUtility::makeInstance('tx_huselpusel_module1');\n$SOBE-&gt;main();\n$SOBE-&gt;printContent();<\/code><\/pre>\n<p>Drop that code, you don't need it anymore as the module is already dispatched with your <code>mainAction<\/code>.<\/p>\n<p>You may move the rest of <code>mod1<\/code> into their appropriate location:<\/p>\n<pre><code class=\"language-php\">moduleicon.gif =&gt;...<\/code><\/pre>","summary":"Since TYPO3 CMS 7.6 backend modules are registered with PSR-7. This tutorial explains how you can do this, too.","date_modified":"2022-03-29T20:56:29+02:00","tags":["psr-7","backend","tutorial"]}]}
