PHP deutsche Zeitangaben

Um die Ausgabe der Zeit, bspw. der deutschen Monate zu erhalten folgendes in PHP:

PHP
setlocale(LC_TIME, 'de_DE.UTF-8');
$formatter = new IntlDateFormatter(
        'de_DE', 
        IntlDateFormatter::LONG, 
        IntlDateFormatter::NONE
        );
$date = (new DateTime(VAR_CONTAINING_THE_DATE))->format('d. F Y');
$date = $formatter->format(new DateTime($date));
PHP

Docker Webdev Stack

Docker: Apache, MySQL, PHP und PhpMyAdmin im Container-Verbund

In diesem Beitrag bauen wir einen Web Application Stack mit Docker-Containern, bestehend aus Apache, MySQL, PHP (inkl. Redis) und PhpMyAdmin. Jeder Service läuft in einem eigenen Container. Auf diese Weise ist der Stack modular mit weiteren Diensten erweiterbar. Mit wenigen Handgriffen kann z.B. MySQL gegen PostgreSQL oder PHP 7.4 gegen eine andere Version getauscht werden. SSL und XDdebug sind bereits vorkonfiguriert. Der Stack kann somit für Development oder App Deployment benutzt werden.

Damit alles schön zusammen passt und wir möglichst wenig Aufwand haben, basiert alles auf den Docker Images von Bitnami.

Für unser Projekt benötigen wir die folgende Verzeichnisstruktur:

Der Unterordner “Build” ist nur für Geräte mit Apple Silicon relevant. Dazu später mehr.

Da alle Container über Docker Compose gestartet werden, erstellen wir zuerst die docker-compose.yml-Datei im Ordner “dstack”, neben dem Unterordner “docker”:

YAML
# AMPP 1.0.9
#
# Runs Apache, MySQL, PHP (+Redis) and PhpMyAdmin
# SSL is preconfigured.
# Imagemagick and XDebug are activated.
#
# Run with 
# docker-compose up -d
#
# (C)2020-2022 Harald Schneider
#
 
version: "3"
 
services:
 
  # --- MySQL 5.7
  #
  mysql:
    container_name: "ampp-mysql"
    image: bitnami/mysql:5.7
    restart: unless-stopped
    environment:
      - MYSQL_ROOT_PASSWORD=MYSQL_PASSWORD
      - MYSQL_USER=admin
      - MYSQL_PASSWORD=MYSQL_PASSWORD
    ports:
      - '3306:3306'
    volumes:
      - ./docker/mysql/data:/bitnami/mysql/data
      - ./docker/mysql/backup:/backup
 
  # --- PHP 7.4
  #
  php:
    container_name: "ampp-php"
    image: bitnami/php-fpm:latest
    restart: unless-stopped
    depends_on:
      - redis
    volumes:
      #- /Users/hschneider/Work/Web:/app:delegated
      - ./docker/www:/app:delegated
      - ./docker/php/php.ini:/opt/bitnami/php/etc/conf.d/php.ini:ro
 
  # --- Apache 2.4
  #
  apache:
    container_name: "ampp-apache"
    image: bitnami/apache:2.4
    ports:
      - '80:8080'
      - '443:8443'
    depends_on:
      - php
    volumes:
      #- /Users/hschneider/Work/Web:/app:delegated
      - ./docker/www:/app:delegated
      - ./docker/apache/my_vhost.conf:/vhosts/myapp.conf:ro
      - ./docker/apache/certs:/certs
      # Use this for bitnami's builtin certs:
      # ./docker/apache/certs:/opt/bitnami/apache2/conf/bitnami/certs 
 
  # --- Redis 6.0
  #
  redis:
    container_name: "ampp-redis"
    image: bitnami/redis:6.0
    restart: unless-stopped
    environment:
      - REDIS_PASSWORD=at15jx13
 
  # --- PhpMyAdmin latest
  # Acccess via
  # http://127.0.0.1:8180 or https://127.0.0.1:8143
  # Login with user root and mysql-password.
  #
  phpmyadmin:
    container_name: "ampp-phpmyadmin"
    image: bitnami/phpmyadmin:latest
    depends_on:
      - mysql
    ports:
      - '8180:8080'
      - '8143:8443'
    environment:
      - DATABASE_HOST=mysql
 
volumes:
  ampp-mysql:
    driver: local
YAML

Außer PhpMyAdmin benutzen alle Dienste ihre Standard-Ports. Alle wichtigen Daten werden in persistenten Ordnern ausserhalb der Container gespeichert (Volumes).

Apache

SSL Variante 1:

Mit mkcert ein neues lokales Zertifikat installieren

Bash
mkcert -install
Bash

Jetzt die Zertifikate für den Apache generieren:

Bash
mkcert localhost 127.0.0.1 ::1
Bash

und speichern diese Dateien  im Ordner ./docker/apache/certs. Wichtig ist, dass die Namen der Zertifikate mit denen aus der Apachekonfiguration übereinstimmen.

SSL Variante 2:

Zuerst generieren wir mit dem folgenden Befehl ein selbst signiertes SSL-Zertifikat und speichern dessen Dateien  im Ordner ./docker/apache/certs:

Bash
openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout server.key -out server.crt -subj "/CN=dstack.local" -days 3650
Bash

Für die Apache-Konfiguration legen wir folgende Datei an: ./docker/apache/my_vhost.conf:

Bash
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://php:9000/app/$1

DirectoryIndex disabled
DirectoryIndex index.php index.html

<VirtualHost *:8080>
  DocumentRoot "/app"

  <Directory "/app">
    Options -Indexes
    AllowOverride All
    Require all granted
  </Directory>
</VirtualHost>

# Create self signed certs with
# openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout server.key -out server.crt -subj "/CN=YOURDOMAIN.LOCAL" -days 3650
#
<VirtualHost *:8443>
  SSLEngine on  
  SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL  
  SSLCertificateFile "/certs/server.crt" // oder server.pem  
  SSLCertificateKeyFile "/certs/server.key"  // oder server-key.pem

  <Directory "/app">
    Options -Indexes
    AllowOverride All
    Require all granted
  </Directory>
</VirtualHost>
Bash

Für unsere Website-Daten legen wir noch den Ordner ./docker/www an. In unserem Beispiel liegt hier die Datei info.php mit folgendem Inhalt:

PHP
<?php
phpinfo();
?>
PHP

Damit ist Apache samt SSL komplett.

MySQL

Für MySQL benötigen wir nur einen leeren Ordner namens ./docker/mysql/data. Nach dem Start des MySQL-Containers, werden in diesem Ordner die Daten des MySQL-Servers erzeugt.

Bei Bedarf kann dieser Container noch mit einer Backup-Lösung für MySQL-Datenbanken, wie hier beschrieben, ergänzt werden.

PHP

Für die PHP-Konfiguration erzeugen wir die folgende Datei in ./docker/php/php.ini:

Bash
display_errors = On
expose_php = off

max_execution_time = 360
max_input_time = 360
memory_limit = 256M
upload_max_filesize = 1G
post_max_size = 1G

opcache.enable = 1
opcache.revalidate_freq = 2
opcache.validate_timestamps = 1
opcache.interned_strings_buffer = 32
opcache.memory_consumption = 256

extension=imagick.so
zend_extension = "/opt/bitnami/php/lib/php/extensions/xdebug.so"

[Xdebug]
xdebug.remote_autostart=1
xdebug.remote_enable=1
xdebug.default_enable=0
xdebug.remote_host=host.docker.internal
xdebug.remote_port=9000
xdebug.remote_connect_back=0
xdebug.profiler_enable=0
xdebug.remote_log="/tmp/xdebug.log"
Bash

Unter Anderem werden hier die ImageMagick extension aktiviert und XDebug konfiguriert. PHP ist damit ebenfalls komplett.

PhpMyAdmin

PhpMyAdmin benötigt keine extra Konfiguration und ist später über die Adresse 127.0.0.1:81 oder :8143 (SSL) erreichbar. Die Anmeldung geschieht mit Benutzer “root” und dem MySQL Root User Passwort.

Alle Docker-Container auf einmal starten

Der folgende Befehlt starten nun alle Container auf einmal:

Bash
docker-compose up -d
Bash

Zum Test geben wir im Browser http://127.0.0.1/info.php ein. Wenn wir alles richtig gemacht haben, erscheint nun die PHP-Info Page.

Docker: Zugriff von PHP auf MySQL liefert “Connection refused”

Hier ist folgende Besonderheit zu beachten:
Da ja alle Dienste in getrennten Docker-Containern laufen, wird ein Aufbau des DBConnects mit

Bash
'host' => '127.0.0.1:3306'
Bash

nicht funktionieren. Statt dessen ist der Docker-Containername als Hostname zu verwenden:

Bash
'host' => 'mysql:3306'
Bash

Solltet Ihr in docker-compose.yaml Port 3306 auf eine andere Portnummer gemapped haben, bleibt es in diesem Fall trotzdem bei 3306, da aus Perspektive des Webservers der interne Port des Containers sichtbar ist.

Nur für Apps, die von außen auf MySQL zugreifen (z.B. SequelPro, Querious, Navicat etc), wäre dann der gemappte Port gültig.

Docker: Bitnami Images auf Macs mit Apple Silicon lauffähig machen

Damit unser Docker Stack auch auf Macs mit Apple Silicon läuft, sind Build Patches für die Apache und PhpMyAdmin Docker Images notwendig. Dazu gehen wir wie folgt vor:

In der docker-compose.yml ergänzen wir die beiden markierten  Zeilen (build):

YAML
  # --- Apache 2.4
  #
  apache:
    container_name: "ampp-apache"
    image: bitnami/apache:2.4
    build: ./build/apache
...

  # --- PhpMyAdmin latest
  # Acccess via
  # http://127.0.0.1:8180 or https://127.0.0.1:8143
  # Login with user root and mysql-password.
  #
  phpmyadmin:
    container_name: "ampp-phpmyadmin"
    image: bitnami/phpmyadmin:latest
    build: ./build/phpmyadmin
...
YAML

Im Ordner “build” legen wir 2 weitere Ordner an:
“apache” mit einer Datei namens “Dockerfile”. Diese hat folgenden Inhalt:

YAML
FROM bitnami/apache:2.4
USER 0
RUN echo 'Mutex posixsem' >>/opt/bitnami/apache2/conf/httpd.conf
USER 1001
YAML

Sowie den Ordner “phpmyadmin” mit ebenfalls einer Datei namens “Dockerfile” und folgenden Inhalt:

YAML
FROM bitnami/phpmyadmin:latest
USER 0
RUN echo 'Mutex posixsem' >>/opt/bitnami/apache2/conf/httpd.conf
USER 1001
YAML

Danach starten beide Docker Container auch auf Apple Silicon.

Quelle

Typo3 // Extensionentwicklung Leitfaden

YouTube-Serie

Ich habe dazu eine Videoserie von Stefan Frömken zusammengestellt. Stefan ist ein Urgestein der Typo3-Community und wer könnte besser über dieses Thema referieren.

Grundgerüst

Composer

Für die Initialisierung mit Composer wird die Extension b13/make benutzt.
Der Ablauf ähnelt dem Initialisierungsprozess mit NPM.
Zu beachten ist hier, ob mit DDEV entwickelt wird oder nicht!
Die Entwicklung der Extension wird im src/extensions – Verzeichnis empfohlen.
In der Composer.json – Datei im Projektroot empfiehlt sich ein Wildcard anzugeben. Damit muss man nicht für jede weitere Extension einen neuen Eintrag machen.

JSON
"repositories": [
		{
			"type": "path",
			"url": "src/extensions/*"
		}
	],
JSON

Am Ende sollte die Extension im Typo3-Backend unter Extensions aufgeführt und aktiviert sein.

Datenbank

Die Datenbankstruktur wird in ext_tables.sql in der Extension angelegt.

SQL
CREATE TABLE tx_sales_campaign (
    titel varchar(255) DEFAULT '' NOT NULL COMMENT 'Titel',
    beschreibung varchar(255) DEFAULT '' NOT NULL COMMENT 'Beschreibung',
    zuordnung varchar(255) DEFAULT '' NOT NULL COMMENT 'Zuordnung',
);

CREATE TABLE tx_sales_object (
    bearbeitungsnummer varchar(20) DEFAULT '' NOT NULL COMMENT 'Bearbeitungsnummer',
    titel varchar(255) DEFAULT '' NOT NULL COMMENT 'Titel',
    preis float(30,2) NOT NULL COMMENT 'Preis in Euro',
    preisart varchar(50) DEFAULT '' NOT NULL COMMENT 'Preisart (z.B. Mindestgebot, Festpreis, Verhandlungsbasis)',
    beschreibung varchar(255) DEFAULT '' NOT NULL COMMENT 'Beschreibung der Kampagne',
    bilder int(11) DEFAULT 0 NOT NULL COMMENT 'Bilder',
    uploads int(11) DEFAULT 0 NOT NULL COMMENT 'Uploads',
    tx_sales_campaign_uid int(10) DEFAULT 0 NOT NULL COMMENT 'Verknüpfung zur Kampagne',
);
SQL

Über das Backend Verwaltungswerkzeuge > Wartung > Analyze Database Structure kann man die Tabellen anlegen oder Änderungen an der DB ausführen. Am besten man legt vorher noch das TCA an, damit auch dynamisch Spalten aus dem TCA gleich mit in der Tabelle angelegt werden. (bspw. crdate oder deleted etc.)

TCA (Table Configuration Array)

Für die Definition der Felder ist die TCA-Dokumentation unabdingbar.
Im Extensionordner legt man nun im Ordner EXT:sales/Configuration/TCA die Dateien tx_sales_campaign.php und tx_sales_objects.php an. Also für jede Tabelle eine eigene TCA.

PHP
<?php

/**
 * TCA for the sales table and its fields in the backend
 */



return [
    'ctrl' => [
        'title' => 'Verkaufskampagnen',
        'label' => 'titel',
        'tstamp' => 'tstamp',
        'crdate' => 'crdate',
        'delete' => 'deleted',
        'sortby' => 'sorting',
        'default_sortby' => 'titel',
        'versioningWS' => true,
        'rootLevel' => 0,
        //'iconfile' => 'EXT:styleguide/Resources/Public/Icons/tx_styleguide.svg',
        'origUid' => 't3_origuid',
        'languageField' => 'sys_language_uid',
        'transOrigPointerField' => 'l10n_parent',
        'transOrigDiffSourceField' => 'l10n_diffsource',
        'translationSource' => 'l10n_source',
        'searchFields' => 'titel,beschreibung',
        'security' => [
            'ignorePageTypeRestriction' => true,
        ],
        'enablecolumns' => [
            'disabled' => 'hidden',
            'starttime' => 'starttime',
            'endtime' => 'endtime',
        ],
    ],
    'columns' => [
        'titel' => [
            'exclude' => true,
            'label' => 'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.campaign.titel',
            'config' => [
                'type' => 'input',
            ],
        ],
        'beschreibung' => [
            'exclude' => true,
            'label' => 'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.campaign.beschreibung',
            'config' => [
                'type' => 'text',
                'cols' => 40,
                'rows' => 5,
                'eval' => 'trim',
            ],
        ],
        'endtime' => [
            'exclude' => false,
            'label' => 'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.campaign.anzeigen_bis',
            'config' => [
                'type' => 'input',
                'renderType' => 'inputDateTime',
                'eval' => 'datetime,int',  // 'datetime' für die Benutzeroberfläche, 'int' für Speicherung als Timestamp
                'default' => 0,            // Standardwert ist 0
            ],
        ],
        'starttime' => [
            'exclude' => false,
            'label' => 'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.campaign.anzeigen_von',
            'config' => [
                'type' => 'input',
                'renderType' => 'inputDateTime',
                'eval' => 'datetime,int',  // 'datetime' für die Benutzeroberfläche, 'int' für Speicherung als Timestamp
                'default' => 0,            // Standardwert ist 0
            ],
        ],
        /* Show delete field */
        'deleted' => [
            'exclude' => true,
            'label' => 'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.campaign.deleted',
            'config' => [
                'type' => 'check',
                'renderType' => 'checkboxToggle',
                'items' => [
                    [
                        0 => '',
                        1 => '',
                        'invertStateDisplay' => true
                    ]
                ],
            ],
        ],
        'hidden' => [
            'exclude' => false,
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.enabled',
            'config' => [
                'type' => 'check',
                'renderType' => 'checkboxToggle',
                'items' => [
                    [
                        0 => '',
                        1 => '',
                        'invertStateDisplay' => true
                    ]
                ],
            ]
        ],
    ],
    'types' => [
        [
            'showitem' => 'titel, beschreibung, hidden, starttime, endtime, deleted',
        ],
    ],
];
PHP
PHP
<?php

/**
 * TCA for the sales table and its fields in the backend
 */

return [
    'ctrl' => [
        'title' => 'Verkaufsobjekte',
        'label' => 'bearbeitungsnummer',
        'label_alt' => 'titel',
        'label_alt_force' => true,
        'tstamp' => 'tstamp',
        'crdate' => 'crdate',
        'delete' => 'deleted',
        'sortby' => 'sorting',
        'default_sortby' => 'bearbeitungsnummer',
        'versioningWS' => true,
        'rootLevel' => 0,
        //'iconfile' => 'EXT:styleguide/Resources/Public/Icons/tx_styleguide.svg',
        'origUid' => 't3_origuid',
        'languageField' => 'sys_language_uid',
        'transOrigPointerField' => 'l10n_parent',
        'transOrigDiffSourceField' => 'l10n_diffsource',
        'translationSource' => 'l10n_source',
        'searchFields' => 'titel,bearbeitungsnummer,beschreibung',
        'security' => [
            'ignorePageTypeRestriction' => true,
        ],
        'enablecolumns' => [
            'disabled' => 'hidden'
        ],
    ],
    'columns' => [
        'bearbeitungsnummer' => [
            'exclude' => true,
            'label' => 'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.object.bearbeitungsnummer',
            'config' => [
                'type' => 'input',
            ],
        ],
        'tx_sales_campaign_uid' => [
            'exclude' => true,
            'label' => 'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.object.kampagne',
            'config' => [
                'type' => 'select',
                'renderType' => 'selectSingle',
                'foreign_table' => 'tx_sales_campaign', // Tabelle für die Kampagnen
                'maxitems' => 1, // Da es eine 1:n Beziehung ist
            ],
        ],
        'titel' => [
            'exclude' => true,
            'label' => 'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.object.titel',
            'config' => [
                'type' => 'input',
            ],
        ],
        'beschreibung' => [
            'exclude' => true,
            'label' => 'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.object.beschreibung',
            'config' => [
                'type' => 'text',
                'cols' => 40,
                'rows' => 5,
                'eval' => 'trim',
            ],
        ],
        'preis' => [
            'exclude' => true,
            'label' => 'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.object.preis',
            'config' => [
                'type' => 'number',
                'size' => 10,
                'format' => 'decimal',
                'eval' => 'double,required', // 'double' für Gleitkommazahlen, 'required' für Pflichtfeld
                'placeholder' => 'z.B. 99.99', // Hinweistext im Eingabefeld
            ],
        ],
        'preisart' => [
            'exclude' => true,
            'label' => 'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.object.preisart',
            'config' => [
                'type' => 'select',
                'renderType' => 'selectSingle',
                'items' => [
                    ['Mindestangebot', 'Mindestangebot'],
                    ['Festpreis', 'Festpreis'],
                    ['Verhandlungsbasis', 'Verhandlungsbasis']
                ],
                'default' => 'Festpreis', // Optional: Standardwert festlegen
            ],
        ],
        'bilder' => [
            'exclude' => true,
            'label' => 'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.object.bilder',
            'config' => [
                'type' => 'inline',
                'foreign_table' => 'sys_file_reference',
                'foreign_field' => 'uid_foreign',
                'foreign_sortby' => 'sorting_foreign',
                'foreign_table_field' => 'tablenames',
                'foreign_match_fields' => [
                    'fieldname' => 'bilder',
                ],
                'foreign_label' => 'uid_local',
                'foreign_selector' => 'uid_local',
                'overrideChildTca' => [
                    'columns' => [
                        'uid_local' => [
                            'config' => [
                                'appearance' => [
                                    'elementBrowserType' => 'file',
                                    'elementBrowserAllowed' => 'jpg,jpeg,png,gif',
                                ],
                            ],
                        ],
                    ],
                ],
                'filter' => [
                    [
                        'userFunc' => \TYPO3\CMS\Core\Imaging\IconFactory::class . '->getFilePickerFilter',
                        'parameters' => [
                            'allowedFileExtensions' => 'jpg,jpeg,png,gif',
                        ],
                    ],
                ],
                'maxitems' => 5, // Maximale Anzahl der hochladbaren Bilder
                'appearance' => [
                    'collapseAll' => true,
                    'showSynchronizationLink' => true,
                    'showAllLocalizationLink' => true,
                ],
            ],
        ],
        'uploads' => [
            'exclude' => true,
            'label' => 'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.object.uploads',
            'config' => [
                'type' => 'inline',
                'foreign_table' => 'sys_file_reference',
                'foreign_field' => 'uid_foreign',
                'foreign_sortby' => 'sorting_foreign',
                'foreign_table_field' => 'tablenames',
                'foreign_match_fields' => [
                    'fieldname' => 'uploads',
                ],
                'foreign_label' => 'uid_local',
                'foreign_selector' => 'uid_local',
                'overrideChildTca' => [
                    'columns' => [
                        'uid_local' => [
                            'config' => [
                                'appearance' => [
                                    'elementBrowserType' => 'file',
                                    'elementBrowserAllowed' => 'pdf',
                                ],
                            ],
                        ],
                    ],
                ],
                'filter' => [
                    [
                        'userFunc' => \TYPO3\CMS\Core\Imaging\IconFactory::class . '->getFilePickerFilter',
                        'parameters' => [
                            'allowedFileExtensions' => 'pdf',
                        ],
                    ],
                ],
                'maxitems' => 5, // Maximale Anzahl der hochladbaren PDFs
                'appearance' => [
                    'collapseAll' => true,
                    'showSynchronizationLink' => true,
                    'showAllLocalizationLink' => true,
                ],
            ],
        ],
        'endtime' => [
            'exclude' => false,
            'label' => 'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.object.anzeigen_bis',
            'config' => [
                'type' => 'input',
                'renderType' => 'inputDateTime',
                'eval' => 'datetime,int',  // 'datetime' für die Benutzeroberfläche, 'int' für Speicherung als Timestamp
                'default' => 0,            // Standardwert ist 0
            ],
        ],
        'starttime' => [
            'exclude' => false,
            'label' => 'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.object.anzeigen_von',
            'config' => [
                'type' => 'input',
                'renderType' => 'inputDateTime',
                'eval' => 'datetime,int',  // 'datetime' für die Benutzeroberfläche, 'int' für Speicherung als Timestamp
                'default' => 0,            // Standardwert ist 0
            ],
        ],
        'hidden' => [
            'exclude' => false,
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.enabled',
            'config' => [
                'type' => 'check',
                'renderType' => 'checkboxToggle',
                'items' => [
                    [
                        0 => '',
                        1 => '',
                        'invertStateDisplay' => true
                    ]
                ],
            ]
        ],
    ],
    'types' => [
        [
            'showitem' => 'bearbeitungsnummer, tx_sales_campaign_uid, titel, beschreibung, preis, preisart, bilder, uploads, hidden, starttime, endtime',
        ],
    ],
];
PHP

Systemordner

Wenn über die Extension Datensätze angelegt werden sollen. Dann ist es am besten dafür einen gesonderten Systemordner anzulegen, in dem die Datensätze gespeichert werden sollen.

Über das Listenmodul kann man dann in dem Systemordner neue Datensätze anlegen.

Übersetzungen

Übersetzungen für Plugin, Felder etc. werden in folgendem Ordner angelegt EXT:Resources/Private/Language. Für Deutsch bspw: de.locallang_db.xlf.
Wie auf die Sprachen referenziert wird, sieht man im TCA. (bspw: LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.object.anzeigen_von)

XML
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.0">
    <file source-language="en" datatype="plaintext" original="messages" date="2024-10-25T13:36:39Z"
          product-name="sales">
        <header/>
        <body>

            <!-- Kampagnen -->
            <trans-unit id="tx_sales_sale.campaign.titel">
                <source>Title</source>
                <target>Titel</target>
            </trans-unit>

            <trans-unit id="tx_sales_sale.campaign.beschreibung">
                <source>Description</source>
                <target>Beschreibung</target>
            </trans-unit>

            <trans-unit id="tx_sales_sale.campaign.zuordnung">
                <source>Assignment</source>
                <target>Zuordnung</target>
            </trans-unit>

            <trans-unit id="tx_sales_sale.campaign.anzeigen_von">
                <source>Publishing date</source>
                <target>Veröffentlichungsdatum</target>
            </trans-unit>

            <trans-unit id="tx_sales_sale.campaign.anzeigen_bis">
                <source>Expiry date</source>
                <target>Ablaufdatum</target>
            </trans-unit>


            <!-- Verkäufe -->
            <trans-unit id="tx_sales_sale.object.bearbeitungsnummer">
                <source>processing number</source>
                <target>Bearbeitungsnummer</target>
            </trans-unit>

            <trans-unit id="tx_sales_sale.object.titel">
                <source>Title</source>
                <target>Titel</target>
            </trans-unit>

            <trans-unit id="tx_sales_sale.object.preis">
                <source>Price (in Euro)</source>
                <target>Preis (in Euro)</target>
            </trans-unit>

            <trans-unit id="tx_sales_sale.object.preisart">
                <source>Pricetype</source>
                <target>Preisart</target>
            </trans-unit>

            <trans-unit id="tx_sales_sale.object.beschreibung">
                <source>Description</source>
                <target>Beschreibung</target>
            </trans-unit>

            <trans-unit id="tx_sales_sale.object.kampagne">
                <source>Campaign</source>
                <target>Kampagne</target>
            </trans-unit>

            <trans-unit id="tx_sales_sale.object.bilder">
                <source>Images</source>
                <target>Bilder</target>
            </trans-unit>

            <trans-unit id="tx_sales_sale.object.uploads">
                <source>Documents</source>
                <target>Dokumente</target>
            </trans-unit>


            <!-- Plugin -->
            <trans-unit id="tx_sales_sale.pluginlabel">
                <source>List sales</source>
                <target>Verkäufe auflisten</target>
            </trans-unit>

            <trans-unit id="tx_sales_sale.plugindescription">
                <source>List sales of campaings of the organisation</source>
                <target>Füge dieses Plugin hinzu, um Verkäufe von Kampagnen der Organisation aufzulisten</target>
            </trans-unit>
            
        </body>
    </file>
</xliff>
XML

Feldspezifische Berechtigungen im Backend (anlegen, ändern, etc.)

Um Felder nur speziellen Benutzergruppen editierbar, anlegbar zur Verfügung zu stellen, muss im TCA an dem entsprechenden Feld exclude aus true gesetzt sein. Damit ist es per Default erstmal nicht verfügbar. Dann muss es in der Benutzergruppe in den Zugriffsrechten unter Erlaubte Ausschlussfelder explizit freigegeben werden.

Plugin für Backend

Im Pluginmanager verfügbar machen

Als erstes legt man unter EXT:sales/Configuration/TCA/Overrides eine Datei tt_content.php an. In dieser wird das Plugin registriert.

PHP
<?php

use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

ExtensionUtility::registerPlugin(
    'Sales',
    'list',
    'LLL:EXT:sales/Resources/Private/Language/locallang_db.xlf:tx_sales_sale.pluginlabel',
    'tx-sales-svgicon' // Has to be defined in EXT:Configuration/Icons.php
);
PHP

Damit sollte das Plugin dann schon auswählbar sein. (Cache löschen nicht vergessen!)

Typoscript des Plugins verfügbar machen

Um später Typoscript aus der Extension im Backend verfügbar zu machen legen wir ebenfalls in dem Verzeichnis eine sys_template.php mit folgendem Inhalt an. Damit ist sämtlicher TypoScript-Code im Verzeichnis Configuration/TypoScript verfügbar. Das Template wird dann im Backend unter Template > Rootseite wählen > Gesamtes Typoscript bearbeiten > Erweiterte Optionen eingebunden. In dem Typoscript wird bspw. festgelegt, wo die HTML-Templates für die Views liegen.

PHP
<?php

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

/**
 * Make the extension configuration accessible for TYPO3 Template
 */

ExtensionManagementUtility::addStaticFile('sales', 'Configuration/TypoScript', 'TS template for sales');
PHP

Content Element Wizard

Der Content Element Wizard beinhaltet bspw. die Pluginübersicht.

Wenn man das Plugin auch schon direkt im Content Element Wizard auswählen können soll, muss noch folgendes TypoScript ergänzt werden. (EXT:sales/Configuration/TSConfig/Page/ContentElementWizard.tsconfig)

Plaintext
# Configuration for Content Element Wizard
mod.wizards {
    newContentElement.wizardItems {
        plugins {
            elements {
                tenders_list {
                    iconIdentifier = tx-tenders-svgicon
                    title = LLL:EXT:tenders/Resources/Private/Language/locallang_db.xlf:tx_tenders_tender.pluginlabel
                    description = LLL:EXT:tenders/Resources/Private/Language/locallang_db.xlf:tx_tenders_tender.plugindescription
                    tt_content_defValues {
                        CType = list
                        list_type = tenders_list
                        header = LLL:EXT:tenders/Resources/Private/Language/locallang_db.xlf:tx_tenders_tender.pluginlabel
                    }
                }
            }
        }
    }
}

# Add CType Preview
mod.web_layout.tt_content.preview.list.sales_list = EXT:sales/Resources/Private/Templates/PluginPreview/Show.html
Plaintext

In Zeile 22 wird noch die Ansicht des ContentTypes in der Seite selbst mit einem Template verfeinert. So sieht man schon in der Übersicht, aus welchem Ordner sich die Extension bedient. Das Template EXT:sales/Resources/Private/Templates/PluginPreview/Show.html sieht wie folgt aus.

PHP
<html lang="en" xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<h4>Tenders</h4>

<table class="table">
    <thead>
        <tr>
            <th>Config</th>
            <th>Value</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><strong>Header</strong></td>
            <td>{header}</td>
        </tr>
        <tr>
            <td><strong>Storage</strong></td>
            <td>{pages}</td>
        </tr>
    </tbody>
</table>
</html>
PHP

und hat folgendes Ergebnis

Das Typoscript wird über die Datei EXT:sales/Configuration/page.tsconfig als Typoscript ins Typo3 gepusht.

Plaintext
@import 'EXT:sales/Configuration/TSConfig/Page/ContenElementWizard.tsconfig'
Plaintext

Eigenes Icon für Plugin

Das Icon im SVG-Format legt man unter EXT:Resources/Public/Icons ab. (bspw. sales.svg). Unter EXT:Configuration legt man eine Icons.php an, in der man den Icon-Identifier bescheibt.

PHP
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider;

return [
    // Icon identifier
    'tx-sales-svgicon' => [
        // Icon provider class
        'provider' => SvgIconProvider::class,
        // The source SVG for the SvgIconProvider
        'source' => 'EXT:sales/Resources/Public/Icons/sales.svg',
    ]
];
PHP

Frontendanbindung

Extbase für das Datenhandling

Alle Klassen für das Plugin kommen ins Verzeichnis EXT:sales/Classes. Einstiegspunkt ist der Controller. Der EXT:sales/Classes/Controller/SaleController.php sieht wie folgt aus.

PHP
<?php


declare(strict_types=1);


namespace OeKonzept\Sales\Controller;

use InvalidArgumentException;
use OeKonzept\Sales\Domain\Model\Sale;
use OeKonzept\Sales\Domain\Repository\SaleRepository;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class SaleController extends ActionController
{

    /**
     * @var \OeKonzept\Sales\Domain\Repository\SaleRepository
     */
    protected $saleObjectRepository;

    public function injectSaleRepository(SaleRepository $salesObjectRepository)
    {
        $this->saleObjectRepository = $salesObjectRepository;
    }


    /**
     * Action to list all sales objects
     * 
     * @return ResponseInterface 
     * @throws InvalidArgumentException 
     */

    public function listAction()
    {
        $salesObjects = $this->saleObjectRepository->findSalesWithCampaigns();


        $this->view->assign('sales', $salesObjects);

        return $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'text/html; charset=utf-8')
            ->withBody($this->streamFactory->createStream($this->view->render()));
    }

    /**
     * Action to show a single sales object
     * 
     * @param Sale $salesObject 
     * @return ResponseInterface 
     * @throws InvalidArgumentException 
     */

    public function showAction(Sale $sale)
    {
        $this->view->assign('sale', $sale);

        return $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'text/html; charset=utf-8')
            ->withBody($this->streamFactory->createStream($this->view->render()));
    }
}
PHP

Der Controller bekommt seine Daten aus dem Repository. Das Repo ist zuständig für die Datenbeschaffung und der Controller ist damit von der Datenquelle entkoppelt.
Das EXT:sales/Classes/Domain/SaleRepository.php kann recht leer sein, da es die Grundfunktionalitäten vom Extbase Repository erbt.

PHP
<?php

namespace OeKonzept\Sales\Domain\Repository;

use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use TYPO3\CMS\Extbase\Persistence\Repository;
use TYPO3\CMS\Extbase\Utility\DebuggerUtility;

class SaleRepository extends Repository
{

    public function findSalesWithCampaigns(): QueryResultInterface
    {
        $query = $this->createQuery();
        $query->setOrderings(['bearbeitungsnummer' => 'ASC']);
        $sales = $query->execute();
        return $sales;
    }
}
PHP

Damit Extbase die einzelnen Datensätze in ein Model packen kann, erstellt man ein Domain Model dafür. (EXT:sales/Classes/Domain/Model/Sale.php)

PHP
<?php

namespace OeKonzept\Sales\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Sale extends AbstractEntity
{

    protected string $bearbeitungsnummer = '';

    protected string $titel = '';

    protected float $preis = 0.0;

    protected string $preisart = '';

    protected string $beschreibung = '';

    protected \TYPO3\CMS\Extbase\Domain\Model\FileReference $bilder;

    protected \TYPO3\CMS\Extbase\Domain\Model\FileReference $uploads;

    protected int $tx_sales_campaign_uid = 0;


    /**
     * Get the value of bearbeitungsnummer
     */
    public function getBearbeitungsnummer(): string
    {
        return $this->bearbeitungsnummer;
    }

    /**
     * Set the value of bearbeitungsnummer
     */
    public function setBearbeitungsnummer(string $bearbeitungsnummer): self
    {
        $this->bearbeitungsnummer = $bearbeitungsnummer;

        return $this;
    }

    /**
     * Get the value of titel
     */
    public function getTitel(): string
    {
        return $this->titel;
    }

    /**
     * Set the value of titel
     */
    public function setTitel(string $titel): self
    {
        $this->titel = $titel;

        return $this;
    }

    /**
     * Get the value of preis
     */
    public function getPreis(): float
    {
        return $this->preis;
    }

    /**
     * Set the value of preis
     */
    public function setPreis(float $preis): self
    {
        $this->preis = $preis;

        return $this;
    }

    /**
     * Get the value of preisart
     */
    public function getPreisart(): string
    {
        return $this->preisart;
    }

    /**
     * Set the value of preisart
     */
    public function setPreisart(string $preisart): self
    {
        $this->preisart = $preisart;

        return $this;
    }

    /**
     * Get the value of beschreibung
     */
    public function getBeschreibung(): string
    {
        return $this->beschreibung;
    }

    /**
     * Set the value of beschreibung
     */
    public function setBeschreibung(string $beschreibung): self
    {
        $this->beschreibung = $beschreibung;

        return $this;
    }

    /**
     * Get the value of bilder
     */
    public function getBilder(): \TYPO3\CMS\Extbase\Domain\Model\FileReference
    {
        return $this->bilder;
    }

    /**
     * Set the value of bilder
     */
    public function setBilder(\TYPO3\CMS\Extbase\Domain\Model\FileReference $bilder): self
    {
        $this->bilder = $bilder;

        return $this;
    }

    /**
     * Get the value of uploads
     */
    public function getUploads(): \TYPO3\CMS\Extbase\Domain\Model\FileReference
    {
        return $this->uploads;
    }

    /**
     * Set the value of uploads
     */
    public function setUploads(\TYPO3\CMS\Extbase\Domain\Model\FileReference $uploads): self
    {
        $this->uploads = $uploads;

        return $this;
    }

    /**
     * Get the value of tx_sales_campaign_uid
     */
    public function getTxSalesCampaignUid(): int
    {
        return $this->tx_sales_campaign_uid;
    }

    /**
     * Set the value of tx_sales_campaign_uid
     */
    public function setTxSalesCampaignUid(int $tx_sales_campaign_uid): self
    {
        $this->tx_sales_campaign_uid = $tx_sales_campaign_uid;

        return $this;
    }
}
PHP

Extbase wird mit der entsprechenden Tabelle, welche die Daten für das Model enthält in EXT:sales/Configuration/Extbase/Persistance/Classes.php verbunden.

PHP
<?php

/**
 * Mapping of classes to tables
 */

declare(strict_types=1);

return [
    \OeKonzept\Sales\Domain\Model\Sale::class => [
        'tableName' => 'tx_sales_object',
    ]
];
PHP

Der Controller für das Plugin muss konfiguriert werden in der EXT:sales/ext_localconf.php.

PHP
<?php

use OeKonzept\Sales\Controller\SaleController;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

ExtensionUtility::configurePlugin(
    'Sales',
    'list',
    [
        SaleController::class => 'list, show'
    ]
);
PHP

Frontend-Templates für die Fluidviews (Show.html, List.html, etc.)

Für die Auflistung der einzelnen Datensätze bspw. EXT:sales/Resources/Private/Templates/Sale/List.html

PHP
<ul>
    <f:for each="{sales}" as="sale">
        <li>
            <f:link.action action="show" arguments="{sale: sale}" class="btn btn-link">{sale.titel}
            </f:link.action>
        </li>
    </f:for>
</ul>
PHP

PHP 8 Codeanpassungen

Return type of ... should either be compatible with ...Code-Sprache: JavaScript (javascript)

Entweder, wenn der Returntype variiert über die Methode die Annotation
#[\ReturnTypeWillChange] einfügen oder die Signatur, speziell meistens den Rückgabewert anpassen.

Deprecated: Creation of dynamic property DefaultBehaviour::$nonExistingProp is deprecatedCode-Sprache: PHP (php)

Ab PHP 8.2 sind dynamische Klasseneigenschaften deprecated und Klassen damit versiegelt. Sollte das dennoch benötigt werden, muss über der Klasse die Annotation #[AllowDynamicProperties] angegeben werden.

Function utf8_encode() is deprecatedCode-Sprache: PHP (php)

Diese Funktion ist zu ersetzen durch
mb_convert_encoding($string, 'UTF-8', 'ISO-8859-1');

Function utf8_decode() is deprecatedCode-Sprache: PHP (php)

Diese Funktion ist zu ersetzen durch
mb_convert_encoding($string, 'ISO-8859-1', 'UTF-8');

Was ist ein Bearer Token? Beispiel einer API Autorisierung über HTTP

Was ist ein Token?

Ein Token kann von einer Partei an eine andere übergeben werden. Ein Personalausweis ist ein Token, das die Stadt an einen Bürger übergibt.

Was sind Bearer Token?

Die Gültigkeit eines Ausweises ist auf eine Person beschränkt. Bei einer Polizeikontrolle muss stets der eigene Ausweis vorgezeigt werden. Es gibt Tokens, die dieser Beschränkung nicht unterliegen. Ein Schlüssel kann einer Werkstatt übergeben werden, damit diese ein Auto entsperren können. Es genügt, im Besitz des Schlüssels zu sein, um die Türe zu öffnen. Der Begriff Bearer bedeutet auf Deutsch Träger oder Inhaber. Ein Bearer Token ist ein Inhaber-Token, das nicht an eine bestimmte Identität gebunden ist.

Token in der IT Sicherheit

In der IT Sicherheit werden Token für die Authentifizierung und Autorisierung von Benutzern und Systemen verwendet. Neben Hardware-Token wie z.B. Kryptokarten und Pin Generatoren können auch Daten als Token verwendet werden.

Die folgenden Token enthalten selbst sinnvolle Daten für den Empfänger bereit:

  • JSON Web Token (JWT)
  • X.509 Zertifikate
  • Passwort Hashes

Des Weiteren gibt es Token, die als Stellvertreter dienen, z.B. für sensible Zugangsdaten, die auch Credentials genannt werden. Der Inhalt oder Wert eines Stellvertreter-Tokens hat selbst keine Bedeutung. Er ist nicht self-contained. Um ein Stellvertreter-Token zu schützen wird als Wert z.B. eine große Zufallszahl verwendet, die nicht leicht zu erraten ist.

Bearer Token Autorisierung für API Aufrufe

Im Folgenden wird eine API Autorisierung über das HTTP Protokoll mit einem Bearer Token beschrieben.

Als Parteien sind ein Keksmonster, eine Keksfabrik und eine Authentifizierungsstelle an der Kommunikation beteiligt. Um Kekse zu erhalten, möchte das Monster eine geschützte API der Keksfabrik aufrufen. Das folgende Bild zeigt diesen Aufbau.

Abbildung : Keksmonster, Keksfabrik und Authentifizierungsstelle für Bearer Token Autorisierung

Wir gehen davon aus, dass alle Aufrufe zusätzlich über das TLS Protokoll geschützt werden, damit das Token nicht auf dem Transportweg ausgespäht werden kann.

Die Geschichte beginnt damit, dass das Keksmonster nur noch einen Keks besitzt und dringend Nachschub benötigt. Den gibt es in der Keksfabrik. Aus diesem Grund macht das Keksmonster dort eine Anfrage, um neue Kekse zu bekommen.

Abbildung : Erste API Anfrage durch das Keksmonster

Die Anfrage sieht im HTTP Protokoll wie folgt aus:

GET /kekse HTTP/1.1

Die Keksfabrik liefert Kekse nur an autorisierte Keksmonster aus. Da die Anfrage des Monsters keine Autorisierung enthält, antwortet die Fabrik mit einem 401 Unauthorized Statuscode und lehnt die Lieferung ab.

Abbildung : Die Keksfabrik liefert keine Kekse ohne Bearer Token Autorisierung

Mit der Antwort liefert die Keksfabrik zwei wichtige Informationen an das Monster. Zum einen die Methode, mit der eine Autorisierung durchzuführen ist und zum anderen den Gültigkeitsbereich der Autorisierung. Die Antwort hat in etwa folgendes Aussehen:

HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer realm=“Keksfabrik“

Ok, stimmt… das Keksmonster erinnert sich, dass es zuvor auch nur autorisiert Kekse bekommen hat. Deswegen folgt jetzt ein kurzer Umweg über die Authentifizierungsstelle. Dort muss das Keksmonster seine Identität mit Benutzername und Passwort nachweisen.

Abbildung : POST Request zur Authentifizierung gegenüber der Authentifizierungsstelle

Die Anfrage des Monsters sieht wie folgt aus:

POST /ausgabe HTTP/1.1 { „user“:“Keksvernichter“, „password“:“U+1F36A“ }

Die Authentifizierungsstelle akzeptiert die Zugangsdaten. Der Keksvernichter ist dort als Keksmonster gut bekannt. Alle Voraussetzungen für die Ausstellung eines Tokens sind somit erfüllt.

Abbildung : Nach erfolgreicher Validierung stellt die Authentifizierungsstelle ein Bearer Token aus

Das Listing unten zeigt die Antwort der Fabrik mit dem Bearer Token im Body:

HTTP/1.1 200 Ok { „token“:“S0VLU0UhIExFQ0tFUiEK“ }

Super, jetzt kann das Keksmonster endlich neue Kekse anfragen! Das Keksmonster bereitet die nächste Nachricht vor um seine neue Keksration zu erhalten. Diese Anfrage gleicht der ersten Anfrage, aber dieses Mal ist das Bearer Token enthalten.

Abbildung : Anfrage nach Keksen mit Bearer Token im Authorization Header

Das Bearer Token wird als Authorization Header in die Anfrage eingebettet.

GET /kekse HTTP/1.1 Authorization: Bearer S0VLU0UhIExFQ0tFUiEK

Diesmal läuft das Prozedere etwas anders ab. Die Keksfabrik erkennt, dass die Anfrage ein Bearer Token für die Autorisierung enthält. Um dem Bearer Token zu vertrauen, muss dieses überprüft bzw. validiert werden.

Die Keksfabrik benötigt die Hilfe der Authentifizierungsstelle, da das Token über keinen kryptographischen Schutz verfügt. Tokens, die über einen kryptographischen Schutz z.B. eine Signatur verfügen, können von den Empfängern direkt überprüft werden. Bei APIs wird der Einfachheit halber oft ein Token ohne Kryptographie verwendet.

Um das Token zu validieren wird es an die Authentifizierungsstelle geschickt.

Abbildung : Die Keksfabrik sendet das Token zur Validierung an die Authentifizierungsstelle

Die Anfrage zur Validierung des Tokens sieht wie folgt aus:

POST /validate HTTP/1.1 { „token“:“S0VLU0UhIExFQ0tFUiEK“ }

Das Token ist bei der Authentifizierungsstelle bekannt, da dieses vor wenigen Sekunden von ihr selbst für das Monster ausgestellt wurde. Das Token ist auch nicht abgelaufen, da kein Timeout überschritten wurde. Die Authentifizierungsstelle antwortet wie erwartet, dass das Token gültig ist.

Abbildung : Das Bearer Token ist gültig

Die Authentifizierungsstelle antwortet der Keksfabrik mit einer Erfolgsmeldung:

HTTP/1.1 200 Ok

Die Keksfabrik hat die Verbindung zum Keksmonster aufrechterhalten und sendet nun die Antwort – Kekse!

Abbildung : Das Keksmonster ist am Ziel. Die Kekse werden über die Leitung geschickt

Die Antwort der Keksfabrik:

HTTP/1.1 200 Ok [„Keks“, „Keks“, „Keks“]

Das Keksmonster hat erfolgreich einen autorisierten Aufruf mit Hilfe eines Bearer Tokens durchgeführt.

Zusammenfassung und Ausblick

Im Beispiel wurden alle Schritte für eine Autorisierung mit Bearer Token aufgezeigt. Wird eine geschützte API ohne Token aufgerufen, bekommt der Aufrufer einen Hinweis, dass eine Autorisierung erfolgen muss. Um ein Token zu erhalten muss die Identität des Aufrufers überprüft werden. Ist das erfolgreich geschehen, so wird ein Bearer Token ausgestellt. Dies kann für den API Aufruf verwendet werden. Ist das Token nicht self-contained, so muss die API das Token über einen Dritten validieren. Bei erfolgreicher Validierung des Tokens wird die Anfrage des Aufrufers durchgeführt.

Der Round-Trip zur Authentifizierungsstelle kann eingespart werden, wenn ein self-contained Token verwendet wird. Beispielsweise ein JSON Web Token mit einer digitalen Signatur. Der Preis für den eingesparten Aufruf ist eine höhere Komplexität durch die Kryptographie. Bei beiden Varianten handelt es sich um Bearer Token, mit denen sich der Inhaber ausweisen kann.

Bearer Token werden für OAuth2 und API Keys verwendet. Hier findest du einen weiteren Artikel mit einer Einführung in OAuth2 und einen Einblick in den Authorization Code Ablauf.

PHP MP4 Video für Safari ausliefern

Safari spielt Videos, welche man plain versucht auszugeben, nicht ab. Das scheint mit der Pufferung zu tun zu haben. Dafür funktioniert folgendes Skript:

$fp = fopen($filepath, "rb");
    $size = filesize($filepath);
    $length = $size;
    $start = 0;
    $end = $size - 1;
    header('Content-type: video/mp4');
    header("Accept-Ranges: 0-$length");
    if (isset($_SERVER['HTTP_RANGE'])) {
        $c_start = $start;
        $c_end = $end;
        list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);

        if (strpos($range, ',') !== false) {
            header('HTTP/1.1 416 Requested Range Not Satisfiable');
            header("Content-Range: bytes $start-$end/$size");
            exit;
        }

        if ($range == '-') {
            $c_start = $size - substr($range, 1);
        } else {
            $range = explode('-', $range);
            $c_start = $range[0];
            $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
        }

        $c_end = ($c_end > $end) ? $end : $c_end;

        if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
            header('HTTP/1.1 416 Requested Range Not Satisfiable');
            header("Content-Range: bytes $start-$end/$size");
            exit;
        }

        $start = $c_start;
        $end = $c_end;
        $length = $end - $start + 1;
        fseek($fp, $start);
        header('HTTP/1.1 206 Partial Content');
    }

    header("Content-Range: bytes $start-$end/$size");
    header("Content-Length: ".$length);

    $buffer = 1024 * 8;

    while(!feof($fp) && ($p = ftell($fp)) <= $end) {
        if ($p + $buffer > $end) {
            $buffer = $end - $p + 1;
        }
        set_time_limit(0);
        echo fread($fp, $buffer);
        flush();
    }

    fclose($fp);
    exit;Code-Sprache: PHP (php)

Quelle