From 9bf72d4109c40a59599f3d90ba929b835e205e51 Mon Sep 17 00:00:00 2001 From: mhofmann Date: Fri, 8 Nov 2024 15:57:49 +0100 Subject: [PATCH 1/9] [TASK] Introduce runTests.sh --- Build/Scripts/checkUtf8Bom.sh | 25 + Build/Scripts/duplicateExceptionCodeCheck.sh | 181 +++++ Build/Scripts/runTests.sh | 653 +++++++++++++++++++ Build/Scripts/testMethodPrefixChecker.php | 87 +++ Build/Scripts/validateRstFiles.php | 267 ++++++++ Build/php-cs-fixer/config.php | 120 ++++ Build/phpstan/phpstan-baseline.neon | 16 + Build/phpstan/phpstan.neon | 17 + Build/phpunit/FunctionalTests.xml | 60 ++ Build/phpunit/FunctionalTestsBootstrap.php | 30 + Build/phpunit/UnitTests.xml | 49 ++ Build/phpunit/UnitTestsBootstrap.php | 87 +++ Build/typoscript-lint/typoscript-lint.yml | 15 + 13 files changed, 1607 insertions(+) create mode 100755 Build/Scripts/checkUtf8Bom.sh create mode 100755 Build/Scripts/duplicateExceptionCodeCheck.sh create mode 100755 Build/Scripts/runTests.sh create mode 100755 Build/Scripts/testMethodPrefixChecker.php create mode 100755 Build/Scripts/validateRstFiles.php create mode 100644 Build/php-cs-fixer/config.php create mode 100644 Build/phpstan/phpstan-baseline.neon create mode 100644 Build/phpstan/phpstan.neon create mode 100644 Build/phpunit/FunctionalTests.xml create mode 100644 Build/phpunit/FunctionalTestsBootstrap.php create mode 100644 Build/phpunit/UnitTests.xml create mode 100644 Build/phpunit/UnitTestsBootstrap.php create mode 100644 Build/typoscript-lint/typoscript-lint.yml diff --git a/Build/Scripts/checkUtf8Bom.sh b/Build/Scripts/checkUtf8Bom.sh new file mode 100755 index 0000000..64c2f72 --- /dev/null +++ b/Build/Scripts/checkUtf8Bom.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +######################### +# +# Check all UTF-8 files do not contain BOM. +# +# It expects to be run from the core root. +# +########################## + +FILES=`find . -type f \ + ! -path "./.Build/*" \ + ! -path "./.git/*" \ + ! -path "./.php_cs.cache" \ + ! -path "./.php-cs-fixer.cache" \ + ! -path "./Documentation-GENERATED-temp/*" \ + -print0 | xargs -0 -n1 -P8 file {} | grep 'UTF-8 Unicode (with BOM)'` + +if [ -n "${FILES}" ]; then + echo "Found UTF-8 files with BOM:"; + echo ${FILES}; + exit 1; +fi + +exit 0 diff --git a/Build/Scripts/duplicateExceptionCodeCheck.sh b/Build/Scripts/duplicateExceptionCodeCheck.sh new file mode 100755 index 0000000..e020316 --- /dev/null +++ b/Build/Scripts/duplicateExceptionCodeCheck.sh @@ -0,0 +1,181 @@ +#!/bin/bash + +######################### +# +# Find duplicate exception timestamps and list them. +# Additionally find exceptions that have no exception code. +# Optionally write the list of found exception codes to the standard output stream in JSON format. +# +# It expects to be run from the core root. +# +########################## + +# -------------------------- +# --- default parameters --- +# -------------------------- +print=0 +scanPath="Classes/" + +ignoreFiles=() +ignoreFiles+="Service/Client/Client.php" + + +# ------------------------ +# --- print usage info --- +# ------------------------ +usage() +{ + echo "Usage: $0 [options] " + echo " " + echo "No arguments/default: Check exception numbers for duplicates. " + echo " " + echo "Options: " + echo " -p " + echo " Specifies whether the list of exceptions found should " + echo " be output as JSON in the standard output stream. " + echo " " + echo " -h " + echo " Show this help. " + echo " " + exit 0 +} + +# ----------------------- +# --- parsing of args --- +# ----------------------- +OPTIND=1 + +while getopts "hp" opt;do + case "$opt" in + h) + usage + ;; + p) + print=1 + ;; + *) + exit 1 + ;; + esac +done + +shift $((OPTIND-1)) + +# ------------------------------------------------ +# --- print list of found exceptions to stdout --- +# ------------------------------------------------ +print_exceptions() { + IFS=$'\n' sorted=($(sort -u <<<"${exceptionCodes[*]}")); unset IFS + + local numExceptions=${#sorted[@]} + + printf "{\n" + printf " \"exceptions\": {\n" + if [ ${numExceptions} -gt 0 ]; then + for (( i=0; i<${numExceptions}-1; i++ )); + do + printf " \"%s\":\"%s\",\n" "${sorted[$i]}" "${sorted[$i]}" + done + printf " \"%s\":\"%s\"\n" "${sorted[${numExceptions}-1]}" "${sorted[${numExceptions}-1]}" + fi + printf " },\n" + printf " \"total\":%s\n" "${numExceptions}" + printf "}\n" +} + +# ------------------------------------------------------------------------------- +# --- check PHP files recursively for missing and duplicate exception numbers --- +# ------------------------------------------------------------------------------- +scan_exceptions() { + local foundNewFile=0 + local oldFilename="" + local firstLineOfMatch="" + local foundExceptionInFile=1 + local exceptionCodes=() + + # grep + # '-r' recursive + # '--include '*.php'' in all .php files + # '-Pzoab' pcre regex, -zo remove all linebreaks for multiline match, treat all files as text, output position "filename:position: match", binary position + # + # (?:(?!Exception\()[\w\\])* negative lookahead. capture all alphanum and \ until we reach "Exception(" + # eat "Exception(" + # (?:(?!\);).|[\r\n])*\);[\r\n]+ negative lookahead again, eat everything including a \n until we reach the first ");", then line breaks + + cd "$scanPath" || exit 1 + + grep \ + -r \ + --include '*.php' \ + -Pzoab \ + 'new (?:(?!Exception\()[\w\\])*Exception\((?:(?!\);).|[\r\n])*\);[\r\n]+' \ + | \ + { + while read line; + do + possibleFilename=`echo ${line} | cut -d':' -f1` + if [[ ${possibleFilename} =~ .php$ ]]; then + # the matched line consists of a file name match, we're dealing with a new match here. + foundNewFile=1 + oldFilename=${currentFilename} + currentFilename=${possibleFilename} + else + foundNewFile=0 + fi + + # skip file if in blacklist + if [[ {$ignoreFiles[@]} =~ ${currentFilename} ]]; then + continue + fi + + # check for match in previous file name + if [[ ${foundNewFile} -eq 1 ]] && [[ ${foundExceptionInFile} -eq 0 ]]; then + if [ "$print" -ne "1" ]; then + # checking exception codes: exit + # listing exception codes: ignore + echo "File: $oldFilename" + echo "The created exception contains no 10 digit exception code as second argument, in or below this line:" + echo "$firstLineOfMatch" + exit 1 + fi + fi + + # reset found flag if we're handling new file + if [[ ${foundNewFile} -eq 1 ]]; then + foundExceptionInFile=0 + firstLineOfMatch=${line} + fi + + # see if the line consists of an exception code + if [[ "$line" =~ .*([0-9]{10}).* ]]; then + foundExceptionInFile=1 + exceptionCode=${BASH_REMATCH[1]} + # check if that code was registered already + if [[ " ${exceptionCodes[@]} " =~ " ${exceptionCode} " ]]; then + if [ "$print" -ne "1" ]; then + # checking exception codes: exit + # listing exception codes: ignore + echo "Duplicate exception code ${exceptionCode} in file:" + echo ${currentFilename} + exit 1 + fi + fi + exceptionCodes+=(${exceptionCode}) + fi + done || exit 1 + + if [ "$print" -eq "1" ]; then + print_exceptions + fi + + exit 0 + } + + exitCode=$? + + cd - > /dev/null + + exit $exitCode +} + +scan_exceptions diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh new file mode 100755 index 0000000..12a03f0 --- /dev/null +++ b/Build/Scripts/runTests.sh @@ -0,0 +1,653 @@ +#!/usr/bin/env bash + +# +# lavitto/typo3-form-to-database test runner based on docker/podman. +# +if [ "${CI}" != "true" ]; then + trap 'echo "runTests.sh SIGINT signal emitted";cleanUp;exit 2' SIGINT +fi + +waitFor() { + local HOST=${1} + local PORT=${2} + local TESTCOMMAND=" + COUNT=0; + while ! nc -z ${HOST} ${PORT}; do + if [ \"\${COUNT}\" -gt 10 ]; then + echo \"Can not connect to ${HOST} port ${PORT}. Aborting.\"; + exit 1; + fi; + sleep 1; + COUNT=\$((COUNT + 1)); + done; + " + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name wait-for-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${IMAGE_ALPINE} /bin/sh -c "${TESTCOMMAND}" + if [[ $? -gt 0 ]]; then + kill -SIGINT -$$ + fi +} + +cleanUp() { + ATTACHED_CONTAINERS=$(${CONTAINER_BIN} ps --filter network=${NETWORK} --format='{{.Names}}') + for ATTACHED_CONTAINER in ${ATTACHED_CONTAINERS}; do + ${CONTAINER_BIN} rm -f ${ATTACHED_CONTAINER} >/dev/null + done + ${CONTAINER_BIN} network rm ${NETWORK} >/dev/null +} + +handleDbmsOptions() { + # -a, -d, -i depend on each other. Validate input combinations and set defaults. + case ${DBMS} in + mariadb) + [ -z "${DATABASE_DRIVER}" ] && DATABASE_DRIVER="mysqli" + if [ "${DATABASE_DRIVER}" != "mysqli" ] && [ "${DATABASE_DRIVER}" != "pdo_mysql" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="10.4" + if ! [[ ${DBMS_VERSION} =~ ^(10.4|10.5|10.6|10.7|10.8|10.9|10.10|10.11|11.0|11.1)$ ]]; then + echo "Invalid combination -d ${DBMS} -i ${DBMS_VERSION}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + mysql) + [ -z "${DATABASE_DRIVER}" ] && DATABASE_DRIVER="mysqli" + if [ "${DATABASE_DRIVER}" != "mysqli" ] && [ "${DATABASE_DRIVER}" != "pdo_mysql" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="8.0" + if ! [[ ${DBMS_VERSION} =~ ^(8.0|8.1|8.2|8.3|8.4)$ ]]; then + echo "Invalid combination -d ${DBMS} -i ${DBMS_VERSION}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + postgres) + if [ -n "${DATABASE_DRIVER}" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="10" + if ! [[ ${DBMS_VERSION} =~ ^(10|11|12|13|14|15|16)$ ]]; then + echo "Invalid combination -d ${DBMS} -i ${DBMS_VERSION}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + sqlite) + if [ -n "${DATABASE_DRIVER}" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + if [ -n "${DBMS_VERSION}" ]; then + echo "Invalid combination -d ${DBMS} -i ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + *) + echo "Invalid option -d ${DBMS}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + ;; + esac +} + +cleanCacheFiles() { + echo -n "Clean caches ... " + rm -rf \ + .Build/.cache \ + .php-cs-fixer.cache + echo "done" +} + +cleanTestFiles() { + # test related + echo -n "Clean test related files ... " + rm -rf \ + .Build/Web/typo3temp/var/tests/ + echo "done" +} + +cleanRenderedDocumentationFiles() { + echo -n "Clean rendered documentation files ... " + rm -rf \ + Documentation-GENERATED-temp + echo "done" +} + +loadHelp() { + # Load help text into $HELP + read -r -d '' HELP < + Specifies which test suite to run + - cgl: cgl test and fix all php files + - checkBom: check UTF-8 files do not contain BOM + - checkExceptionCodes: Check for duplicate exception codes + - checkRst: test .rst files for integrity + - checkTestMethodsPrefix: check tests methods do not start with "test" + - clean: clean up build, cache and testing related files and folders + - cleanCache: clean up cache related files and folders + - cleanRenderedDocumentation: clean up rendered documentation files and folders (Documentation-GENERATED-temp) + - clean: clean up build and testing related files + - composer: "composer" with all remaining arguments dispatched. + - composerUpdate: "composer update", handy if host has no PHP + - functional: functional tests + - lintPhp: PHP linting + - lintTypoScript: TypoScript linting + - renderDocumentation: This uses the official rendering container to render the extension documentation. + - phpstan: phpstan analyze + - phpstanGenerateBaseline: regenerate phpstan baseline, handy after phpstan updates + - unit: PHP unit tests + + -b + Container environment: + - docker + - podman + + If not specified, podman will be used if available. Otherwise, docker is used. + + -a + Only with -s functional|functionalDeprecated + Specifies to use another driver, following combinations are available: + - mysql + - mysqli (default) + - pdo_mysql + - mariadb + - mysqli (default) + - pdo_mysql + + -d + Only with -s functional|functionalDeprecated|acceptance|acceptanceInstall + Specifies on which DBMS tests are performed + - sqlite: (default): use sqlite + - mariadb: use mariadb + - mysql: use MySQL + - postgres: use postgres + + -i version + Specify a specific database version + With "-d mariadb": + - 10.4 short-term, maintained until 2024-06-18 (default) + - 10.5 short-term, maintained until 2025-06-24 + - 10.6 long-term, maintained until 2026-06 + - 10.7 short-term, no longer maintained + - 10.8 short-term, maintained until 2023-05 + - 10.9 short-term, maintained until 2023-08 + - 10.10 short-term, maintained until 2023-11 + - 10.11 long-term, maintained until 2028-02 + - 11.0 development series + - 11.1 short-term development series + With "-d mysql": + - 8.0 maintained until 2026-04 (default) + - 8.1 unmaintained since 2023-10 + - 8.2 unmaintained since 2024-01 + - 8.3 maintained until 2024-04 + - 8.4 maintained until 2032-04 LTS + With "-d postgres": + - 10 unmaintained since 2022-11-10 (default) + - 11 maintained until 2023-11-09 + - 12 maintained until 2024-11-14 + - 13 maintained until 2025-11-13 + - 14 maintained until 2026-11-12 + - 15 maintained until 2027-11-11 + - 16 maintained until 2028-11-09 + + -t <12|13> + Only with -s composerInstall|composerInstallMin|composerInstallMax + Specifies the TYPO3 CORE Version to be used + - 12: (default) use TYPO3 v12 + - 13: use TYPO3 v13 + + -p <8.1|8.2|8.3|8.4> + Specifies the PHP minor version to be used + - 8.1: (default) use PHP 8.1 + - 8.2: use PHP 8.2 + - 8.3: use PHP 8.3 + - 8.4: use PHP 8.4 + + -x + Only with -s functional|functionalDeprecated|unit|unitDeprecated|unitRandom|acceptance|acceptanceInstall + Send information to host instance for test or system under test break points. This is especially + useful if a local PhpStorm instance is listening on default xdebug port 9003. A different port + can be selected with -y + + -y + Send xdebug information to a different port than default 9003 if an IDE like PhpStorm + is not listening on default port. + + -o + Only with -s unitRandom + Set specific random seed to replay a random run in this order again. The phpunit randomizer + outputs the used seed at the end (in gitlab core testing logs, too). Use that number to + replay the unit tests in that order. + + -n + Only with -s cgl + Activate dry-run in CGL check that does not actively change files and only prints broken ones. + + -u + Update existing typo3/core-testing-*:latest container images and remove dangling local volumes. + New images are published once in a while and only the latest ones are supported by core testing. + Use this if weird test errors occur. Also removes obsolete image versions of typo3/core-testing-*. + + -h + Show this help. + +Examples: + # Run all core unit tests using PHP 8.2 + ./Build/Scripts/runTests.sh + ./Build/Scripts/runTests.sh -s unit + + # Run all core units tests and enable xdebug (have a PhpStorm listening on port 9003!) + ./Build/Scripts/runTests.sh -x + + # Run unit tests in phpunit verbose mode with xdebug on PHP 8.2 and filter for test canRetrieveValueWithGP + ./Build/Scripts/runTests.sh -x -p 8.2 -e "-v --filter canRetrieveValueWithGP" + + # Run functional tests in phpunit with a filtered test method name in a specified file + # example will currently execute two tests, both of which start with the search term + ./Build/Scripts/runTests.sh -s functional -e "--filter deleteContent" typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/ActionTest.php + + # Run functional tests on postgres with xdebug, php 8.2 and execute a restricted set of tests + ./Build/Scripts/runTests.sh -x -p 8.2 -s functional -d postgres typo3/sysext/core/Tests/Functional/Authentication + + # Run functional tests on postgres 11 + ./Build/Scripts/runTests.sh -s functional -d postgres -k 11 + + # Run restricted set of application acceptance tests + ./Build/Scripts/runTests.sh -s acceptance typo3/sysext/core/Tests/Acceptance/Application/Login/BackendLoginCest.php:loginButtonMouseOver + + # Run installer tests of a new instance on sqlite + ./Build/Scripts/runTests.sh -s acceptanceInstall -d sqlite +EOF +} + +# Test if docker exists, else exit out with error +if ! type "docker" >/dev/null 2>&1 && ! type "podman" >/dev/null 2>&1; then + echo "This script relies on docker or podman. Please install" >&2 + exit 1 +fi + +# Option defaults +TEST_SUITE="unit" +CORE_VERSION="12" +DBMS="sqlite" +PHP_VERSION="8.1" +PHP_XDEBUG_ON=0 +PHP_XDEBUG_PORT=9003 +PHPUNIT_RANDOM="" +CGLCHECK_DRY_RUN=0 +DATABASE_DRIVER="" +DBMS_VERSION="" +CONTAINER_BIN="" +CONTAINER_HOST="host.docker.internal" + +# Option parsing updates above default vars +# Reset in case getopts has been used previously in the shell +OPTIND=1 +# Array for invalid options +INVALID_OPTIONS=() +# Simple option parsing based on getopts (! not getopt) +while getopts "a:b:s:d:i:p:t:xy:o:nhu" OPT; do + case ${OPT} in + s) + TEST_SUITE=${OPTARG} + ;; + b) + if ! [[ ${OPTARG} =~ ^(docker|podman)$ ]]; then + INVALID_OPTIONS+=("${OPTARG}") + fi + CONTAINER_BIN=${OPTARG} + ;; + a) + DATABASE_DRIVER=${OPTARG} + ;; + d) + DBMS=${OPTARG} + ;; + i) + DBMS_VERSION=${OPTARG} + ;; + p) + PHP_VERSION=${OPTARG} + if ! [[ ${PHP_VERSION} =~ ^(8.1|8.2|8.3|8.4)$ ]]; then + INVALID_OPTIONS+=("p ${OPTARG}") + fi + ;; + t) + CORE_VERSION=${OPTARG} + if ! [[ ${CORE_VERSION} =~ ^(12|13)$ ]]; then + INVALID_OPTIONS+=("t ${OPTARG}") + fi + ;; + x) + PHP_XDEBUG_ON=1 + ;; + y) + PHP_XDEBUG_PORT=${OPTARG} + ;; + o) + PHPUNIT_RANDOM="--random-order-seed=${OPTARG}" + ;; + n) + CGLCHECK_DRY_RUN=1 + ;; + h) + loadHelp + echo "${HELP}" + exit 0 + ;; + u) + TEST_SUITE=update + ;; + \?) + INVALID_OPTIONS+=("${OPTARG}") + ;; + :) + INVALID_OPTIONS+=("${OPTARG}") + ;; + esac +done + +# Exit on invalid options +if [ ${#INVALID_OPTIONS[@]} -ne 0 ]; then + echo "Invalid option(s):" >&2 + for I in "${INVALID_OPTIONS[@]}"; do + echo "-"${I} >&2 + done + echo >&2 + echo "call \".Build/Scripts/runTests.sh -h\" to display help and valid options" + exit 1 +fi + +handleDbmsOptions + +COMPOSER_ROOT_VERSION="4.1.x-dev" +CONTAINER_INTERACTIVE="-it --init" +HOST_UID=$(id -u) +USERSET="" +if [ $(uname) != "Darwin" ]; then + USERSET="--user $HOST_UID" +fi + +# Go to the directory this script is located, so everything else is relative +# to this dir, no matter from where this script is called, then go up two dirs. +THIS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd "$THIS_SCRIPT_DIR" || exit 1 +cd ../../ || exit 1 +ROOT_DIR="${PWD}" + +# Create .cache dir: composer need this. +mkdir -p .Build/.cache +mkdir -p .Build/Web/typo3temp/var/tests + + +IS_CORE_CI=0 +# ENV var "CI" is set by gitlab-ci. We use it here to distinct 'local' and 'CI' environment. +if [ "${CI}" == "true" ]; then + IS_CORE_CI=1 + IMAGE_PREFIX="" + CONTAINER_INTERACTIVE="" +fi + +# determine default container binary to use: 1. podman 2. docker +if [[ -z "${CONTAINER_BIN}" ]]; then + if type "podman" >/dev/null 2>&1; then + CONTAINER_BIN="podman" + elif type "docker" >/dev/null 2>&1; then + CONTAINER_BIN="docker" + fi +fi + +IMAGE_PHP="ghcr.io/typo3/core-testing-$(echo "php${PHP_VERSION}" | sed -e 's/\.//'):latest" +IMAGE_ALPINE="docker.io/alpine:3.8" +IMAGE_DOCS="ghcr.io/typo3-documentation/render-guides:latest" +IMAGE_SELENIUM="docker.io/selenium/standalone-chrome:4.0.0-20211102" +IMAGE_MARIADB="docker.io/mariadb:${DBMS_VERSION}" +IMAGE_MYSQL="docker.io/mysql:${DBMS_VERSION}" +IMAGE_POSTGRES="docker.io/postgres:${DBMS_VERSION}-alpine" + + +# Detect arm64 and use a seleniarm image. +# In a perfect world selenium would have a arm64 integrated, but that is not on the horizon. +# So for the time being we have to use seleniarm image. +ARCH=$(uname -m) +if [ ${ARCH} = "arm64" ]; then + IMAGE_SELENIUM="docker.io/seleniarm/standalone-chromium:4.1.2-20220227" +fi +echo "Architecture" ${ARCH} "requires" ${IMAGE_SELENIUM} "to run acceptance tests." + +# Set $1 to first mass argument, this is the optional test file or test directory to execute +shift $((OPTIND - 1)) + +SUFFIX=$(echo $RANDOM) +NETWORK="lavitto-form-to-database-${SUFFIX}" +${CONTAINER_BIN} network create ${NETWORK} >/dev/null + +if [ "${CONTAINER_BIN}" == "docker" ]; then + # docker needs the add-host for xdebug remote debugging. podman has host.container.internal built in + CONTAINER_COMMON_PARAMS="${CONTAINER_INTERACTIVE} --rm --network ${NETWORK} --add-host ${CONTAINER_HOST}:host-gateway ${USERSET} -v ${ROOT_DIR}:${ROOT_DIR} -w ${ROOT_DIR}" + CONTAINER_SIMPLE_PARAMS="${CONTAINER_INTERACTIVE} --rm --network ${NETWORK} --add-host ${CONTAINER_HOST}:host-gateway ${USERSET} -v ${ROOT_DIR}:${ROOT_DIR} -w ${ROOT_DIR}" + DOCUMENTATION_COMMON_PARAMS="${CONTAINER_INTERACTIVE} --rm ${USERSET} -v ${ROOT_DIR}:/project" +else + # podman + CONTAINER_HOST="host.containers.internal" + CONTAINER_COMMON_PARAMS="${CONTAINER_INTERACTIVE} ${CI_PARAMS} --rm --network ${NETWORK} -v ${ROOT_DIR}:${ROOT_DIR} -w ${ROOT_DIR}" + CONTAINER_SIMPLE_PARAMS="${CONTAINER_INTERACTIVE} ${CI_PARAMS} --rm -v ${ROOT_DIR}:${ROOT_DIR} -w ${ROOT_DIR}" + DOCUMENTATION_COMMON_PARAMS="${CONTAINER_INTERACTIVE} ${CI_PARAMS} --rm -v ${ROOT_DIR}:${ROOT_DIR} -v ${ROOT_DIR}:/project" +fi + +if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE="-e XDEBUG_MODE=off" + XDEBUG_CONFIG=" " +else + XDEBUG_MODE="-e XDEBUG_MODE=debug -e XDEBUG_TRIGGER=foo" + XDEBUG_CONFIG="client_port=${PHP_XDEBUG_PORT} client_host=${CONTAINER_HOST}" +fi + +# Suite execution +case ${TEST_SUITE} in + cgl) + if [ "${CGLCHECK_DRY_RUN}" -eq 1 ]; then + COMMAND="php -dxdebug.mode=off .Build/bin/php-cs-fixer fix --config Build/php-cs-fixer/config.php -v --dry-run --using-cache no --diff" + else + COMMAND="php -dxdebug.mode=off .Build/bin/php-cs-fixer fix --config Build/php-cs-fixer/config.php --using-cache no" + fi + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name cgl-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + checkBom) + COMMAND="Build/Scripts/checkUtf8Bom.sh" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name cgl-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + checkRst) + COMMAND="php -dxdebug.mode=off Build/Scripts/validateRstFiles.php" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name cgl-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + checkExceptionCodes) + COMMAND="Build/Scripts/duplicateExceptionCodeCheck.sh" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name cgl-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + checkTestMethodsPrefix) + COMMAND="php -dxdebug.mode=off Build/Scripts/testMethodPrefixChecker.php" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name cgl-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + clean) + cleanCacheFiles + cleanRenderedDocumentationFiles + cleanTestFiles + ;; + cleanCache) + cleanCacheFiles + ;; + cleanRenderedDocumentation) + cleanRenderedDocumentationFiles + ;; + cleanTests) + cleanTestFiles + ;; + composer) + COMMAND=(composer "$@") + ${CONTAINER_BIN} run ${CONTAINER_SIMPLE_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + composerUpdate) + COMMAND=(composer update) + ${CONTAINER_BIN} run ${CONTAINER_SIMPLE_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + functional) + PHPUNIT_CONFIG_FILE="Build/phpunit/FunctionalTests.xml" + COMMAND=(.Build/bin/phpunit -c ${PHPUNIT_CONFIG_FILE} --exclude-group not-${DBMS} "$@") + case ${DBMS} in + mariadb) + echo "Using driver: ${DATABASE_DRIVER}" + ${CONTAINER_BIN} run --name mariadb-func-${SUFFIX} --network ${NETWORK} -d -e MYSQL_ROOT_PASSWORD=funcp --tmpfs /var/lib/mysql/:rw,noexec,nosuid ${IMAGE_MARIADB} >/dev/null + waitFor mariadb-func-${SUFFIX} 3306 + CONTAINERPARAMS="-e typo3DatabaseDriver=${DATABASE_DRIVER} -e typo3DatabaseName=func_test -e typo3DatabaseUsername=root -e typo3DatabaseHost=mariadb-func-${SUFFIX} -e typo3DatabasePassword=funcp" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + mysql) + echo "Using driver: ${DATABASE_DRIVER}" + ${CONTAINER_BIN} run --name mysql-func-${SUFFIX} --network ${NETWORK} -d -e MYSQL_ROOT_PASSWORD=funcp --tmpfs /var/lib/mysql/:rw,noexec,nosuid ${IMAGE_MYSQL} >/dev/null + waitFor mysql-func-${SUFFIX} 3306 + CONTAINERPARAMS="-e typo3DatabaseDriver=${DATABASE_DRIVER} -e typo3DatabaseName=func_test -e typo3DatabaseUsername=root -e typo3DatabaseHost=mysql-func-${SUFFIX} -e typo3DatabasePassword=funcp" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + postgres) + ${CONTAINER_BIN} run --name postgres-func-${SUFFIX} --network ${NETWORK} -d -e POSTGRES_PASSWORD=funcp -e POSTGRES_USER=funcu --tmpfs /var/lib/postgresql/data:rw,noexec,nosuid ${IMAGE_POSTGRES} >/dev/null + waitFor postgres-func-${SUFFIX} 5432 + CONTAINERPARAMS="-e typo3DatabaseDriver=pdo_pgsql -e typo3DatabaseName=bamboo -e typo3DatabaseUsername=funcu -e typo3DatabaseHost=postgres-func-${SUFFIX} -e typo3DatabasePassword=funcp" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + sqlite) + # create sqlite tmpfs mount typo3temp/var/tests/functional-sqlite-dbs/ to avoid permission issues + mkdir -p "${ROOT_DIR}/.Build/Web/typo3temp/var/tests/functional-sqlite-dbs/" + CONTAINERPARAMS="-e typo3DatabaseDriver=pdo_sqlite --tmpfs ${ROOT_DIR}/.Build/Web/typo3temp/var/tests/functional-sqlite-dbs/:rw,noexec,nosuid" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + esac + ;; + lintPhp) + COMMAND="find . -name \\*.php ! -path "./.Build/\\*" ! -path "./.cache/\\*" -print0 | xargs -0 -n1 -P4 php -dxdebug.mode=off -l >/dev/null" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name lint-php-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + lintTypoScript) + COMMAND="php -dxdebug.mode=off .Build/bin/typoscript-lint --ansi --config=./Build/typoscript-lint/typoscript-lint.yml" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name lint-php-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + renderDocumentation) + ${CONTAINER_BIN} run ${CONTAINER_INTERACTIVE} --pull always -v ${ROOT_DIR}:/project -it ghcr.io/typo3-documentation/render-guides:latest --config=Documentation + SUITE_EXIT_CODE=$? + ;; + phpstan) + PHPSTAN_CONFIG_FILE="Build/phpstan/phpstan.neon" + COMMAND=(php -dxdebug.mode=off .Build/bin/phpstan analyse -c ${PHPSTAN_CONFIG_FILE} --no-progress --no-interaction --memory-limit 4G "$@") + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name phpstan-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + phpstanGenerateBaseline) + PHPSTAN_CONFIG_FILE="Build/phpstan/phpstan.neon" + COMMAND=(php -dxdebug.mode=off .Build/bin/phpstan analyse -c ${PHPSTAN_CONFIG_FILE} --no-progress --no-interaction --memory-limit 4G --allow-empty-baseline --generate-baseline=Build/phpstan/phpstan-baseline.neon) + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name phpstan-baseline-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + unit) + PHPUNIT_CONFIG_FILE="Build/phpunit/UnitTests.xml" + COMMAND=(.Build/bin/phpunit -c ${PHPUNIT_CONFIG_FILE} "$@") + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name unit-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + unitRandom) + PHPUNIT_CONFIG_FILE="Build/phpunit/UnitTests.xml" + COMMAND=(.Build/bin/phpunit -c ${PHPUNIT_CONFIG_FILE} --order-by=random ${PHPUNIT_RANDOM} "$@") + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name unit-random-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + update) + # pull typo3/core-testing-* versions of those ones that exist locally + echo "> pull ghcr.io/typo3/core-testing-* versions of those ones that exist locally" + ${CONTAINER_BIN} images "ghcr.io/typo3/core-testing-*" --format "{{.Repository}}:{{.Tag}}" | xargs -I {} ${CONTAINER_BIN} pull {} + echo "" + # remove "dangling" typo3/core-testing-* images (those tagged as ) + echo "> remove \"dangling\" ghcr.io/typo3/core-testing-* images (those tagged as )" + ${CONTAINER_BIN} images --filter "reference=ghcr.io/typo3/core-testing-*" --filter "dangling=true" --format "{{.ID}}" | xargs -I {} ${CONTAINER_BIN} rmi -f {} + echo "" + ;; + *) + loadHelp + echo "Invalid -s option argument ${TEST_SUITE}" >&2 + echo >&2 + echo "${HELP}" >&2 + exit 1 + ;; +esac + +cleanUp + +# Print summary +echo "" >&2 +echo "###########################################################################" >&2 +echo "Result of ${TEST_SUITE}" >&2 +echo "Container runtime: ${CONTAINER_BIN}" >&2 +echo "Container suffix: ${SUFFIX}" +if [[ ${IS_CORE_CI} -eq 1 ]]; then + echo "Environment: CI" >&2 +else + echo "Environment: local" >&2 +fi +echo "PHP: ${PHP_VERSION}" >&2 +echo "TYPO3: ${CORE_VERSION}" >&2 +if [[ ${TEST_SUITE} =~ ^(functional|functionalDeprecated|acceptance|acceptanceInstall)$ ]]; then + case "${DBMS}" in + mariadb|mysql|postgres) + echo "DBMS: ${DBMS} version ${DBMS_VERSION} driver ${DATABASE_DRIVER}" >&2 + ;; + sqlite) + echo "DBMS: ${DBMS}" >&2 + ;; + esac +fi +if [[ ${SUITE_EXIT_CODE} -eq 0 ]]; then + echo "SUCCESS" >&2 +else + echo "FAILURE" >&2 +fi +echo "###########################################################################" >&2 +echo "" >&2 + +# Exit with code of test suite - This script return non-zero if the executed test failed. +exit $SUITE_EXIT_CODE diff --git a/Build/Scripts/testMethodPrefixChecker.php b/Build/Scripts/testMethodPrefixChecker.php new file mode 100755 index 0000000..e1f1886 --- /dev/null +++ b/Build/Scripts/testMethodPrefixChecker.php @@ -0,0 +1,87 @@ +name->name, 'test')) { + $this->matches[$node->getLine()] = $node->name->name; + } + } +} + +$parser = (new ParserFactory())->createForVersion(\PhpParser\PhpVersion::getHostVersion()); + +$finder = new Symfony\Component\Finder\Finder(); +$finder->files() + ->in([ + __DIR__ . '/../../Tests/Unit/', + __DIR__ . '/../../Tests/Functional/', + ]) + ->name('/Test\.php$/'); + +$output = new ConsoleOutput(); + +$errors = []; +foreach ($finder as $file) { + try { + $ast = $parser->parse($file->getContents()); + } catch (Error $error) { + $output->writeln('Parse error: ' . $error->getMessage() . ''); + exit(1); + } + + $visitor = new NodeVisitor(); + + $traverser = new NodeTraverser(); + $traverser->addVisitor($visitor); + + $ast = $traverser->traverse($ast); + + if (!empty($visitor->matches)) { + $errors[$file->getRealPath()] = $visitor->matches; + $output->write('F'); + } else { + $output->write('.'); + } +} + +$output->writeln(''); + +if (!empty($errors)) { + $output->writeln(''); + + foreach ($errors as $file => $matchesPerLine) { + $output->writeln(''); + $output->writeln('At least on method starts with "test" in ' . $file . ''); + + /** + * @var array $matchesPerLine + * @var int $line + * @var array $matches + */ + foreach ($matchesPerLine as $line => $methodName) { + $output->writeln('Method:' . $methodName . ' Line:' . $line); + } + } + exit(1); +} + +exit(0); diff --git a/Build/Scripts/validateRstFiles.php b/Build/Scripts/validateRstFiles.php new file mode 100755 index 0000000..975c340 --- /dev/null +++ b/Build/Scripts/validateRstFiles.php @@ -0,0 +1,267 @@ +#!/usr/bin/env php + + * + * By default, the standard path is used. You can override this for + * testing purposes. + */ +class validateRstFiles +{ + /** + * @var array + */ + protected $messages; + + protected array $linkTargets = []; + + /** + * @var bool + */ + protected $isError; + + /** + * @var string + */ + protected $baseDir = 'Documentation'; + + public function __construct(string $dir = '') + { + if ($dir) { + $this->baseDir = $dir; + } + } + + public function validate() + { + printf('Searching for rst snippets in ' . $this->baseDir . chr(10)); + + $count = 0; + $finder = $this->findFiles(); + foreach ($finder as $file) { + $filename = (string)$file; + $this->clearMessages(); + $fileContent = $file->getContents(); + $this->validateContent($fileContent); + $a = explode(chr(10), trim($fileContent)); + $lastLine = array_pop($a); + $this->validateLastLine($lastLine); + $this->validateLastLineByFilename($filename, $lastLine); + + if ($this->isError) { + $shortPath = substr($filename, strlen($this->baseDir)); + $shortPath = ltrim($shortPath, '/\\'); + $count++; + printf( + '%-10s | %-12s | %-17s | %s ' . chr(10), + $this->messages['include']['title'], + $this->messages['reference']['title'], + $this->messages['index']['title'], + $shortPath + ); + foreach ($this->messages as $message) { + if ($message['message']) { + printf($message['message'] . chr(10)); + } + } + } + } + + if ($count > 0) { + fwrite(STDERR, 'Found ' . $count . ' rst files with errors, check full log for details.' . chr(10)); + exit(1); + } + exit(0); + } + + public function findFiles(): Finder + { + $finder = new Finder(); + $finder + ->files() + ->in($this->baseDir) + ->name('/\.rst$/') + ->notName('Index.rst') + ->notName('Howto.rst'); + + return $finder; + } + + protected function clearMessages() + { + $this->messages = [ + 'include' => [ + 'title' => '', + 'message' => '', + ], + 'reference' => [ + 'title' => '', + 'message' => '', + ], + 'index' => [ + 'title' => '', + 'message' => '', + ], + ]; + + $this->isError = false; + } + + protected function validateContent(string $fileContent) + { + $checkFor = [ + [ + 'type' => 'include', + 'regex' => '#^\\.\\. include:: /Includes.rst.txt#m', + 'title' => 'no include', + 'message' => 'insert \'.. include:: /Includes.rst.txt\' in first line of the file', + ], + [ + 'type' => 'title', + 'regex' => '#\={2,}\n.*\n\={2,}#m', + 'title' => 'no title', + 'message' => 'Each document must have a title with multiple === above and below', + ], + [ + 'type' => 'titleinvalid', + 'regex' => '#(\={2,}\n)(Deprecation|Feature|Breaking|Important)(\:\s+\#)([0-9]{4,8})(=?.*\n\={2,})#m', + 'title' => 'invalid title format', + 'message' => 'A changelog entry title must have the following format: ' + . '\'(Breaking|Feature|Deprecation|Important) #: \'', + ], + [ + 'type' => 'titleformat', + 'regex' => '#^See :issue:`[0-9]{4,6}`#m', + 'title' => 'no reference', + 'message' => 'insert \'See :issue:`<issuenumber>`\' after headline', + ], + ]; + + foreach ($checkFor as $values) { + if (preg_match($values['regex'], $fileContent) !== 1) { + $this->setError($values); + } + } + $this->validateLinkTarget($fileContent); + } + + private function setError(array $config) + { + $this->messages[$config['type']]['title'] = $config['title']; + $this->messages[$config['type']]['message'] = $config['message']; + $this->isError = true; + } + + private function validateLinkTarget(string $fileContent) + { + $linkTargetConfig = [ + 'type' => 'linktarget', + 'regex' => '#(\.\.\s+\_)([a-zA-Z0-9-_]*)(\:\s*)(\={2,}\n.*\n\={2,})#m', + 'title' => 'no link target', + 'message' => 'Each document must have a unique link target right before the main headline. ' + . ' \'.. _deprecation-issuenumber:\' or \'.. _feature-issuenumber-currenttimestamp:\' are good choices.', + ]; + $result = preg_match($linkTargetConfig['regex'], $fileContent, $matches); + if ($result === 1 && count($matches) > 2) { + $linkTarget = $matches[2]; + if (in_array($linkTarget, $this->linkTargets)) { + $this->setError([ + 'type' => 'linktarget', + 'title' => 'linktarget', + 'message' => 'Link target _' . $linkTarget . ': is not unique. ' + . 'Try adding a timestamp for uniqueness. i.e. _' . $linkTarget . '-' . time() . ':', + ]); + } else { + $this->linkTargets[] = $linkTarget; + } + } else { + $this->setError($linkTargetConfig); + } + } + + protected function validateLastLine(string $line) + { + $checkFor = [ + [ + 'type' => 'index', + 'regex' => '#^\.\. index:: (?:(?:TypoScript|TSConfig|TCA|FlexForm|LocalConfiguration|Fluid|FAL|Database|JavaScript|PHP-API|Frontend|Backend|CLI|RTE|YAML|ext:[a-zA-Z_0-9]+)(?:,\\s|$))+#', + 'title' => 'no or wrong index', + 'message' => 'insert \'.. index:: <at least one valid keyword>\' at the last line of the file. See Build/Scripts/validateRstFiles.php for allowed keywords', + ], + ]; + + foreach ($checkFor as $values) { + if (preg_match($values['regex'], $line) !== 1) { + $this->messages[$values['type']]['title'] = $values['title']; + $this->messages[$values['type']]['message'] = $values['message']; + $this->isError = true; + } + } + } + + protected function validateLastLineByFilename(string $path, string $lastLine) + { + $checkFor = [ + [ + 'type' => 'index', + 'regexIgnoreFilename' => '#' + . 'Changelog[\\\\/]' // Ignore all Changelog files + . '(?:' // which are either + . '.+[\\\\/](?:Feature|Important)' // from any version but of type "Feature" or "Important" + . '|' // or + . '[78]' // from 7.x and 8.x (as there was no extension scanner back then) + . ')' + . '#', + 'regex' => '#^\.\. index:: .*[, ](?:Fully|Partially|Not)Scanned([, ]|$).*#', + 'title' => 'missing FullyScanned / PartiallyScanned / NotScanned tag', + 'message' => 'insert \'.. index:: <at least one valid keyword and either FullyScanned, PartiallyScanned or NotScanned>\' at the last line of the file. See Build/Scripts/validateRstFiles.php for allowed keywords', + ], + ]; + + foreach ($checkFor as $values) { + if (preg_match($values['regexIgnoreFilename'], $path) === 1) { + continue; + } + if (preg_match($values['regex'], $lastLine) !== 1) { + $this->messages[$values['type']]['title'] = $values['title']; + $this->messages[$values['type']]['message'] = $values['message']; + $this->isError = true; + } + } + } +} + +$dir = ''; +$args = getopt('d:'); +if (isset($args['d'])) { + $dir = $args['d']; +} +$validator = new validateRstFiles($dir); +$validator->validate(); diff --git a/Build/php-cs-fixer/config.php b/Build/php-cs-fixer/config.php new file mode 100644 index 0000000..e89ab10 --- /dev/null +++ b/Build/php-cs-fixer/config.php @@ -0,0 +1,120 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +/** + * This file represents the configuration for Code Sniffing PER-related + * automatic checks of coding guidelines. + * + * Run it using runTests.sh, see 'runTests.sh -h' for more options. + * + * Fix entire core: + * > Build/Scripts/runTests.sh -s cgl + * + * Fix your current patch: + * > Build/Scripts/runTests.sh -s cglGit + */ +if (PHP_SAPI !== 'cli') { + die('This script supports command line usage only. Please check your command.'); +} + +// Return a Code Sniffing configuration using +// all sniffers needed for PER +// and additionally: +// - Remove leading slashes in use clauses. +// - PHP single-line arrays should not have trailing comma. +// - Single-line whitespace before closing semicolon are prohibited. +// - Remove unused use statements in the PHP source code +// - Ensure Concatenation to have at least one whitespace around +// - Remove trailing whitespace at the end of blank lines. +return (new \PhpCsFixer\Config()) + ->setParallelConfig(\PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) + ->setFinder( + (new PhpCsFixer\Finder()) + ->ignoreVCSIgnored(true) + ->in([ + __DIR__ . '/../../Classes', + __DIR__ . '/../../Configuration', + __DIR__ . '/../../Tests', + ]) + ) + ->setRiskyAllowed(true) + ->setRules([ + '@DoctrineAnnotation' => true, + // @todo: Switch to @PER-CS2.0 once php-cs-fixer's todo list is done: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7247 + '@PER-CS1.0' => true, + 'array_indentation' => true, + 'array_syntax' => ['syntax' => 'short'], + 'cast_spaces' => ['space' => 'none'], + // @todo: Can be dropped once we enable @PER-CS2.0 + 'concat_space' => ['spacing' => 'one'], + 'declare_equal_normalize' => ['space' => 'none'], + 'declare_parentheses' => true, + 'declare_strict_types' => true, + 'dir_constant' => true, + // @todo: Can be dropped once we enable @PER-CS2.0 + 'function_declaration' => [ + 'closure_fn_spacing' => 'none', + ], + 'function_to_constant' => ['functions' => ['get_called_class', 'get_class', 'get_class_this', 'php_sapi_name', 'phpversion', 'pi']], + 'type_declaration_spaces' => true, + 'global_namespace_import' => ['import_classes' => false, 'import_constants' => false, 'import_functions' => false], + 'list_syntax' => ['syntax' => 'short'], + // @todo: Can be dropped once we enable @PER-CS2.0 + 'method_argument_space' => true, + 'modernize_strpos' => true, + 'modernize_types_casting' => true, + 'native_function_casing' => true, + 'no_alias_functions' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_extra_blank_lines' => true, + 'no_leading_namespace_whitespace' => true, + 'no_null_property_initialization' => true, + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_superfluous_elseif' => true, + 'no_trailing_comma_in_singleline' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_nullsafe_operator' => true, + 'nullable_type_declaration' => [ + 'syntax' => 'question_mark', + ], + 'nullable_type_declaration_for_default_null_value' => true, + 'ordered_imports' => ['imports_order' => ['class', 'function', 'const'], 'sort_algorithm' => 'alpha'], + 'php_unit_construct' => ['assertions' => ['assertEquals', 'assertSame', 'assertNotEquals', 'assertNotSame']], + 'php_unit_mock_short_will_return' => true, + 'php_unit_test_case_static_method_calls' => ['call_type' => 'self'], + 'phpdoc_no_access' => true, + 'phpdoc_no_empty_return' => true, + 'phpdoc_no_package' => true, + 'phpdoc_scalar' => true, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], + 'return_type_declaration' => ['space_before' => 'none'], + 'single_quote' => true, + 'single_space_around_construct' => true, + 'single_line_comment_style' => ['comment_types' => ['hash']], + // @todo: Can be dropped once we enable @PER-CS2.0 + 'single_line_empty_body' => true, + 'trailing_comma_in_multiline' => ['elements' => ['arrays']], + 'whitespace_after_comma_in_array' => ['ensure_single_space' => true], + 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false], + ]); diff --git a/Build/phpstan/phpstan-baseline.neon b/Build/phpstan/phpstan-baseline.neon new file mode 100644 index 0000000..6ac1669 --- /dev/null +++ b/Build/phpstan/phpstan-baseline.neon @@ -0,0 +1,16 @@ +parameters: + ignoreErrors: + - + message: "#^Parameter \\#1 \\$formSettings of method Lavitto\\\\FormToDatabase\\\\Controller\\\\FormResultsController\\:\\:getAvailableFormDefinitions\\(\\) expects array\\{persistenceManager\\: array\\{allowedFileMounts\\: array\\<string\\>\\}\\}, array given\\.$#" + count: 2 + path: ../../Classes/Controller/FormResultsController.php + + - + message: "#^Parameter \\#1 \\$targetFolder of method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\AbstractFile\\:\\:moveTo\\(\\) expects TYPO3\\\\CMS\\\\Core\\\\Resource\\\\Folder, TYPO3\\\\CMS\\\\Core\\\\Resource\\\\FolderInterface given\\.$#" + count: 1 + path: ../../Classes/Controller/FormResultsController.php + + - + message: "#^Parameter \\#1 \\$targetFolder of method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\AbstractFile\\:\\:copyTo\\(\\) expects TYPO3\\\\CMS\\\\Core\\\\Resource\\\\Folder, TYPO3\\\\CMS\\\\Core\\\\Resource\\\\FolderInterface given\\.$#" + count: 1 + path: ../../Classes/Hooks/FormHooks.php diff --git a/Build/phpstan/phpstan.neon b/Build/phpstan/phpstan.neon new file mode 100644 index 0000000..de1cd19 --- /dev/null +++ b/Build/phpstan/phpstan.neon @@ -0,0 +1,17 @@ +includes: + - ../../.Build/vendor/saschaegerer/phpstan-typo3/extension.neon + - phpstan-baseline.neon + +parameters: + # Use local .cache dir instead of /tmp + tmpDir: ../../.cache/phpstan + ignoreErrors: + - '#Variable \$_EXTKEY might not be defined\.#' + level: 8 + + paths: + - ../../. + + excludePaths: + - ../../.Build/* + - ../../Build/* diff --git a/Build/phpunit/FunctionalTests.xml b/Build/phpunit/FunctionalTests.xml new file mode 100644 index 0000000..336bc03 --- /dev/null +++ b/Build/phpunit/FunctionalTests.xml @@ -0,0 +1,60 @@ +<?xml version="1.0"?> +<!-- + Boilerplate for a functional test suite setup. + + This file is loosely maintained within TYPO3 testing-framework, extensions + are encouraged to not use it directly, but to copy it to an own place, + for instance Build/FunctionalTests.xml. + Note FunctionalTestsBootstrap.php should be copied along the way. + + Functional tests should extend \TYPO3\TestingFramework\Core\Tests\FunctionalTestCase, + take a look at this class for further documentation on how to run the suite. + + TYPO3 CMS functional test suite also needs phpunit bootstrap code, the + file is located next to this .xml as FunctionalTestsBootstrap.php + + phpunit v9 compatible version, use -10.xml file for phpunit 10. +--> +<phpunit + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.4/phpunit.xsd" + backupGlobals="true" + beStrictAboutTestsThatDoNotTestAnything="false" + bootstrap="FunctionalTestsBootstrap.php" + cacheDirectory=".phpunit.cache" + cacheResult="false" + colors="true" + displayDetailsOnTestsThatTriggerDeprecations="true" + displayDetailsOnTestsThatTriggerErrors="true" + displayDetailsOnTestsThatTriggerNotices="true" + displayDetailsOnTestsThatTriggerWarnings="true" + failOnDeprecation="true" + failOnNotice="true" + failOnRisky="true" + failOnWarning="true" + requireCoverageMetadata="false" +> + <testsuites> + <testsuite name="Functional tests"> + <!-- + This path either needs an adaption in extensions, or an extension's + test location path needs to be given to phpunit. + --> + <directory>../../Tests/Functional/</directory> + </testsuite> + </testsuites> + <php> + <!-- @deprecated: will be removed with next major version, constant TYPO3_MODE is deprecated --> + <const name="TYPO3_MODE" value="BE"/> + <!-- + @deprecated: Set this to not suppress warnings, notices and deprecations in functional tests + with TYPO3 core v11 and up. + Will always be done with next major version. + To still suppress warnings, notices and deprecations, do NOT define the constant at all. + --> + <const name="TYPO3_TESTING_FUNCTIONAL_REMOVE_ERROR_HANDLER" + value="true"/> + <ini name="display_errors" value="1"/> + <env name="TYPO3_CONTEXT" value="Testing"/> + </php> +</phpunit> diff --git a/Build/phpunit/FunctionalTestsBootstrap.php b/Build/phpunit/FunctionalTestsBootstrap.php new file mode 100644 index 0000000..a95bc52 --- /dev/null +++ b/Build/phpunit/FunctionalTestsBootstrap.php @@ -0,0 +1,30 @@ +<?php +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +/** + * Boilerplate for a functional test phpunit boostrap file. + * + * This file is loosely maintained within TYPO3 testing-framework, extensions + * are encouraged to not use it directly, but to copy it to an own place, + * usually in parallel to a FunctionalTests.xml file. + * + * This file is defined in FunctionalTests.xml and called by phpunit + * before instantiating the test suites. + */ +(static function () { + $testbase = new \TYPO3\TestingFramework\Core\Testbase(); + $testbase->defineOriginalRootPath(); + $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests'); + $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient'); +})(); diff --git a/Build/phpunit/UnitTests.xml b/Build/phpunit/UnitTests.xml new file mode 100644 index 0000000..a0f7f08 --- /dev/null +++ b/Build/phpunit/UnitTests.xml @@ -0,0 +1,49 @@ +<?xml version="1.0"?> +<!-- + Boilerplate for a unit test suite setup. + + This file is loosely maintained within TYPO3 testing-framework, extensions + are encouraged to not use it directly, but to copy it to an own place, + for instance Build/UnitTests.xml. + Note UnitTestsBootstrap.php should be copied along the way. + + Functional tests should extend \TYPO3\TestingFramework\Core\Tests\FunctionalTestCase, + take a look at this class for further documentation on how to run the suite. + + TYPO3 CMS functional test suite also needs phpunit bootstrap code, the + file is located next to this .xml as FunctionalTestsBootstrap.php + + phpunit v9 compatible version, use -10.xml file for phpunit 10. +--> +<phpunit + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.4/phpunit.xsd" + backupGlobals="true" + bootstrap="UnitTestsBootstrap.php" + cacheResult="false" + colors="true" + processIsolation="false" + stopOnError="false" + stopOnFailure="false" + stopOnIncomplete="false" + stopOnSkipped="false" + beStrictAboutTestsThatDoNotTestAnything="false" + failOnWarning="true" + failOnRisky="true" +> + <testsuites> + <testsuite name="Unit tests"> + <!-- + This path either needs an adaption in extensions, or an extension's + test location path needs to be given to phpunit. + --> + <directory suffix="Test.php">../../Tests/Unit/</directory> + </testsuite> + </testsuites> + <php> + <!-- @deprecated: will be removed with next major version, constant TYPO3_MODE is deprecated --> + <const name="TYPO3_MODE" value="BE"/> + <ini name="display_errors" value="1"/> + <env name="TYPO3_CONTEXT" value="Testing"/> + </php> +</phpunit> diff --git a/Build/phpunit/UnitTestsBootstrap.php b/Build/phpunit/UnitTestsBootstrap.php new file mode 100644 index 0000000..0c0b8e7 --- /dev/null +++ b/Build/phpunit/UnitTestsBootstrap.php @@ -0,0 +1,87 @@ +<?php +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +/** + * Boilerplate for a unit test phpunit boostrap file. + * + * This file is loosely maintained within TYPO3 testing-framework, extensions + * are encouraged to not use it directly, but to copy it to an own place, + * usually in parallel to a UnitTests.xml file. + * + * This file is defined in UnitTests.xml and called by phpunit + * before instantiating the test suites. + * + * The recommended way to execute the suite is "runTests.sh". See the + * according script within TYPO3 core's Build/Scripts directory and + * adapt to extensions needs. + */ +(static function () { + $testbase = new \TYPO3\TestingFramework\Core\Testbase(); + + // These if's are for core testing (package typo3/cms) only. cms-composer-installer does + // not create the autoload-include.php file that sets these env vars and sets composer + // mode to true. testing-framework can not be used without composer anyway, so it is safe + // to do this here. This way it does not matter if 'bin/phpunit' or 'vendor/phpunit/phpunit/phpunit' + // is called to run the tests since the 'relative to entry script' path calculation within + // SystemEnvironmentBuilder is not used. However, the binary must be called from the document + // root since getWebRoot() uses 'getcwd()'. + if (!getenv('TYPO3_PATH_ROOT')) { + putenv('TYPO3_PATH_ROOT=' . rtrim($testbase->getWebRoot(), '/')); + } + if (!getenv('TYPO3_PATH_WEB')) { + putenv('TYPO3_PATH_WEB=' . rtrim($testbase->getWebRoot(), '/')); + } + + $testbase->defineSitePath(); + + // We can use the "typo3/cms-composer-installers" constant "TYPO3_COMPOSER_MODE" to determine composer mode. + // This should be always true except for TYPO3 mono repository. + $composerMode = defined('TYPO3_COMPOSER_MODE') && TYPO3_COMPOSER_MODE === true; + $requestType = \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_BE | \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_CLI; + \TYPO3\TestingFramework\Core\SystemEnvironmentBuilder::run(0, $requestType, $composerMode); + + $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3conf/ext'); + $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/assets'); + $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/var/tests'); + $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/var/transient'); + + // Retrieve an instance of class loader and inject to core bootstrap + $classLoader = require $testbase->getPackagesPath() . '/autoload.php'; + \TYPO3\CMS\Core\Core\Bootstrap::initializeClassLoader($classLoader); + + // Initialize default TYPO3_CONF_VARS + $configurationManager = new \TYPO3\CMS\Core\Configuration\ConfigurationManager(); + $GLOBALS['TYPO3_CONF_VARS'] = $configurationManager->getDefaultConfiguration(); + + $cache = new \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend( + 'core', + new \TYPO3\CMS\Core\Cache\Backend\NullBackend('production', []) + ); + + // Set all packages to active + if (interface_exists(\TYPO3\CMS\Core\Package\Cache\PackageCacheInterface::class)) { + $packageManager = \TYPO3\CMS\Core\Core\Bootstrap::createPackageManager(\TYPO3\CMS\Core\Package\UnitTestPackageManager::class, \TYPO3\CMS\Core\Core\Bootstrap::createPackageCache($cache)); + } else { + // v10 compatibility layer + // @deprecated Will be removed when v10 compat is dropped from testing-framework + $packageManager = \TYPO3\CMS\Core\Core\Bootstrap::createPackageManager(\TYPO3\CMS\Core\Package\UnitTestPackageManager::class, $cache); + } + + \TYPO3\CMS\Core\Utility\GeneralUtility::setSingletonInstance(\TYPO3\CMS\Core\Package\PackageManager::class, $packageManager); + \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::setPackageManager($packageManager); + + $testbase->dumpClassLoadingInformation(); + + \TYPO3\CMS\Core\Utility\GeneralUtility::purgeInstances(); +})(); diff --git a/Build/typoscript-lint/typoscript-lint.yml b/Build/typoscript-lint/typoscript-lint.yml new file mode 100644 index 0000000..285bd8e --- /dev/null +++ b/Build/typoscript-lint/typoscript-lint.yml @@ -0,0 +1,15 @@ +paths: + - Configuration + +filePatterns: + - "*.tsconfig" + - "*.typoscript" + - ext_conf_template.txt + - ext_typoscript_*.txt + +sniffs: + - class: Indentation + parameters: + indentPerLevel: 4 + - class: RepeatingRValue + disabled: true From 2cc928e2737daba0d00af1f2ad47ff6ff3d22faf Mon Sep 17 00:00:00 2001 From: mhofmann <m.hofmann@web-vision.de> Date: Fri, 8 Nov 2024 15:58:47 +0100 Subject: [PATCH 2/9] [TASK] Remove old documentation rendering --- docker-compose.yml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index c1b1d5d..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: '3' -services: - t3docmake: - image: t3docs/render-documentation:latest - command: makehtml - volumes: - - ./:/PROJECT:ro - - ./Documentation-GENERATED-temp:/RESULT From 495e07b13c5f66848ac5656dea7441a4b45ad05f Mon Sep 17 00:00:00 2001 From: mhofmann <m.hofmann@web-vision.de> Date: Fri, 8 Nov 2024 16:12:59 +0100 Subject: [PATCH 3/9] [TASK] Remove old code style configuration --- .config/.php-cs-rules.php | 79 ---------------------------------- .config/phpstan.neon | 20 --------- .config/phpunit-functional.xml | 23 ---------- .config/phpunit-unit.xml | 24 ----------- composer.json | 13 ------ 5 files changed, 159 deletions(-) delete mode 100644 .config/.php-cs-rules.php delete mode 100644 .config/phpstan.neon delete mode 100644 .config/phpunit-functional.xml delete mode 100644 .config/phpunit-unit.xml diff --git a/.config/.php-cs-rules.php b/.config/.php-cs-rules.php deleted file mode 100644 index 3f6b6d3..0000000 --- a/.config/.php-cs-rules.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php -/** - * $ php-cs-fixer fix --config .php-cs-rules.php - * - * inside the TYPO3 directory. Warning: This may take up to 10 minutes. - * - * For more information read: - * https://www.php-fig.org/psr/psr-2/ - * https://cs.sensiolabs.org - */ -if (PHP_SAPI !== 'cli') { - die('This script supports command line usage only. Please check your command.'); -} - -$finder = PhpCsFixer\Finder::create() - ->exclude([ - __DIR__ . '/../var', - __DIR__ . '/../vendor', - __DIR__ . '/../bin', - __DIR__ . '/../public', - __DIR__ . '/../config', - ]) - ->in(__DIR__ . '/../'); - -return (new PhpCsFixer\Config()) - ->setRiskyAllowed(true) - ->setRules([ - '@PSR12' => true, - '@PHP74Migration' => true, - '@DoctrineAnnotation' => true, - 'general_phpdoc_annotation_remove' => [ - 'annotations' => [ - 'author' - ] - ], - 'no_leading_import_slash' => true, - 'no_trailing_comma_in_singleline_array' => true, - 'no_singleline_whitespace_before_semicolons' => true, - 'no_unused_imports' => true, - 'concat_space' => ['spacing' => 'one'], - 'no_whitespace_in_blank_line' => true, - 'ordered_imports' => true, - 'single_quote' => true, - 'no_empty_statement' => true, - 'no_extra_blank_lines' => true, - 'phpdoc_no_package' => true, - 'phpdoc_scalar' => true, - 'no_blank_lines_after_phpdoc' => true, - 'array_syntax' => ['syntax' => 'short'], - 'whitespace_after_comma_in_array' => true, - 'function_typehint_space' => true, - 'no_alias_functions' => true, - 'lowercase_cast' => true, - 'no_leading_namespace_whitespace' => true, - 'native_function_casing' => true, - 'no_short_bool_cast' => true, - 'no_unneeded_control_parentheses' => true, - 'phpdoc_no_empty_return' => true, - 'phpdoc_trim' => true, - 'no_superfluous_elseif' => true, - 'no_useless_else' => true, - 'phpdoc_types' => true, - 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], - 'return_type_declaration' => ['space_before' => 'none'], - 'cast_spaces' => ['space' => 'none'], - 'dir_constant' => true, - 'phpdoc_no_access' => true, - 'braces' => ['allow_single_line_closure' => true], - 'compact_nullable_typehint' => true, - 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], - 'modernize_types_casting' => true, - 'new_with_braces' => true, - 'no_empty_phpdoc' => true, - 'no_null_property_initialization' => true, - 'php_unit_mock_short_will_return' => true, - 'php_unit_test_case_static_method_calls' => ['call_type' => 'static'], - 'single_trait_insert_per_statement' => true, - ]) - ->setFinder($finder); diff --git a/.config/phpstan.neon b/.config/phpstan.neon deleted file mode 100644 index b81bbc6..0000000 --- a/.config/phpstan.neon +++ /dev/null @@ -1,20 +0,0 @@ -includes: - - ../.Build/vendor/saschaegerer/phpstan-typo3/extension.neon - -parameters: - level: 8 - paths: - - ../Classes - - ../ext_emconf.php - - ../ext_localconf.php - - ../ext_tables.php - - ../Configuration - excludePaths: - - ../.Build - inferPrivatePropertyTypeFromConstructor: true - ignoreErrors: - - message: '#Variable \$_EXTKEY might not be defined.#' - path: ../*/ext_emconf.php - - reportUnmatchedIgnoredErrors: false - #bootstrapFiles: ~ diff --git a/.config/phpunit-functional.xml b/.config/phpunit-functional.xml deleted file mode 100644 index af186f0..0000000 --- a/.config/phpunit-functional.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0"?> -<phpunit - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" - backupGlobals="true" - bootstrap="../.Build/vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTestsBootstrap.php" - colors="true" - convertErrorsToExceptions="true" - convertWarningsToExceptions="true" - forceCoversAnnotation="false" - stopOnError="false" - stopOnFailure="false" - stopOnIncomplete="false" - stopOnSkipped="false" - verbose="false" - beStrictAboutTestsThatDoNotTestAnything="false" -> - <testsuites> - <testsuite name="extended-routing-functional-tests"> - <directory>../Tests/Functional</directory> - </testsuite> - </testsuites> -</phpunit> diff --git a/.config/phpunit-unit.xml b/.config/phpunit-unit.xml deleted file mode 100644 index 28bbffb..0000000 --- a/.config/phpunit-unit.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0"?> -<phpunit - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" - backupGlobals="true" - bootstrap="../.Build/vendor/typo3/testing-framework/Resources/Core/Build/UnitTestsBootstrap.php" - colors="true" - convertErrorsToExceptions="true" - convertWarningsToExceptions="true" - forceCoversAnnotation="false" - processIsolation="false" - stopOnError="false" - stopOnFailure="false" - stopOnIncomplete="false" - stopOnSkipped="false" - verbose="false" - beStrictAboutTestsThatDoNotTestAnything="true" -> - <testsuites> - <testsuite name="extended-routing-package-unit-tests"> - <directory>../Tests/Unit</directory> - </testsuite> - </testsuites> -</phpunit> diff --git a/composer.json b/composer.json index b6faa04..c0da48e 100755 --- a/composer.json +++ b/composer.json @@ -44,19 +44,6 @@ "Calien\\Xlsexport\\": "Classes/" } }, - "scripts": { - "ec:check": "ec -v -n --no-progress -e'var/log' -e'.Build' -e'.ddev' -e'phpstan-baseline.neon'", - "ec:fix": "ec -v -n --fix -e'var/log' -e'.Build' -e'.ddev'", - "cs:check": "php-cs-fixer fix --config .config/.php-cs-rules.php --ansi --diff --verbose --dry-run", - "cs:fix": "php-cs-fixer fix --config .config/.php-cs-rules.php --ansi", - "analyze:php": "phpstan analyse --ansi --no-progress --memory-limit=768M --configuration=.config/phpstan.neon", - "test:php": [ - "@test:php:unit", - "@test:php:functional" - ], - "test:php:unit": "phpunit --colors=always --configuration .config/phpunit-unit.xml", - "test:php:functional": "@test:php:unit --configuration .config/phpunit-functional.xml" - }, "config": { "allow-plugins": { "typo3/cms-composer-installers": true, From 5ce60e9268e5a2d03ccb783451020b1ca67a10b4 Mon Sep 17 00:00:00 2001 From: mhofmann <m.hofmann@web-vision.de> Date: Fri, 8 Nov 2024 16:13:31 +0100 Subject: [PATCH 4/9] [TASK] Update extension dependencies Commands: ``` composer config bin-dir ".Build/bin" composer config vendor-dir ".Build/vendor" composer config extra.typo3/cms.web-dir ".Build/Web" composer remove --dev typo3/cms-backend --no-update composer remove --dev phpunit/phpunit --no-update composer remove --dev typo3/cms-fluid-styled-content --no-update composer remove --dev typo3/cms-install --no-update composer remove --dev typo3/cms-lowlevel --no-update composer remove --dev typo3/cms-tstemplate --no-update composer remove --dev friendsoftypo3/tt-address --no-update composer require "php:^8.1 || ^8.2 || ^8.3 || ^8.4" --no-update composer require "typo3/cms-core:^12.4 || ^13.4" --no-update composer require "typo3/cms-backend:^12.4 || ^13.4" --no-update composer require "phpoffice/phpspreadsheet:^3.3" --no-update composer require --dev "typo3/testing-framework:^8.0" --no-update ``` --- composer.json | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/composer.json b/composer.json index c0da48e..9b7db0e 100755 --- a/composer.json +++ b/composer.json @@ -16,27 +16,22 @@ } ], "require": { - "php": ">=7.4", - "typo3/cms-core": "^11.5", - "phpoffice/phpspreadsheet": "^1.27", - "ext-pdo": "*" + "php": "^8.1 || ^8.2 || ^8.3 || ^8.4", + "ext-pdo": "*", + "phpoffice/phpspreadsheet": "^3.3", + "typo3/cms-backend": "^12.4 || ^13.4", + "typo3/cms-core": "^12.4 || ^13.4" }, "require-dev": { "armin/editorconfig-cli": "^1.5", "friendsofphp/php-cs-fixer": "^3.0", - "friendsoftypo3/tt-address": "^7", "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^9.5", - "typo3/cms-backend": "^11.5 || ^12.2", - "typo3/cms-fluid-styled-content": "^11.5 || ^12.2", - "typo3/cms-install": "^11.5 || ^12.2", - "typo3/cms-lowlevel": "^11.5 || ^12.2", - "typo3/cms-tstemplate": "^11.5 || ^12.2" + "typo3/testing-framework": "^8.0" }, "extra": { "typo3/cms": { "extension-key": "xlsexport", - "web-dir": "public" + "web-dir": ".Build/Web" } }, "autoload": { @@ -49,8 +44,9 @@ "typo3/cms-composer-installers": true, "typo3/class-alias-loader": true }, - "bin-dir": "bin", + "bin-dir": ".Build/bin", "optimize-autoloader": true, - "sort-packages": true + "sort-packages": true, + "vendor-dir": ".Build/vendor" } } From 6dd2603e85291ab7b5cdc75bae9b85718208c96c Mon Sep 17 00:00:00 2001 From: mhofmann <m.hofmann@web-vision.de> Date: Fri, 8 Nov 2024 16:15:40 +0100 Subject: [PATCH 5/9] [TASK] Install rector Commands: ``` composer require --dev ssch/typo3-rector:^2.11 -W ``` --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 9b7db0e..fc4e0b4 100755 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "armin/editorconfig-cli": "^1.5", "friendsofphp/php-cs-fixer": "^3.0", "phpstan/phpstan": "^1.3", + "ssch/typo3-rector": "^2.11", "typo3/testing-framework": "^8.0" }, "extra": { From 2621a455cefbecf30c071b03ca9502f83b50c66f Mon Sep 17 00:00:00 2001 From: mhofmann <m.hofmann@web-vision.de> Date: Fri, 8 Nov 2024 16:18:19 +0100 Subject: [PATCH 6/9] [TASK] Create rector base config Commands: ``` .Build/bin/typo3-init ``` Changed `PHP_VERSION_CONSTRAINT` and `TYPO3_VERSION_CONSTRAINT` fitting 12 and 13 --- .gitattributes | 1 + rector.php | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 rector.php diff --git a/.gitattributes b/.gitattributes index 56c01db..50024d2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,6 +15,7 @@ .phplint.yml export-ignore .stylelintrc export-ignore docker-compose.yml export-ignore +rector.php export-ignore # Enforce checkout with linux lf consistent over all plattforms *.xml text eol=lf diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..2a169d7 --- /dev/null +++ b/rector.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); + +use Rector\Config\RectorConfig; +use Rector\PostRector\Rector\NameImportingPostRector; +use Rector\TypeDeclaration\Rector\ClassMethod\AddVoidReturnTypeWhereNoReturnRector; +use Rector\ValueObject\PhpVersion; +use Ssch\TYPO3Rector\CodeQuality\General\ConvertImplicitVariablesToExplicitGlobalsRector; +use Ssch\TYPO3Rector\CodeQuality\General\ExtEmConfRector; +use Ssch\TYPO3Rector\Configuration\Typo3Option; +use Ssch\TYPO3Rector\Set\Typo3LevelSetList; +use Ssch\TYPO3Rector\Set\Typo3SetList; + +return RectorConfig::configure() + ->withPaths([ + __DIR__ . '/Build', + __DIR__ . '/Classes', + __DIR__ . '/Configuration', + __DIR__ . '/ext_emconf.php', + __DIR__ . '/ext_localconf.php', + __DIR__ . '/ext_tables.php', + ]) + // uncomment to reach your current PHP version + // ->withPhpSets() + ->withPhpVersion(PhpVersion::PHP_81) + ->withSets([ + Typo3SetList::CODE_QUALITY, + Typo3SetList::GENERAL, + Typo3LevelSetList::UP_TO_TYPO3_12, + ]) + # To have a better analysis from PHPStan, we teach it here some more things + ->withPHPStanConfigs([ + Typo3Option::PHPSTAN_FOR_RECTOR_PATH + ]) + ->withRules([ + AddVoidReturnTypeWhereNoReturnRector::class, + ConvertImplicitVariablesToExplicitGlobalsRector::class, + ]) + ->withConfiguredRule(ExtEmConfRector::class, [ + ExtEmConfRector::PHP_VERSION_CONSTRAINT => '8.1.0-8.4.99', + ExtEmConfRector::TYPO3_VERSION_CONSTRAINT => '12.4.0-13.4.99', + ExtEmConfRector::ADDITIONAL_VALUES_TO_BE_REMOVED => [] + ]) + # If you use withImportNames(), you should consider excluding some TYPO3 files. + ->withSkip([ + // @see https://github.com/sabbelasichon/typo3-rector/issues/2536 + __DIR__ . '/**/Configuration/ExtensionBuilder/*', + NameImportingPostRector::class => [ + 'ext_localconf.php', // This line can be removed since TYPO3 11.4, see https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/11.4/Important-94280-MoveContentsOfExtPhpIntoLocalScopes.html + 'ext_tables.php', // This line can be removed since TYPO3 11.4, see https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/11.4/Important-94280-MoveContentsOfExtPhpIntoLocalScopes.html + 'ClassAliasMap.php', + ] + ]) +; From 034635f828231c1ac74cfbf165ce174615c0adb7 Mon Sep 17 00:00:00 2001 From: mhofmann <m.hofmann@web-vision.de> Date: Fri, 8 Nov 2024 16:19:13 +0100 Subject: [PATCH 7/9] [TASK] Rector run Commands: ``` .Build/bin/rector ``` And increased version in ext_emconf.php --- Build/Scripts/validateRstFiles.php | 6 +++--- Classes/Controller/XlsExportController.php | 6 ++---- .../Export/Event/AlternateCheckQueryEvent.php | 2 +- .../Export/Event/AlternateHeaderLineEvent.php | 4 +--- ext_emconf.php | 4 ++-- ext_tables.php | 17 ----------------- 6 files changed, 9 insertions(+), 30 deletions(-) diff --git a/Build/Scripts/validateRstFiles.php b/Build/Scripts/validateRstFiles.php index 975c340..15dde6f 100755 --- a/Build/Scripts/validateRstFiles.php +++ b/Build/Scripts/validateRstFiles.php @@ -59,7 +59,7 @@ public function __construct(string $dir = '') } } - public function validate() + public function validate(): void { printf('Searching for rst snippets in ' . $this->baseDir . chr(10)); @@ -172,14 +172,14 @@ protected function validateContent(string $fileContent) $this->validateLinkTarget($fileContent); } - private function setError(array $config) + private function setError(array $config): void { $this->messages[$config['type']]['title'] = $config['title']; $this->messages[$config['type']]['message'] = $config['message']; $this->isError = true; } - private function validateLinkTarget(string $fileContent) + private function validateLinkTarget(string $fileContent): void { $linkTargetConfig = [ 'type' => 'linktarget', diff --git a/Classes/Controller/XlsExportController.php b/Classes/Controller/XlsExportController.php index 9a2d4ab..63b6ccb 100644 --- a/Classes/Controller/XlsExportController.php +++ b/Classes/Controller/XlsExportController.php @@ -54,7 +54,7 @@ public function __construct( */ public function indexAction(): ResponseInterface { - $this->pageId = (int)GeneralUtility::_GP('id') ?? 0; + $this->pageId = (int)($this->request->getParsedBody()['id'] ?? $this->request->getQueryParams()['id'] ?? null) ?? 0; $this->view->assign('id', $this->pageId); if ($this->pageId > 0) { $this->loadTSconfig($this->pageId); @@ -104,9 +104,7 @@ public function exportAction(int $id, string $config): ResponseInterface $dbQuery->update($settings['table']) ->where( $dbQuery->expr()->eq('pid', $dbQuery->createNamedParameter($this->pageId, \PDO::PARAM_INT)) - ) - ->set('pid', $dbQuery->createNamedParameter($archive, \PDO::PARAM_INT)) - ->execute(); + )->set('pid', $dbQuery->createNamedParameter($archive, \PDO::PARAM_INT))->executeStatement(); } return (new Response()) diff --git a/Classes/Export/Event/AlternateCheckQueryEvent.php b/Classes/Export/Event/AlternateCheckQueryEvent.php index a7b90cf..fe74a43 100644 --- a/Classes/Export/Event/AlternateCheckQueryEvent.php +++ b/Classes/Export/Event/AlternateCheckQueryEvent.php @@ -48,7 +48,7 @@ public function checkExportConfigExists(string $exportKey): bool return in_array($exportKey, $this->exportKeys); } - public function alternateCheckQuery(string $exportKey, string $check) + public function alternateCheckQuery(string $exportKey, string $check): void { $exportConfig = sprintf('%s.', $exportKey); if ($this->exportConfiguration[$exportConfig]['check'] && !$this->exportConfiguration[$exportConfig]['manipulated']) { diff --git a/Classes/Export/Event/AlternateHeaderLineEvent.php b/Classes/Export/Event/AlternateHeaderLineEvent.php index 8248987..538a573 100644 --- a/Classes/Export/Event/AlternateHeaderLineEvent.php +++ b/Classes/Export/Event/AlternateHeaderLineEvent.php @@ -10,6 +10,4 @@ namespace Calien\Xlsexport\Export\Event; -final class AlternateHeaderLineEvent -{ -} +final class AlternateHeaderLineEvent {} diff --git a/ext_emconf.php b/ext_emconf.php index 392bb15..8068fb0 100755 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -8,10 +8,10 @@ 'author_email' => 'typo3@calien.de', 'author_company' => '', 'state' => 'beta', - 'version' => '3.1.8', + 'version' => '4.0.0', 'constraints' => [ 'depends' => [ - 'typo3' => '11.5.0-11.5.99', + 'typo3' => '12.4.0-13.4.99', ], 'conflicts' => [ ], diff --git a/ext_tables.php b/ext_tables.php index 63d3e17..c8cc980 100755 --- a/ext_tables.php +++ b/ext_tables.php @@ -1,21 +1,4 @@ <?php (static function () { - \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule( - 'Xlsexport', - 'web', - 'xlsexport', - 'bottom', - [ - \Calien\Xlsexport\Controller\XlsExportController::class => 'index,export', - ], - [ - 'access' => 'user,group', - 'name' => 'web_xlsexport', - 'routeTarget' => \Calien\Xlsexport\Controller\XlsExportController::class . '::handleRequest', - 'iconIdentifier' => 'mimetypes-excel', - 'labels' => 'LLL:EXT:xlsexport/Resources/Private/Language/locallang_db.xlf', - 'inheritNavigationComponentFromMainModule' => true, - ] - ); })(); From 1a1a251ef5ac3ccbea1ea0c0590d3aafd00da704 Mon Sep 17 00:00:00 2001 From: mhofmann <m.hofmann@web-vision.de> Date: Fri, 8 Nov 2024 16:20:42 +0100 Subject: [PATCH 8/9] [TASK] Cleanup .gitattributes Removed not needed files from export-ignore --- .gitattributes | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitattributes b/.gitattributes index 50024d2..d82a3de 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,10 +11,6 @@ .editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore -.php-cs-fixer.dist.php export-ignore -.phplint.yml export-ignore -.stylelintrc export-ignore -docker-compose.yml export-ignore rector.php export-ignore # Enforce checkout with linux lf consistent over all plattforms From 72389db1fcf46071d201c55a0cb2a3854b3b8a75 Mon Sep 17 00:00:00 2001 From: mhofmann <m.hofmann@web-vision.de> Date: Sat, 9 Nov 2024 00:30:01 +0100 Subject: [PATCH 9/9] [TASK] Refactor complete controller --- .ddev/config.yaml | 7 +- .github/workflows/testcore12.yml | 143 +++++ .github/workflows/testcore13.yml | 139 +++++ Build/Scripts/runTests.sh | 4 +- Build/Scripts/testMethodPrefixChecker.php | 2 +- Build/phpstan/phpstan-baseline.neon | 62 ++- Build/phpstan/phpstan.neon | 7 +- Classes/Controller/XlsExportController.php | 242 +++------ .../AlternateFirstColumnInSheetEvent.php | 21 + Classes/Event/AlternateHeaderLineEvent.php | 43 ++ Classes/Event/ManipulateRowEntryEvent.php | 47 ++ .../ConfigurationNotFoundException.php | 7 + .../ExportFormatNotDetectedException.php | 7 + .../ExportWithoutConfigurationException.php | 7 + .../ExpressionTypeNotValidException.php | 7 + .../ParameterHasWrongTypeException.php | 7 + .../TypeIsNotAllowedAsQuoteException.php | 7 + .../Export/Event/AddColumnsToSheetEvent.php | 73 --- .../Export/Event/AlternateCheckQueryEvent.php | 75 --- .../Event/AlternateExportQueryEvent.php | 62 --- .../Export/Event/AlternateHeaderLineEvent.php | 13 - .../Export/Event/ManipulateCellDataEvent.php | 50 -- .../Service/DatabaseQueryTypoScriptParser.php | 509 ++++++++++++++++++ Classes/Service/SpreadsheetWriteService.php | 91 ++++ Classes/Traits/ExportTrait.php | 164 ------ Classes/Traits/ExportWithTsSettingsTrait.php | 133 ----- Configuration/Backend/Modules.php | 26 + Configuration/Services.yaml | 3 - Configuration/TCA/Overrides/pages.php | 14 - .../TSconfig/Page/exampleExport.tsconfig | 108 ++-- Configuration/page.tsconfig | 46 ++ Resources/Private/Layouts/Default.html | 14 - Resources/Private/Partials/.gitkeep | 0 Resources/Private/Partials/List.html | 14 +- .../Private/Templates/XlsExport/Export.html | 16 - .../Private/Templates/XlsExport/Index.html | 21 +- .../DatabaseQueryTypoScriptParserTest.php | 174 ++++++ Tests/Unit/DummyTest.php | 17 + composer.json | 9 +- ext_localconf.php | 2 +- ext_tables.php | 2 +- ext_typoscript_setup.typoscript | 13 - rector.php | 55 -- 43 files changed, 1509 insertions(+), 954 deletions(-) create mode 100644 .github/workflows/testcore12.yml create mode 100644 .github/workflows/testcore13.yml create mode 100644 Classes/Event/AlternateFirstColumnInSheetEvent.php create mode 100644 Classes/Event/AlternateHeaderLineEvent.php create mode 100644 Classes/Event/ManipulateRowEntryEvent.php create mode 100644 Classes/Exception/ConfigurationNotFoundException.php create mode 100644 Classes/Exception/ExportFormatNotDetectedException.php create mode 100644 Classes/Exception/ExportWithoutConfigurationException.php create mode 100644 Classes/Exception/ExpressionTypeNotValidException.php create mode 100644 Classes/Exception/ParameterHasWrongTypeException.php create mode 100644 Classes/Exception/TypeIsNotAllowedAsQuoteException.php delete mode 100644 Classes/Export/Event/AddColumnsToSheetEvent.php delete mode 100644 Classes/Export/Event/AlternateCheckQueryEvent.php delete mode 100644 Classes/Export/Event/AlternateExportQueryEvent.php delete mode 100644 Classes/Export/Event/AlternateHeaderLineEvent.php delete mode 100644 Classes/Export/Event/ManipulateCellDataEvent.php create mode 100644 Classes/Service/DatabaseQueryTypoScriptParser.php create mode 100644 Classes/Service/SpreadsheetWriteService.php delete mode 100644 Classes/Traits/ExportTrait.php delete mode 100644 Classes/Traits/ExportWithTsSettingsTrait.php create mode 100644 Configuration/Backend/Modules.php delete mode 100755 Configuration/TCA/Overrides/pages.php create mode 100644 Configuration/page.tsconfig delete mode 100755 Resources/Private/Layouts/Default.html delete mode 100644 Resources/Private/Partials/.gitkeep delete mode 100755 Resources/Private/Templates/XlsExport/Export.html create mode 100644 Tests/Functional/Service/DatabaseQueryTypoScriptParserTest.php create mode 100644 Tests/Unit/DummyTest.php delete mode 100755 ext_typoscript_setup.typoscript delete mode 100644 rector.php diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 38fd594..ebd7f72 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -1,7 +1,7 @@ name: xlsexport type: typo3 -docroot: public -php_version: "8.1" +docroot: .Build/Web +php_version: "8.2" webserver_type: nginx-fpm router_http_port: "80" router_https_port: "443" @@ -11,8 +11,7 @@ additional_fqdns: [] database: type: mariadb version: "10.3" -nfs_mount_enabled: false -mutagen_enabled: false +performance_mode: none use_dns_when_possible: true composer_version: "2" web_environment: diff --git a/.github/workflows/testcore12.yml b/.github/workflows/testcore12.yml new file mode 100644 index 0000000..0545afb --- /dev/null +++ b/.github/workflows/testcore12.yml @@ -0,0 +1,143 @@ +name: tests core 12 + +on: + pull_request: + workflow_dispatch: + +jobs: + code-quality: + name: "code quality with core v12" + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + php-version: [ '8.1'] + permissions: + # actions: read|write|none + actions: none + # checks: read|write|none + checks: none + # contents: read|write|none + contents: read + # deployments: read|write|none + deployments: none + # id-token: read|write|none + id-token: none + # issues: read|write|none + issues: none + # discussions: read|write|none + discussions: none + # packages: read|write|none + packages: read + # pages: read|write|none + pages: none + # pull-requests: read|write|none + pull-requests: none + # repository-projects: read|write|none + repository-projects: read + # security-events: read|write|none + security-events: none + # statuses: read|write|none + statuses: none + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Prepare dependencies for TYPO3 v12" + run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s composer require typo3/cms-core:^12.4 -W" + +# Disabled, as latest installable version of TypoScript linter does not support the TYPO3 backend layout +# override syntax in PageTSConfig files. +# @see https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/12.0/Feature-96812-OverrideBackendTemplatesWithTSconfig.html +# - name: "Run TypoScript lint" +# run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s lintTypoScript" + + - name: "Run PHP lint" + run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s lintPhp" + + - name: "Validate CGL" + run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s cgl" + + - name: "Ensure tests methods do not start with \"test\"" + run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s checkTestMethodsPrefix" + + - name: "Ensure UTF-8 files do not contain BOM" + run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s checkBom" + +# Disabled until documentation is added +# - name: "Test .rst files for integrity" +# run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s checkRst" + + - name: "Find duplicate exception codes" + run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s checkExceptionCodes" + + - name: "Run PHPStan" + run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s phpstan" + + + testsuite: + name: all tests with core v12 + runs-on: ubuntu-22.04 + needs: + - code-quality + strategy: + fail-fast: false + matrix: + php-version: [ '8.1', '8.2', '8.3', '8.4' ] + permissions: + # actions: read|write|none + actions: none + # checks: read|write|none + checks: none + # contents: read|write|none + contents: read + # deployments: read|write|none + deployments: none + # id-token: read|write|none + id-token: none + # issues: read|write|none + issues: none + # discussions: read|write|none + discussions: none + # packages: read|write|none + packages: read + # pages: read|write|none + pages: none + # pull-requests: read|write|none + pull-requests: none + # repository-projects: read|write|none + repository-projects: read + # security-events: read|write|none + security-events: none + # statuses: read|write|none + statuses: none + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Prepare dependencies for TYPO3 v12" + run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s composer require typo3/cms-core:^12.4 -W" + + - name: "Unit" + run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s unit" + + - name: "Functional SQLite" + run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s functional -d sqlite" + + - name: "Functional MariaDB 10.5 mysqli" + run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s functional -d mariadb -a mysqli" + + - name: "Functional MariaDB 10.5 pdo_mysql" + run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s functional -d mariadb -a pdo_mysql" + + - name: "Functional MySQL 8.0 mysqli" + run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s functional -d mariadb -a mysqli" + + - name: "Functional MySQL 8.0 pdo_mysql" + run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s functional -d mariadb -a pdo_mysql" + + - name: "Functional PostgresSQL 10" + # v12 postgres functional disabled with PHP 8.2 since https://github.com/doctrine/dbal/commit/73eec6d882b99e1e2d2d937accca89c1bd91b2d7 + # is not fixed in doctrine core v12 doctrine 2.13.9 + if: ${{ matrix.php <= '8.1' }} + run: "Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php-version }} -s functional -d postgres" diff --git a/.github/workflows/testcore13.yml b/.github/workflows/testcore13.yml new file mode 100644 index 0000000..634bf26 --- /dev/null +++ b/.github/workflows/testcore13.yml @@ -0,0 +1,139 @@ +name: tests core 13 + +on: + pull_request: + workflow_dispatch: + +jobs: + code-quality: + name: "code quality with core v13" + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + php-version: [ '8.2' ] + permissions: + # actions: read|write|none + actions: none + # checks: read|write|none + checks: none + # contents: read|write|none + contents: read + # deployments: read|write|none + deployments: none + # id-token: read|write|none + id-token: none + # issues: read|write|none + issues: none + # discussions: read|write|none + discussions: none + # packages: read|write|none + packages: read + # pages: read|write|none + pages: none + # pull-requests: read|write|none + pull-requests: none + # repository-projects: read|write|none + repository-projects: read + # security-events: read|write|none + security-events: none + # statuses: read|write|none + statuses: none + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Prepare dependencies for TYPO3 v13" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s composer require typo3/cms-core:^13.4 -W" + + # Disabled, as latest installable version of TypoScript linter does not support the TYPO3 backend layout + # override syntax in PageTSConfig files. + # @see https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/13.0/Feature-96813-OverrideBackendTemplatesWithTSconfig.html + # - name: "Run TypoScript lint" + # run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s lintTypoScript" + + - name: "Run PHP lint" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s lintPhp" + + - name: "Validate CGL" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s cgl" + + - name: "Ensure tests methods do not start with \"test\"" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s checkTestMethodsPrefix" + + - name: "Ensure UTF-8 files do not contain BOM" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s checkBom" + +# Deactivated until documentation is written +# - name: "Test .rst files for integrity" +# run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s checkRst" + + - name: "Find duplicate exception codes" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s checkExceptionCodes" + + - name: "Run PHPStan" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s phpstan" + + testsuite: + name: all tests with core v13 + runs-on: ubuntu-22.04 + needs: + - code-quality + strategy: + fail-fast: false + matrix: + php-version: [ '8.2', '8.3', '8.4' ] + permissions: + # actions: read|write|none + actions: none + # checks: read|write|none + checks: none + # contents: read|write|none + contents: read + # deployments: read|write|none + deployments: none + # id-token: read|write|none + id-token: none + # issues: read|write|none + issues: none + # discussions: read|write|none + discussions: none + # packages: read|write|none + packages: read + # pages: read|write|none + pages: none + # pull-requests: read|write|none + pull-requests: none + # repository-projects: read|write|none + repository-projects: read + # security-events: read|write|none + security-events: none + # statuses: read|write|none + statuses: none + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Prepare dependencies for TYPO3 v13" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s composer require typo3/cms-core:^13.4 -W" + + - name: "Unit" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s unit" + + - name: "Functional SQLite" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s functional -d sqlite" + + - name: "Functional MariaDB 10.5 mysqli" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s functional -d mariadb -a mysqli" + + - name: "Functional MariaDB 10.5 pdo_mysql" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s functional -d mariadb -a pdo_mysql" + + - name: "Functional MySQL 8.0 mysqli" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s functional -d mariadb -a mysqli" + + - name: "Functional MySQL 8.0 pdo_mysql" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s functional -d mariadb -a pdo_mysql" + + - name: "Functional PostgresSQL 10" + run: "Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php-version }} -s functional -d postgres" diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh index 12a03f0..6d3a6b7 100755 --- a/Build/Scripts/runTests.sh +++ b/Build/Scripts/runTests.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # -# lavitto/typo3-form-to-database test runner based on docker/podman. +# calien-typo3-xlseport test runner based on docker/podman. # if [ "${CI}" != "true" ]; then trap 'echo "runTests.sh SIGINT signal emitted";cleanUp;exit 2' SIGINT @@ -444,7 +444,7 @@ echo "Architecture" ${ARCH} "requires" ${IMAGE_SELENIUM} "to run acceptance test shift $((OPTIND - 1)) SUFFIX=$(echo $RANDOM) -NETWORK="lavitto-form-to-database-${SUFFIX}" +NETWORK="calien-typo3-xlseport-${SUFFIX}" ${CONTAINER_BIN} network create ${NETWORK} >/dev/null if [ "${CONTAINER_BIN}" == "docker" ]; then diff --git a/Build/Scripts/testMethodPrefixChecker.php b/Build/Scripts/testMethodPrefixChecker.php index e1f1886..b733752 100755 --- a/Build/Scripts/testMethodPrefixChecker.php +++ b/Build/Scripts/testMethodPrefixChecker.php @@ -27,7 +27,7 @@ public function enterNode(Node $node): void } } -$parser = (new ParserFactory())->createForVersion(\PhpParser\PhpVersion::getHostVersion()); +$parser = (new ParserFactory())->createForNewestSupportedVersion(); $finder = new Symfony\Component\Finder\Finder(); $finder->files() diff --git a/Build/phpstan/phpstan-baseline.neon b/Build/phpstan/phpstan-baseline.neon index 6ac1669..09602d8 100644 --- a/Build/phpstan/phpstan-baseline.neon +++ b/Build/phpstan/phpstan-baseline.neon @@ -1,16 +1,66 @@ parameters: ignoreErrors: - - message: "#^Parameter \\#1 \\$formSettings of method Lavitto\\\\FormToDatabase\\\\Controller\\\\FormResultsController\\:\\:getAvailableFormDefinitions\\(\\) expects array\\{persistenceManager\\: array\\{allowedFileMounts\\: array\\<string\\>\\}\\}, array given\\.$#" + message: "#^Expression on left side of \\?\\? is not nullable\\.$#" + count: 1 + path: ../../Classes/Controller/XlsExportController.php + + - + message: "#^Property Calien\\\\Xlsexport\\\\Controller\\\\XlsExportController\\:\\:\\$modTSconfig \\(array\\<non\\-empty\\-string, mixed\\>\\) does not accept array\\<int\\|string, mixed\\>\\.$#" + count: 1 + path: ../../Classes/Controller/XlsExportController.php + + - + message: "#^Instanceof between string and Doctrine\\\\DBAL\\\\ParameterType will always evaluate to false\\.$#" + count: 1 + path: ../../Classes/Service/DatabaseQueryTypoScriptParser.php + + - + message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\<T of object\\>\\|T of object, string given\\.$#" + count: 1 + path: ../../Classes/Service/DatabaseQueryTypoScriptParser.php + + - + message: "#^Parameter \\#2 \\$configuration of method Calien\\\\Xlsexport\\\\Service\\\\DatabaseQueryTypoScriptParser\\:\\:generateValue\\(\\) expects array\\{fieldName\\: string, parameter\\: array\\<float\\|int\\|string\\>\\|float\\|int\\|string, type\\: string, expressionType\\: string, isColumn\\?\\: bool\\}, array\\{fieldName\\: string, parameter\\: array\\<float\\|int\\|string\\>\\|float\\|int\\|string, type\\: 0\\|1\\|2\\|3\\|4\\|5\\|101\\|102\\|117, expressionType\\: string, isColumn\\?\\: bool\\} given\\.$#" + count: 6 + path: ../../Classes/Service/DatabaseQueryTypoScriptParser.php + + - + message: "#^Strict comparison using \\=\\=\\= between '0'\\|bool and '1' will always evaluate to false\\.$#" + count: 1 + path: ../../Classes/Service/DatabaseQueryTypoScriptParser.php + + - + message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Core\\\\Database\\\\Query\\\\QueryBuilder\\:\\:getFrom\\(\\)\\.$#" count: 2 - path: ../../Classes/Controller/FormResultsController.php + path: ../../Tests/Functional/Service/DatabaseQueryTypoScriptParserTest.php + + - + message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Core\\\\Database\\\\Query\\\\QueryBuilder\\:\\:getSelect\\(\\)\\.$#" + count: 4 + path: ../../Tests/Functional/Service/DatabaseQueryTypoScriptParserTest.php + + - + message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Core\\\\Database\\\\Query\\\\QueryBuilder\\:\\:getWhere\\(\\)\\.$#" + count: 1 + path: ../../Tests/Functional/Service/DatabaseQueryTypoScriptParserTest.php + + - + message: "#^Parameter \\#1 \\$configuration of method Calien\\\\Xlsexport\\\\Service\\\\DatabaseQueryTypoScriptParser\\:\\:buildQueryBuilderFromArray\\(\\) expects array\\{table\\: non\\-empty\\-string, alias\\?\\: non\\-empty\\-string, select\\: array\\<non\\-empty\\-string\\>, count\\?\\: non\\-empty\\-string, selectLiteral\\?\\: array\\<non\\-empty\\-string\\>, where\\: array\\<array\\{fieldName\\: string, parameter\\: array\\<float\\|int\\|string\\>\\|float\\|int\\|string, type\\: 0\\|1\\|2\\|3\\|4\\|5\\|101\\|102\\|117, expressionType\\: string, isColumn\\?\\: bool\\}\\>, join\\?\\: array\\<array\\{from\\: non\\-empty\\-string, to\\: non\\-empty\\-string, toAlias\\?\\: non\\-empty\\-string, where\\: array\\<array\\{fieldName\\: string, parameter\\: array\\<float\\|int\\|string\\>\\|float\\|int\\|string, type\\: 0\\|1\\|2\\|3\\|4\\|5\\|101\\|102\\|117, expressionType\\: string, isColumn\\?\\: bool\\}\\>\\}\\>, leftJoin\\?\\: array\\<array\\{from\\: non\\-empty\\-string, to\\: non\\-empty\\-string, toAlias\\?\\: non\\-empty\\-string, where\\: array\\<array\\{fieldName\\: string, parameter\\: array\\<float\\|int\\|string\\>\\|float\\|int\\|string, type\\: 0\\|1\\|2\\|3\\|4\\|5\\|101\\|102\\|117, expressionType\\: string, isColumn\\?\\: bool\\}\\>\\}\\>, \\.\\.\\.\\}, array\\{table\\: 'pages', select\\: array\\{'uid', 'pid', 'title'\\}, where\\: array\\{array\\{fieldName\\: 'pid', parameter\\: 1, type\\: 'Connection\\:\\:PARAM…', expressionType\\: 'eq'\\}\\}, join\\: array\\{array\\{from\\: 'pages', to\\: 'tt_content', where\\: array\\{array\\{fieldName\\: 'pages\\.uid', parameter\\: 'tt_content\\.pid', type\\: 'Connection\\:\\:PARAM…', expressionType\\: 'eq', isColumn\\: true\\}\\}\\}\\}\\} given\\.$#" + count: 1 + path: ../../Tests/Functional/Service/DatabaseQueryTypoScriptParserTest.php + + - + message: "#^Parameter \\#1 \\$configuration of method Calien\\\\Xlsexport\\\\Service\\\\DatabaseQueryTypoScriptParser\\:\\:buildQueryBuilderFromArray\\(\\) expects array\\{table\\: non\\-empty\\-string, alias\\?\\: non\\-empty\\-string, select\\: array\\<non\\-empty\\-string\\>, count\\?\\: non\\-empty\\-string, selectLiteral\\?\\: array\\<non\\-empty\\-string\\>, where\\: array\\<array\\{fieldName\\: string, parameter\\: array\\<float\\|int\\|string\\>\\|float\\|int\\|string, type\\: 0\\|1\\|2\\|3\\|4\\|5\\|101\\|102\\|117, expressionType\\: string, isColumn\\?\\: bool\\}\\>, join\\?\\: array\\<array\\{from\\: non\\-empty\\-string, to\\: non\\-empty\\-string, toAlias\\?\\: non\\-empty\\-string, where\\: array\\<array\\{fieldName\\: string, parameter\\: array\\<float\\|int\\|string\\>\\|float\\|int\\|string, type\\: 0\\|1\\|2\\|3\\|4\\|5\\|101\\|102\\|117, expressionType\\: string, isColumn\\?\\: bool\\}\\>\\}\\>, leftJoin\\?\\: array\\<array\\{from\\: non\\-empty\\-string, to\\: non\\-empty\\-string, toAlias\\?\\: non\\-empty\\-string, where\\: array\\<array\\{fieldName\\: string, parameter\\: array\\<float\\|int\\|string\\>\\|float\\|int\\|string, type\\: 0\\|1\\|2\\|3\\|4\\|5\\|101\\|102\\|117, expressionType\\: string, isColumn\\?\\: bool\\}\\>\\}\\>, \\.\\.\\.\\}, array\\{table\\: 'pages', select\\: array\\{'uid', 'pid', 'title'\\}, where\\: array\\{array\\{fieldName\\: 'pid', parameter\\: 1, type\\: 'Connection\\:\\:PARAM…', expressionType\\: 'eq'\\}\\}\\} given\\.$#" + count: 1 + path: ../../Tests/Functional/Service/DatabaseQueryTypoScriptParserTest.php - - message: "#^Parameter \\#1 \\$targetFolder of method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\AbstractFile\\:\\:moveTo\\(\\) expects TYPO3\\\\CMS\\\\Core\\\\Resource\\\\Folder, TYPO3\\\\CMS\\\\Core\\\\Resource\\\\FolderInterface given\\.$#" + message: "#^Parameter \\#1 \\$configuration of method Calien\\\\Xlsexport\\\\Service\\\\DatabaseQueryTypoScriptParser\\:\\:buildQueryBuilderFromArray\\(\\) expects array\\{table\\: non\\-empty\\-string, alias\\?\\: non\\-empty\\-string, select\\: array\\<non\\-empty\\-string\\>, count\\?\\: non\\-empty\\-string, selectLiteral\\?\\: array\\<non\\-empty\\-string\\>, where\\: array\\<array\\{fieldName\\: string, parameter\\: array\\<float\\|int\\|string\\>\\|float\\|int\\|string, type\\: 0\\|1\\|2\\|3\\|4\\|5\\|101\\|102\\|117, expressionType\\: string, isColumn\\?\\: bool\\}\\>, join\\?\\: array\\<array\\{from\\: non\\-empty\\-string, to\\: non\\-empty\\-string, toAlias\\?\\: non\\-empty\\-string, where\\: array\\<array\\{fieldName\\: string, parameter\\: array\\<float\\|int\\|string\\>\\|float\\|int\\|string, type\\: 0\\|1\\|2\\|3\\|4\\|5\\|101\\|102\\|117, expressionType\\: string, isColumn\\?\\: bool\\}\\>\\}\\>, leftJoin\\?\\: array\\<array\\{from\\: non\\-empty\\-string, to\\: non\\-empty\\-string, toAlias\\?\\: non\\-empty\\-string, where\\: array\\<array\\{fieldName\\: string, parameter\\: array\\<float\\|int\\|string\\>\\|float\\|int\\|string, type\\: 0\\|1\\|2\\|3\\|4\\|5\\|101\\|102\\|117, expressionType\\: string, isColumn\\?\\: bool\\}\\>\\}\\>, \\.\\.\\.\\}, array\\{table\\: 'pages', select\\: array\\{'uid', 'pid', 'title'\\}, where\\: array\\{array\\{fieldName\\: 'pid', parameter\\: 1, type\\: 'Connection\\:\\:PARAM…', expressionType\\: 'not\\-allowed…'\\}\\}\\} given\\.$#" count: 1 - path: ../../Classes/Controller/FormResultsController.php + path: ../../Tests/Functional/Service/DatabaseQueryTypoScriptParserTest.php - - message: "#^Parameter \\#1 \\$targetFolder of method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\AbstractFile\\:\\:copyTo\\(\\) expects TYPO3\\\\CMS\\\\Core\\\\Resource\\\\Folder, TYPO3\\\\CMS\\\\Core\\\\Resource\\\\FolderInterface given\\.$#" + message: "#^Parameter \\#1 \\$configuration of method Calien\\\\Xlsexport\\\\Service\\\\DatabaseQueryTypoScriptParser\\:\\:buildQueryBuilderFromArray\\(\\) expects array\\{table\\: non\\-empty\\-string, alias\\?\\: non\\-empty\\-string, select\\: array\\<non\\-empty\\-string\\>, count\\?\\: non\\-empty\\-string, selectLiteral\\?\\: array\\<non\\-empty\\-string\\>, where\\: array\\<array\\{fieldName\\: string, parameter\\: array\\<float\\|int\\|string\\>\\|float\\|int\\|string, type\\: 0\\|1\\|2\\|3\\|4\\|5\\|101\\|102\\|117, expressionType\\: string, isColumn\\?\\: bool\\}\\>, join\\?\\: array\\<array\\{from\\: non\\-empty\\-string, to\\: non\\-empty\\-string, toAlias\\?\\: non\\-empty\\-string, where\\: array\\<array\\{fieldName\\: string, parameter\\: array\\<float\\|int\\|string\\>\\|float\\|int\\|string, type\\: 0\\|1\\|2\\|3\\|4\\|5\\|101\\|102\\|117, expressionType\\: string, isColumn\\?\\: bool\\}\\>\\}\\>, leftJoin\\?\\: array\\<array\\{from\\: non\\-empty\\-string, to\\: non\\-empty\\-string, toAlias\\?\\: non\\-empty\\-string, where\\: array\\<array\\{fieldName\\: string, parameter\\: array\\<float\\|int\\|string\\>\\|float\\|int\\|string, type\\: 0\\|1\\|2\\|3\\|4\\|5\\|101\\|102\\|117, expressionType\\: string, isColumn\\?\\: bool\\}\\>\\}\\>, \\.\\.\\.\\}, array\\{table\\: 'pages', select\\: array\\{'uid', 'pid', 'title'\\}, where\\: array\\{array\\{fieldName\\: 'pid', parameter\\: array\\{1\\}, type\\: 'Connection\\:\\:PARAM…', expressionType\\: 'in'\\}\\}\\} given\\.$#" count: 1 - path: ../../Classes/Hooks/FormHooks.php + path: ../../Tests/Functional/Service/DatabaseQueryTypoScriptParserTest.php diff --git a/Build/phpstan/phpstan.neon b/Build/phpstan/phpstan.neon index de1cd19..38667e0 100644 --- a/Build/phpstan/phpstan.neon +++ b/Build/phpstan/phpstan.neon @@ -5,12 +5,13 @@ includes: parameters: # Use local .cache dir instead of /tmp tmpDir: ../../.cache/phpstan - ignoreErrors: - - '#Variable \$_EXTKEY might not be defined\.#' +# ignoreErrors: +# - '#Variable \$_EXTKEY might not be defined\.#' level: 8 paths: - - ../../. + - ../../Classes + - ../../Tests excludePaths: - ../../.Build/* diff --git a/Classes/Controller/XlsExportController.php b/Classes/Controller/XlsExportController.php index 63b6ccb..6242703 100644 --- a/Classes/Controller/XlsExportController.php +++ b/Classes/Controller/XlsExportController.php @@ -1,201 +1,109 @@ <?php -/** - * Markus Hofmann - * 12.10.21 21:33 - * churchevent - */ - declare(strict_types=1); namespace Calien\Xlsexport\Controller; -use Calien\Xlsexport\Export\Event\AlternateCheckQueryEvent; -use Calien\Xlsexport\Export\Event\AlternateExportQueryEvent; -use Calien\Xlsexport\Traits\ExportWithTsSettingsTrait; -use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Driver\Exception; +use Calien\Xlsexport\Exception\ConfigurationNotFoundException; +use Calien\Xlsexport\Exception\ExportWithoutConfigurationException; +use Calien\Xlsexport\Service\DatabaseQueryTypoScriptParser; +use Calien\Xlsexport\Service\SpreadsheetWriteService; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Backend\Attribute\AsController; +use TYPO3\CMS\Backend\Template\ModuleTemplate; use TYPO3\CMS\Backend\Template\ModuleTemplateFactory; -use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Http\Response; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Http\ResponseFactory; +use TYPO3\CMS\Core\TypoScript\TypoScriptService; -class XlsExportController extends ActionController +#[AsController] +final class XlsExportController { - use ExportWithTsSettingsTrait; - - protected ConnectionPool $dbConnection; - - protected ModuleTemplateFactory $moduleTemplateFactory; - - protected int $pageId = 0; /** - * @deprecated will be removed in future versions + * @var array<non-empty-string, mixed> */ - protected array $hooks = []; + protected array $modTSconfig = []; + + private readonly string $moduleName; + protected ModuleTemplate $moduleTemplate; public function __construct( - ConnectionPool $connectionPool, - ModuleTemplateFactory $moduleTemplateFactory + private readonly ModuleTemplateFactory $moduleTemplateFactory, + private readonly TypoScriptService $typoScriptService, + private readonly DatabaseQueryTypoScriptParser $databaseQueryTypoScriptParser, + private readonly SpreadsheetWriteService $spreadsheetWriteService ) { - $this->dbConnection = $connectionPool; - $this->moduleTemplateFactory = $moduleTemplateFactory; - $this->loadHooks(); + $this->moduleName = 'web_xlsexport'; } - /** - * action index - * renders the export view - * - * @throws Exception - * @throws \Doctrine\DBAL\Exception - */ - public function indexAction(): ResponseInterface + public function index(ServerRequestInterface $request): ResponseInterface { - $this->pageId = (int)($this->request->getParsedBody()['id'] ?? $this->request->getQueryParams()['id'] ?? null) ?? 0; - $this->view->assign('id', $this->pageId); - if ($this->pageId > 0) { - $this->loadTSconfig($this->pageId); - - if ( - array_key_exists('exports.', $this->selfSettings) - && is_array($this->selfSettings['exports.']) - ) { - $this->buildDataArrayForListView(); - $this->view->assign('settings', $this->selfSettings); - $this->addAdditionalData(); - } else { - $this->view->assign('noconfig', 1); - } + $this->moduleTemplate = $this->moduleTemplateFactory->create($request); + $pageId = (int)($request->getQueryParams()['id'] ?? null) ?? 0; + $this->loadTSconfig($pageId); + $assignedValues = [ + 'noConfig' => $this->modTSconfig === [], + 'datasets' => [], + 'pageId' => $pageId, + ]; + foreach ($this->modTSconfig as $configName => $configuration) { + $countQuery = $this->databaseQueryTypoScriptParser->buildCountQueryFromArray($configuration); + $this->databaseQueryTypoScriptParser->replacePlaceholderWithCurrentId($countQuery, $pageId); + $assignedValues['datasets'][$configName] = [ + 'label' => $configuration['label'] ?? $configuration['table'], + 'count' => $countQuery->executeQuery()->fetchOne(), + ]; } - return $this->htmlResponse(); + + $this->moduleTemplate->assignMultiple($assignedValues); + + return $this->moduleTemplate->renderResponse('XlsExport/Index'); } /** - * action export - * - * @throws Exception - * @throws \Doctrine\DBAL\Exception - * @throws \PhpOffice\PhpSpreadsheet\Exception - * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception - * @throws DBALException + * @throws ExportWithoutConfigurationException + * @throws ConfigurationNotFoundException */ - public function exportAction(int $id, string $config): ResponseInterface + public function export(ServerRequestInterface $request): ResponseInterface { - $this->pageId = $id; - - $this->loadTSconfig($this->pageId); - - $settings = $this->selfSettings['exports.'][$config . '.']; - - $event = $this->eventDispatcher->dispatch(new AlternateExportQueryEvent($settings, $config)); - - $settings = $event->getManipulatedSettings(); - - $file = $this->doExport($settings, $this->pageId); - - //ins Archiv verschieben - if ($settings['archive']) { - $archive = $settings['archive']; - - $dbQuery = $this->dbConnection->getQueryBuilderForTable($settings['table']); - $dbQuery->update($settings['table']) - ->where( - $dbQuery->expr()->eq('pid', $dbQuery->createNamedParameter($this->pageId, \PDO::PARAM_INT)) - )->set('pid', $dbQuery->createNamedParameter($archive, \PDO::PARAM_INT))->executeStatement(); + $pageId = $request->getQueryParams()['id'] ?? null; + $configuration = $request->getQueryParams()['configuration'] ?? null; + if ($pageId === null || $configuration === null) { + throw new ExportWithoutConfigurationException( + 'For an export you need a valid configuration key', + 1731105142347 + ); + } + $pageId = (int)$pageId; + + $this->loadTSconfig($pageId); + if (!array_key_exists($configuration, $this->modTSconfig)) { + throw new ConfigurationNotFoundException( + 'Configuration not found for export on current page', + 1731105227250 + ); } - return (new Response()) - ->withHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') - ->withHeader( - 'Content-Disposition', - sprintf( - 'attachment;filename="%s_%s_%d.xlsx"', - date('Y-m-d-His'), - $settings['table'], - $this->pageId - ) - ) - ->withHeader('Cache-Control', 'max-age=0') - ->withBody($file); - } + $exportDataQuery = $this->databaseQueryTypoScriptParser->buildQueryBuilderFromArray($this->modTSconfig[$configuration]); + $this->databaseQueryTypoScriptParser->replacePlaceholderWithCurrentId($exportDataQuery, $pageId); - protected function buildDataArrayForListView(): void - { - $datasets = []; - $event = $this->eventDispatcher->dispatch(new AlternateCheckQueryEvent($this->selfSettings['exports.'])); - $this->selfSettings['exports.'] = $event->getManipulatedSettings(); - foreach ($this->selfSettings['exports.'] as $key => $config) { - $keyWithoutDot = str_replace('.', '', $key); - if (strlen($config['check']) > 20) { - $table = $config['table']; - $checkQuery = $config['check']; - - /** @deprecated use PSR-14 event instead, will be removed in future versions */ - if (array_key_exists($table, $this->hooks) && is_array($this->hooks[$keyWithoutDot])) { - foreach ($this->hooks[$keyWithoutDot] as $classObj) { - $hookObj = GeneralUtility::makeInstance($classObj); - if (method_exists($hookObj, 'alternateCheckQuery')) { - trigger_error( - 'Usage of hooks inside XLS export is deprecated and will be removed in future versions. Use PSR-14 Event dispatching instead.', - E_USER_DEPRECATED - ); - $checkQuery = $hookObj->alternateCheckQuery($checkQuery, $this); - } - } - } - - $statement = sprintf($checkQuery, $this->pageId); - $dbQuery = $this->dbConnection->getQueryBuilderForTable($table)->getConnection(); - $result = $dbQuery->executeQuery($statement)->fetchAllAssociative(); - - // if all datasets from this page should be exported - if (count($result) == 1) { - $count = $result[0]; - $datasets[$keyWithoutDot]['count'] = $count['count(uid)'] ?? $count['count(*)']; - } else { - foreach ($result as $row) { - $datasets[$keyWithoutDot]['options'][end($row)]['count'] = $row['count(*)']; - } - } - - $datasets[$keyWithoutDot]['label'] = $config['label'] ?: $table; - $datasets[$keyWithoutDot]['config'] = $keyWithoutDot; - } - } - $this->view->assign('datasets', $datasets); - } + $result = $exportDataQuery->executeQuery(); - protected function addAdditionalData(): void - { - $additionalData = []; - if (array_key_exists('additionalData', $this->hooks)) { - foreach ($this->hooks['additionalData'] as $classObj) { - $hookObj = GeneralUtility::makeInstance($classObj); - if (method_exists($hookObj, 'addAdditionalData')) { - $hookObj->addAdditionalData($additionalData, $this); - } - } - } - if (count($additionalData) > 0) { - $this->view->assign('additionalData', $additionalData); - } + $spreadsheet = $this->spreadsheetWriteService->generateSpreadsheet($result, $this->modTSconfig[$configuration], $configuration); + + return (new ResponseFactory()) + ->createResponse() + ->withBody($spreadsheet) + ->withHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); } - /** - * @deprecated Will be removed in future version - */ - private function loadHooks(): void + private function loadTSconfig(int $currentId): void { - /** @deprecated Use PSR-14 Events instead */ - if ( - array_key_exists('xlsexport', $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']) - && array_key_exists('alternateQueries', $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['xlsexport']) - && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['xlsexport']['alternateQueries']) - ) { - $this->hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['xlsexport']['alternateQueries']; + $TSconfig = BackendUtility::getPagesTSconfig($currentId); + $moduleConfigArrayName = sprintf('%s.', $this->moduleName); + if (array_key_exists($moduleConfigArrayName, $TSconfig['mod.'])) { + $this->modTSconfig = $this->typoScriptService->convertTypoScriptArrayToPlainArray($TSconfig['mod.'][$moduleConfigArrayName]); } } } diff --git a/Classes/Event/AlternateFirstColumnInSheetEvent.php b/Classes/Event/AlternateFirstColumnInSheetEvent.php new file mode 100644 index 0000000..0c8cbc8 --- /dev/null +++ b/Classes/Event/AlternateFirstColumnInSheetEvent.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +namespace Calien\Xlsexport\Event; + +class AlternateFirstColumnInSheetEvent +{ + private string $firstColumn = 'A'; + + public function getFirstColumn(): string + { + return $this->firstColumn; + } + + public function setFirstColumn(string $firstColumn): void + { + $this->firstColumn = $firstColumn; + } + +} diff --git a/Classes/Event/AlternateHeaderLineEvent.php b/Classes/Event/AlternateHeaderLineEvent.php new file mode 100644 index 0000000..f535656 --- /dev/null +++ b/Classes/Event/AlternateHeaderLineEvent.php @@ -0,0 +1,43 @@ +<?php + +/** + * Markus Hofmann + * 12.10.21 23:17 + * churchevent + */ + +declare(strict_types=1); + +namespace Calien\Xlsexport\Event; + +final class AlternateHeaderLineEvent +{ + /** + * @param string[] $headerFieldLabels + */ + public function __construct( + private array $headerFieldLabels, + private readonly string $configuration + ) {} + + /** + * @return string[] + */ + public function getHeaderFieldLabels(): array + { + return $this->headerFieldLabels; + } + + /** + * @param string[] $headerFieldLabels + */ + public function setHeaderFieldLabels(array $headerFieldLabels): void + { + $this->headerFieldLabels = $headerFieldLabels; + } + + public function getConfiguration(): string + { + return $this->configuration; + } +} diff --git a/Classes/Event/ManipulateRowEntryEvent.php b/Classes/Event/ManipulateRowEntryEvent.php new file mode 100644 index 0000000..4ce1a04 --- /dev/null +++ b/Classes/Event/ManipulateRowEntryEvent.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +namespace Calien\Xlsexport\Event; + +final class ManipulateRowEntryEvent +{ + /** + * @param array<array-key, mixed> $row + * @param string[] $fieldLabels + */ + public function __construct( + private array $row, + private readonly array $fieldLabels, + private readonly string $configurationKey + ) {} + + /** + * @return array<array-key, mixed> + */ + public function getRow(): array + { + return $this->row; + } + + /** + * @param array<array-key, mixed> $row + */ + public function setRow(array $row): void + { + $this->row = $row; + } + + /** + * @return string[] + */ + public function getFieldLabels(): array + { + return $this->fieldLabels; + } + + public function getConfigurationKey(): string + { + return $this->configurationKey; + } +} diff --git a/Classes/Exception/ConfigurationNotFoundException.php b/Classes/Exception/ConfigurationNotFoundException.php new file mode 100644 index 0000000..a4661ad --- /dev/null +++ b/Classes/Exception/ConfigurationNotFoundException.php @@ -0,0 +1,7 @@ +<?php + +declare(strict_types=1); + +namespace Calien\Xlsexport\Exception; + +final class ConfigurationNotFoundException extends \Exception {} diff --git a/Classes/Exception/ExportFormatNotDetectedException.php b/Classes/Exception/ExportFormatNotDetectedException.php new file mode 100644 index 0000000..b13a01b --- /dev/null +++ b/Classes/Exception/ExportFormatNotDetectedException.php @@ -0,0 +1,7 @@ +<?php + +declare(strict_types=1); + +namespace Calien\Xlsexport\Exception; + +final class ExportFormatNotDetectedException extends \Exception {} diff --git a/Classes/Exception/ExportWithoutConfigurationException.php b/Classes/Exception/ExportWithoutConfigurationException.php new file mode 100644 index 0000000..0312a2d --- /dev/null +++ b/Classes/Exception/ExportWithoutConfigurationException.php @@ -0,0 +1,7 @@ +<?php + +declare(strict_types=1); + +namespace Calien\Xlsexport\Exception; + +final class ExportWithoutConfigurationException extends \Exception {} diff --git a/Classes/Exception/ExpressionTypeNotValidException.php b/Classes/Exception/ExpressionTypeNotValidException.php new file mode 100644 index 0000000..ce09a46 --- /dev/null +++ b/Classes/Exception/ExpressionTypeNotValidException.php @@ -0,0 +1,7 @@ +<?php + +declare(strict_types=1); + +namespace Calien\Xlsexport\Exception; + +final class ExpressionTypeNotValidException extends \Exception {} diff --git a/Classes/Exception/ParameterHasWrongTypeException.php b/Classes/Exception/ParameterHasWrongTypeException.php new file mode 100644 index 0000000..2672104 --- /dev/null +++ b/Classes/Exception/ParameterHasWrongTypeException.php @@ -0,0 +1,7 @@ +<?php + +declare(strict_types=1); + +namespace Calien\Xlsexport\Exception; + +final class ParameterHasWrongTypeException extends \Exception {} diff --git a/Classes/Exception/TypeIsNotAllowedAsQuoteException.php b/Classes/Exception/TypeIsNotAllowedAsQuoteException.php new file mode 100644 index 0000000..e029b4c --- /dev/null +++ b/Classes/Exception/TypeIsNotAllowedAsQuoteException.php @@ -0,0 +1,7 @@ +<?php + +declare(strict_types=1); + +namespace Calien\Xlsexport\Exception; + +final class TypeIsNotAllowedAsQuoteException extends \Exception {} diff --git a/Classes/Export/Event/AddColumnsToSheetEvent.php b/Classes/Export/Event/AddColumnsToSheetEvent.php deleted file mode 100644 index f9aff7b..0000000 --- a/Classes/Export/Event/AddColumnsToSheetEvent.php +++ /dev/null @@ -1,73 +0,0 @@ -<?php - -/** - * Markus Hofmann - * 12.10.21 23:02 - * churchevent - */ - -declare(strict_types=1); - -namespace Calien\Xlsexport\Export\Event; - -use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; - -final class AddColumnsToSheetEvent -{ - /** - * @var Worksheet - */ - private Worksheet $sheet; - /** - * @var int - */ - private int $colIndexer; - /** - * @var int - */ - private int $currentRow; - - /** - * @param Worksheet $sheet - * @param int $colIndexer - * @param int $currentRow - */ - public function __construct(Worksheet $sheet, int $colIndexer, int $currentRow) - { - $this->sheet = $sheet; - $this->colIndexer = $colIndexer; - $this->currentRow = $currentRow; - } - - /** - * getSheet - * returns the current worksheet to add new columns - * - * @return Worksheet - */ - public function getSheet(): Worksheet - { - return $this->sheet; - } - - /** - * returns the current colIndexer for the next column to be written - * can be manipulated - * to get next Columns name, call ExportTrait::$cols[$colIndexer] - * - * @return int - */ - public function getColIndexer(): int - { - return $this->colIndexer; - } - - /** - * returns the current row of the sheet, should not be manipulated to avoid overriding in next line - * @return int - */ - public function getCurrentRow(): int - { - return $this->currentRow; - } -} diff --git a/Classes/Export/Event/AlternateCheckQueryEvent.php b/Classes/Export/Event/AlternateCheckQueryEvent.php deleted file mode 100644 index fe74a43..0000000 --- a/Classes/Export/Event/AlternateCheckQueryEvent.php +++ /dev/null @@ -1,75 +0,0 @@ -<?php - -/** - * Markus Hofmann - * 12.10.21 22:05 - * churchevent - */ - -declare(strict_types=1); - -namespace Calien\Xlsexport\Export\Event; - -use Psr\EventDispatcher\StoppableEventInterface; - -final class AlternateCheckQueryEvent implements StoppableEventInterface -{ - /** - * @var array|string[] - */ - protected array $exportKeys = []; - /** - * @var array - */ - protected array $exportConfiguration = []; - - /** - * @param array $settings - */ - public function __construct(array $settings) - { - foreach ($settings as $exportConfigKey => $exportConfig) { - $keyWithoutDot = str_replace('.', '', $exportConfigKey); - $this->exportKeys[] = $keyWithoutDot; - $this->exportConfiguration[$exportConfigKey] = $exportConfig; - } - } - - /** - * checkExportConfigExists - * - * Event listener should call this method to check if access is needed - * - * @param string $exportKey - * @return bool - */ - public function checkExportConfigExists(string $exportKey): bool - { - return in_array($exportKey, $this->exportKeys); - } - - public function alternateCheckQuery(string $exportKey, string $check): void - { - $exportConfig = sprintf('%s.', $exportKey); - if ($this->exportConfiguration[$exportConfig]['check'] && !$this->exportConfiguration[$exportConfig]['manipulated']) { - $this->exportConfiguration[$exportConfig]['check'] = $check; - $this->exportConfiguration[$exportConfig]['manipulated'] = true; - } - } - - public function isPropagationStopped(): bool - { - $allManipulated = true; - foreach ($this->exportConfiguration as $config) { - if (!array_key_exists('manipulated', $config) || !$config['manipulated']) { - $allManipulated = false; - } - } - return $allManipulated; - } - - public function getManipulatedSettings(): array - { - return $this->exportConfiguration; - } -} diff --git a/Classes/Export/Event/AlternateExportQueryEvent.php b/Classes/Export/Event/AlternateExportQueryEvent.php deleted file mode 100644 index b3c37e9..0000000 --- a/Classes/Export/Event/AlternateExportQueryEvent.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Calien\Xlsexport\Export\Event; - -use Psr\EventDispatcher\StoppableEventInterface; - -final class AlternateExportQueryEvent implements StoppableEventInterface -{ - protected string $exportKey = ''; - - /** - * @var array <string, mixed> - */ - protected array $exportConfiguration = []; - - /** - * @param array $settings - * @param string $config - */ - public function __construct(array $settings, string $config) - { - $this->exportKey = $config; - $this->exportConfiguration = $settings; - } - - /** - * checkExportConfigExists - * - * Event listener should call this method to check if access is needed - * - * @param string $exportKey - * @return bool - */ - public function checkExportConfigExists(string $exportKey): bool - { - return $exportKey === $this->exportKey; - } - - public function alternateExportQuery(string $export): void - { - if ($this->exportConfiguration['export'] && !$this->exportConfiguration['manipulated']) { - $this->exportConfiguration['export'] = $export; - $this->exportConfiguration['manipulated'] = true; - } - } - - public function isPropagationStopped(): bool - { - $allManipulated = true; - if (!array_key_exists('manipulated', $this->exportConfiguration) || !$this->exportConfiguration['manipulated']) { - $allManipulated = false; - } - return $allManipulated; - } - - public function getManipulatedSettings(): array - { - return $this->exportConfiguration; - } -} diff --git a/Classes/Export/Event/AlternateHeaderLineEvent.php b/Classes/Export/Event/AlternateHeaderLineEvent.php deleted file mode 100644 index 538a573..0000000 --- a/Classes/Export/Event/AlternateHeaderLineEvent.php +++ /dev/null @@ -1,13 +0,0 @@ -<?php - -/** - * Markus Hofmann - * 12.10.21 23:17 - * churchevent - */ - -declare(strict_types=1); - -namespace Calien\Xlsexport\Export\Event; - -final class AlternateHeaderLineEvent {} diff --git a/Classes/Export/Event/ManipulateCellDataEvent.php b/Classes/Export/Event/ManipulateCellDataEvent.php deleted file mode 100644 index d2df643..0000000 --- a/Classes/Export/Event/ManipulateCellDataEvent.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Calien\Xlsexport\Export\Event; - -final class ManipulateCellDataEvent -{ - private string $columnName; - - /** - * @var array<array-key, mixed> $currentRow - */ - private array $currentRow; - - private mixed $value; - - public function __construct( - string $columnName, - array $currentRow, - mixed $value - ) { - $this->columnName = $columnName; - $this->currentRow = $currentRow; - $this->value = $value; - } - - public function getColumnName(): string - { - return $this->columnName; - } - - /** - * @return array<array-key, mixed> - */ - public function getCurrentRow(): array - { - return $this->currentRow; - } - - public function getValue(): mixed - { - return $this->value; - } - - public function setValue(mixed $value): void - { - $this->value = $value; - } -} diff --git a/Classes/Service/DatabaseQueryTypoScriptParser.php b/Classes/Service/DatabaseQueryTypoScriptParser.php new file mode 100644 index 0000000..d229e96 --- /dev/null +++ b/Classes/Service/DatabaseQueryTypoScriptParser.php @@ -0,0 +1,509 @@ +<?php + +declare(strict_types=1); + +namespace Calien\Xlsexport\Service; + +use Calien\Xlsexport\Exception\ExpressionTypeNotValidException; +use Calien\Xlsexport\Exception\ParameterHasWrongTypeException; +use Calien\Xlsexport\Exception\TypeIsNotAllowedAsQuoteException; +use Doctrine\DBAL\ParameterType; +use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Database\Query\QueryBuilder; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * This Service parses the TSconfig array to a valid QueryBuilder + */ +final class DatabaseQueryTypoScriptParser +{ + /** + * @param array{ + * table: non-empty-string, + * alias?: non-empty-string, + * select: non-empty-string[], + * count?: non-empty-string, + * selectLiteral?: non-empty-string[], + * where: array<array-key, array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * }>, + * join?: array<array{ + * from: non-empty-string, + * to: non-empty-string, + * toAlias?: non-empty-string, + * where: array<array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * }> + * }>, + * leftJoin?: array<array{ + * from: non-empty-string, + * to: non-empty-string, + * toAlias?: non-empty-string, + * where: array<array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * }> + * }>, + * rightJoin?: array<array{ + * from: non-empty-string, + * to: non-empty-string, + * toAlias?: non-empty-string, + * where: array<array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * }> + * }>, + * } $configuration + * @throws ExpressionTypeNotValidException + * @throws TypeIsNotAllowedAsQuoteException + */ + public function buildQueryBuilderFromArray(array $configuration): QueryBuilder + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable($configuration['table']); + $statement = $queryBuilder + ->select(...array_values($configuration['select'])) + ->from($configuration['table'], $configuration['alias'] ?? null); + + if (($configuration['selectLiteral'] ?? []) !== []) { + $statement->selectLiteral(...array_values($configuration['selectLiteral'])); + } + if ($configuration['where'] !== []) { + $where = []; + foreach ($configuration['where'] as $whereConfiguration) { + $where[] = $this->buildForExpressionType($queryBuilder, $whereConfiguration); + } + + $statement->where(...array_values($where)); + } + + if (($configuration['join'] ?? []) !== []) { + foreach ($configuration['join'] as $equiJoin) { + $this->buildEquiJoin($statement, $queryBuilder, $equiJoin); + } + } + + if (($configuration['leftJoin'] ?? []) !== []) { + foreach ($configuration['leftJoin'] as $leftJoin) { + $this->buildLeftJoin($statement, $queryBuilder, $leftJoin); + } + } + + if (($configuration['rightJoin'] ?? []) !== []) { + foreach ($configuration['rightJoin'] as $rightJoin) { + $this->buildRightJoin($statement, $queryBuilder, $rightJoin); + } + } + + return $statement; + } + + /** + * @param array{ + * table: non-empty-string, + * alias?: non-empty-string, + * select: non-empty-string[], + * count?: non-empty-string, + * selectLiteral?: non-empty-string[], + * where: array<array-key, array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * }>, + * join?: array<array{ + * from: non-empty-string, + * to: non-empty-string, + * toAlias?: non-empty-string, + * where: array<array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * }> + * }>, + * leftJoin?: array<array{ + * from: non-empty-string, + * to: non-empty-string, + * toAlias?: non-empty-string, + * where: array<array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * }> + * }>, + * rightJoin?: array<array{ + * from: non-empty-string, + * to: non-empty-string, + * toAlias?: non-empty-string, + * where: array<array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * }> + * }>, + * } $configuration + * @throws ExpressionTypeNotValidException + * @throws TypeIsNotAllowedAsQuoteException + */ + public function buildCountQueryFromArray(array $configuration): QueryBuilder + { + $statement = $this->buildQueryBuilderFromArray($configuration); + $statement->getConcreteQueryBuilder()->resetOrderBy(); + $statement->count($configuration['count'] ?? '*'); + + return $statement; + } + + public function replacePlaceholderWithCurrentId(QueryBuilder $statement, int $currentId): void + { + foreach ($statement->getParameters() as $key => $param) { + if ($param === '###CURRENT_ID###') { + $statement->setParameter($key, $currentId, Connection::PARAM_INT); + } + } + } + + /** + * @param array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * } $configuration + * @throws ExpressionTypeNotValidException + * @throws TypeIsNotAllowedAsQuoteException + */ + private function buildForExpressionType( + QueryBuilder $queryBuilder, + array $configuration + ): string { + return match ($configuration['expressionType']) { + 'eq' => $this->buildEquals($queryBuilder, $configuration), + 'neq' => $this->buildNotEquals($queryBuilder, $configuration), + 'gt' => $this->buildGreaterThan($queryBuilder, $configuration), + 'gte' => $this->buildGreaterThanOrEquals($queryBuilder, $configuration), + 'lt' => $this->buildLessThan($queryBuilder, $configuration), + 'lte' => $this->buildLessThanOrEquals($queryBuilder, $configuration), + 'isNull' => $this->buildIsNull($queryBuilder, $configuration['fieldName']), + 'isNotNull' => $this->buildIsNotNull($queryBuilder, $configuration['fieldName']), + 'in' => $this->buildIn($queryBuilder, $configuration['fieldName'], $configuration['parameter'], $configuration['type']), + 'inSet' => $this->buildInSet($queryBuilder, $configuration['fieldName'], $configuration['parameter']), + default => throw new ExpressionTypeNotValidException( + sprintf('The given expression type "%s" is not valid', $configuration['expressionType']), + 1731081406988 + ) + }; + } + + /** + * @param array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * } $configuration + * @throws ParameterHasWrongTypeException + * @throws \ReflectionException + */ + private function buildEquals(QueryBuilder $queryBuilder, array $configuration): string + { + return $queryBuilder->expr()->eq( + $configuration['fieldName'], + $this->generateValue($queryBuilder, $configuration) + ); + } + + /** + * @param array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * } $configuration + * @throws ParameterHasWrongTypeException + * @throws \ReflectionException + */ + private function buildNotEquals(QueryBuilder $queryBuilder, array $configuration): string + { + return $queryBuilder->expr()->neq( + $configuration['fieldName'], + $this->generateValue($queryBuilder, $configuration) + ); + } + + /** + * @param array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * } $configuration + * @throws ParameterHasWrongTypeException + * @throws \ReflectionException + */ + private function buildGreaterThan(QueryBuilder $queryBuilder, array $configuration): string + { + return $queryBuilder->expr()->gt( + $configuration['fieldName'], + $this->generateValue($queryBuilder, $configuration) + ); + } + + /** + * @param array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * } $configuration + * @throws ParameterHasWrongTypeException + * @throws \ReflectionException + */ + private function buildGreaterThanOrEquals(QueryBuilder $queryBuilder, array $configuration): string + { + return $queryBuilder->expr()->gte( + $configuration['fieldName'], + $this->generateValue($queryBuilder, $configuration) + ); + } + + /** + * @param array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * } $configuration + * @throws ParameterHasWrongTypeException + * @throws \ReflectionException + */ + private function buildLessThan(QueryBuilder $queryBuilder, array $configuration): string + { + return $queryBuilder->expr()->lt( + $configuration['fieldName'], + $this->generateValue($queryBuilder, $configuration) + ); + } + + /** + * @param array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * } $configuration + * @throws ParameterHasWrongTypeException + * @throws \ReflectionException + */ + private function buildLessThanOrEquals(QueryBuilder $queryBuilder, array $configuration): string + { + return $queryBuilder->expr()->lte( + $configuration['fieldName'], + $this->generateValue($queryBuilder, $configuration) + ); + } + + private function buildIsNull(QueryBuilder $queryBuilder, string $fieldName): string + { + return $queryBuilder->expr()->isNull($fieldName); + } + + private function buildIsNotNull(QueryBuilder $queryBuilder, string $fieldName): string + { + return $queryBuilder->expr()->isNotNull($fieldName); + } + + /** + * @param array<float|int|string>|float|int|string $parameter + */ + private function buildInSet(QueryBuilder $queryBuilder, string $fieldName, mixed $parameter): string + { + return $queryBuilder->expr()->inSet($fieldName, $queryBuilder->createNamedParameter($parameter, Connection::PARAM_STR)); + } + + /** + * @param array<float|int|string>|float|int|string $parameter + * @param string|int|ParameterType $type Connection::PARAM_* + * @throws TypeIsNotAllowedAsQuoteException + * @throws ParameterHasWrongTypeException + * @see Connection::PARAM_* + */ + private function buildIn(QueryBuilder $queryBuilder, string $fieldName, mixed $parameter, string|int|ParameterType $type): string + { + if (!is_array($parameter)) { + throw new ParameterHasWrongTypeException( + sprintf('Parameter has to be array for building "in" statement, "%s" given', gettype($parameter)), + 1731094230854 + ); + } + $quotedParameter = match ($type) { + Connection::PARAM_STR => $queryBuilder->quoteArrayBasedValueListToStringList($parameter), + Connection::PARAM_INT => $queryBuilder->quoteArrayBasedValueListToIntegerList($parameter), + default => throw new TypeIsNotAllowedAsQuoteException( + sprintf('The type "%s" can not be quoted for usage as `in`', $type?->name ?? $type), + 1731082482677 + ) + }; + return $queryBuilder->expr()->in($fieldName, $quotedParameter); + } + + /** + * @param array{ + * from: non-empty-string, + * to: non-empty-string, + * toAlias?: non-empty-string, + * where: array<array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * }> + * } $joinConfiguration + * @throws ExpressionTypeNotValidException + * @throws TypeIsNotAllowedAsQuoteException + */ + private function buildEquiJoin(QueryBuilder $statement, QueryBuilder $queryBuilder, array $joinConfiguration): void + { + $where = []; + foreach ($joinConfiguration['where'] as $joinWhere) { + $where[] = $this->buildForExpressionType($queryBuilder, $joinWhere); + } + $statement->join( + $joinConfiguration['from'], + $joinConfiguration['to'], + $joinConfiguration['toAlias'] ?? $joinConfiguration['to'], + ($where !== []) ? (string)$queryBuilder->expr()->and(...array_values($where)) : null + ); + } + + /** + * @param array{ + * from: non-empty-string, + * to: non-empty-string, + * toAlias?: non-empty-string, + * where: array<array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * }> + * } $joinConfiguration + * @throws ExpressionTypeNotValidException + * @throws TypeIsNotAllowedAsQuoteException + */ + private function buildLeftJoin(QueryBuilder $statement, QueryBuilder $queryBuilder, array $joinConfiguration): void + { + $where = []; + foreach ($joinConfiguration['where'] as $joinWhere) { + $where[] = $this->buildForExpressionType($queryBuilder, $joinWhere); + } + $statement->leftJoin( + $joinConfiguration['from'], + $joinConfiguration['to'], + $joinConfiguration['toAlias'] ?? $joinConfiguration['to'], + ($where !== []) ? (string)$queryBuilder->expr()->and(...array_values($where)) : null + ); + } + + /** + * @param array{ + * from: non-empty-string, + * to: non-empty-string, + * toAlias?: non-empty-string, + * where: array<array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: Connection::PARAM_*, + * expressionType: string, + * isColumn?: bool + * }> + * } $joinConfiguration + * @throws ExpressionTypeNotValidException + * @throws TypeIsNotAllowedAsQuoteException + */ + private function buildRightJoin(QueryBuilder $statement, QueryBuilder $queryBuilder, array $joinConfiguration): void + { + $where = []; + foreach ($joinConfiguration['where'] as $joinWhere) { + $where[] = $this->buildForExpressionType($queryBuilder, $joinWhere); + } + $statement->rightJoin( + $joinConfiguration['from'], + $joinConfiguration['to'], + $joinConfiguration['toAlias'] ?? $joinConfiguration['to'], + ($where !== []) ? (string)$queryBuilder->expr()->and(...array_values($where)) : null + ); + } + + /** + * @param array{ + * fieldName: string, + * parameter: float|int|string|float[]|int[]|string[], + * type: class-string|string, + * expressionType: string, + * isColumn?: bool + * } $configuration + * @throws ParameterHasWrongTypeException + * @throws \ReflectionException + */ + private function generateValue(QueryBuilder $queryBuilder, array $configuration): string + { + // true not possible because of TypoScript load + if (($configuration['isColumn'] ?? '0') === '1') { + if (!is_string($configuration['parameter'])) { + throw new ParameterHasWrongTypeException( + sprintf('Parameter has to be string, if "isColumn" is set to true, "%s" given', gettype($configuration['parameter'])), + 1731093539911 + ); + } + $value = $queryBuilder->quoteIdentifier($configuration['parameter']); + } else { + if ($configuration['type'] instanceof ParameterType) { + $constant = $configuration['type']; + } else { + $partsOfType = GeneralUtility::trimExplode('::', $configuration['type']); + $class = $partsOfType[0]; + if ($partsOfType[0] === 'Connection') { + $class = Connection::class; + } + $reflection = new \ReflectionClass($class); + $constant = $reflection->getConstant($partsOfType[1]); + } + + $value = $queryBuilder->createNamedParameter($configuration['parameter'], $constant); + } + + return $value; + } +} diff --git a/Classes/Service/SpreadsheetWriteService.php b/Classes/Service/SpreadsheetWriteService.php new file mode 100644 index 0000000..4703c10 --- /dev/null +++ b/Classes/Service/SpreadsheetWriteService.php @@ -0,0 +1,91 @@ +<?php + +declare(strict_types=1); + +namespace Calien\Xlsexport\Service; + +use Calien\Xlsexport\Event\AlternateFirstColumnInSheetEvent; +use Calien\Xlsexport\Event\AlternateHeaderLineEvent; +use Calien\Xlsexport\Event\ManipulateRowEntryEvent; +use Calien\Xlsexport\Exception\ExportFormatNotDetectedException; +use Doctrine\DBAL\Exception; +use Doctrine\DBAL\Result; +use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use TYPO3\CMS\Core\EventDispatcher\EventDispatcher; +use TYPO3\CMS\Core\Http\Stream; + +/** + * @internal + */ +final class SpreadsheetWriteService +{ + public function __construct( + private readonly EventDispatcher $eventDispatcher + ) {} + + /** + * @param array{ + * select: non-empty-string[], + * format?: non-empty-string, + * fieldLabels: non-empty-string[] + * } $configuration + * @throws ExportFormatNotDetectedException + * @throws Exception + */ + public function generateSpreadsheet(Result $result, array $configuration, string $configurationKey): Stream + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->setActiveSheetIndex(0); + + /** @var AlternateFirstColumnInSheetEvent $alternateFirstColumnEvent */ + $alternateFirstColumnEvent = $this->eventDispatcher->dispatch(new AlternateFirstColumnInSheetEvent()); + $firstColumn = $alternateFirstColumnEvent->getFirstColumn(); + + /** @var AlternateHeaderLineEvent $alternateHeaderLineEvent */ + $alternateHeaderLineEvent = $this->eventDispatcher->dispatch(new AlternateHeaderLineEvent($configuration['fieldLabels'], $configurationKey)); + $headerFieldLabels = $alternateHeaderLineEvent->getHeaderFieldLabels(); + $sheet->fromArray($headerFieldLabels, null, $firstColumn . '1'); + while ($dataRow = $result->fetchAssociative()) { + $row = $sheet->getHighestRow() + 1; + + /** @var ManipulateRowEntryEvent $manipulateRowEvent */ + $manipulateRowEvent = $this->eventDispatcher->dispatch(new ManipulateRowEntryEvent($dataRow, $headerFieldLabels, $configurationKey)); + $sheet->fromArray($manipulateRowEvent->getRow(), null, $firstColumn . $row); + } + + $iWriter = IOFactory::createWriter( + $spreadsheet, + $this->resolveFormatToWriterConstant($configuration['format'] ?? 'xlsx') + ); + + $resource = fopen('php://memory', 'w'); + if (!is_resource($resource)) { + throw new \RuntimeException( + 'Can not create resource for spreadsheet writer', + 1731108793376 + ); + } + $iWriter->save($resource); + + return new Stream($resource); + } + + /** + * @throws ExportFormatNotDetectedException + */ + private function resolveFormatToWriterConstant(string $format): string + { + $detectConstant = sprintf('WRITER_%s', mb_strtoupper($format)); + $reflection = new \ReflectionClass(IOFactory::class); + $writerType = $reflection->getConstant($detectConstant); + if (!is_string($writerType)) { + throw new ExportFormatNotDetectedException( + sprintf('The export format for file format "%s" was not found.', $format), + 1731106070328 + ); + } + + return $writerType; + } +} diff --git a/Classes/Traits/ExportTrait.php b/Classes/Traits/ExportTrait.php deleted file mode 100644 index 2ebdfac..0000000 --- a/Classes/Traits/ExportTrait.php +++ /dev/null @@ -1,164 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Calien\Xlsexport\Traits; - -use Calien\Xlsexport\Export\Event\AddColumnsToSheetEvent; -use Calien\Xlsexport\Export\Event\ManipulateCellDataEvent; -use PhpOffice\PhpSpreadsheet\Exception; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PhpOffice\PhpSpreadsheet\Worksheet\Row; -use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -use TYPO3\CMS\Core\Utility\GeneralUtility; - -trait ExportTrait -{ - protected $eventDispatcher; - /** - * @var array - */ - public static array $cols = [ - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL', 'AM', 'AN', 'AO', 'AP', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AV', 'AW', 'AX', 'AY', 'AZ', - ]; - /** - * @var int - */ - protected static int $rowCount = 0; - /** - * @var Spreadsheet|null - */ - protected static ?Spreadsheet $spreadSheet = null; - - /** - * loadSheet - * @return Worksheet - * @throws Exception - */ - protected static function loadSheet(): Worksheet - { - self::$spreadSheet = new Spreadsheet(); - self::$spreadSheet->getProperties()->setCreator('TYPO3 Export') - ->setLastModifiedBy('TYPO3 Export') - ->setTitle('Export ' . ' Dokument') - ->setSubject('Export ' . ' Dokument') - ->setCreated(time()) - ->setDescription('Export ' . ' Dokument Quelle '); - - $sheet = self::$spreadSheet->setActiveSheetIndex(0); - - self::$rowCount = 1; - - return $sheet; - } - - /** - * writeHeader - * @param Worksheet $sheet - * @param array $headerFields - */ - protected static function writeHeader(Worksheet $sheet, array $headerFields) - { - foreach ($headerFields as $field => $value) { - $sheet->setCellValue(self::$cols[$field] . self::$rowCount, $value); - } - self::$rowCount++; - } - - /** - * writeExcel - * @param Worksheet $sheet - * @param array $dataset - * @param array $exportFields - * @param string $table - * @param bool $autoFilter - * @param array $hookArray @deprecated - */ - protected function writeExcel( - Worksheet $sheet, - array $dataset, - array $exportFields, - string $table = '', - bool $autoFilter = false, - array $hookArray = [] - ) { - $data = []; - foreach ($dataset as $item) { - $data[] = $item; - } - - foreach ($data as $currentData) { - $colIndexer = 0; - foreach ($exportFields as $colIndexer => $value) { - $manipulateCellData = new ManipulateCellDataEvent($value, $currentData, $currentData[$value]); - if (!empty($this->eventDispatcher)) { - $this->eventDispatcher->dispatch($manipulateCellData); - } - $sheet->setCellValue(self::$cols[$colIndexer] . self::$rowCount, $manipulateCellData->getValue()); - } - $colIndexer++; - if (!empty($this->eventDispatcher)) { - $this->eventDispatcher->dispatch(new AddColumnsToSheetEvent($sheet, $colIndexer, self::$rowCount)); - } - if (array_key_exists($table, $hookArray) && is_array($hookArray[$table])) { - $colIndexer--; - foreach ($hookArray[$table] as $classObj) { - $hookObj = GeneralUtility::makeInstance($classObj); - if (method_exists($hookObj, 'addColumns')) { - trigger_error( - 'Usage of hooks inside XLS export is deprecated and will be removed in future versions. Use PSR-14 Event dispatching instead.', - E_USER_DEPRECATED - ); - $hookObj->addColumns($sheet, self::class, $colIndexer, self::$rowCount); - } - } - } - self::$rowCount++; - } - - if ($autoFilter) { - $sheet->setAutoFilter($sheet->calculateWorksheetDimension()); - } - - for ($i = 0; $i < count($exportFields); $i++) { - $sheet->getColumnDimension(self::$cols[$i])->setAutoSize(true); - } - - foreach ($sheet->getRowIterator() as $rowDimension) { - self::_autofitRowHeight($rowDimension); - } - } - - /** - * _autofitRowHeight - * @param Row $row - * @param int $rowPadding - * @return Worksheet - */ - private static function _autofitRowHeight(Row $row, int $rowPadding = 5): Worksheet - { - $ws = $row->getWorksheet(); - $cellIterator = $row->getCellIterator(); - $maxCellLines = 0; // Init - - // Find out max cell line count - foreach ($cellIterator as $cell) { - $lines = explode("\n", (string)$cell->getValue()); - $lineCount = 0; - // Ignore empty lines - foreach ($lines as &$ignored) { - $lineCount++; - } - $maxCellLines = max($maxCellLines, $lineCount); - } - - // Force minimum line height to 1 - $maxCellLines = max($maxCellLines, 1); - - // Adjust row height - $rowDimension = $ws->getRowDimension($row->getRowIndex()); - $rowHeight = (15 * $maxCellLines) + $rowPadding; // XLSX_LINE_HEIGHT = 13 - $rowDimension->setRowHeight($rowHeight); - return $ws; - } -} diff --git a/Classes/Traits/ExportWithTsSettingsTrait.php b/Classes/Traits/ExportWithTsSettingsTrait.php deleted file mode 100644 index 414ddc6..0000000 --- a/Classes/Traits/ExportWithTsSettingsTrait.php +++ /dev/null @@ -1,133 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Calien\Xlsexport\Traits; - -use Doctrine\DBAL\Driver\Exception; -use PhpOffice\PhpSpreadsheet\IOFactory; -use Psr\Http\Message\StreamInterface; -use TYPO3\CMS\Backend\Utility\BackendUtility; -use TYPO3\CMS\Core\Http\Stream; -use TYPO3\CMS\Core\Utility\GeneralUtility; - -trait ExportWithTsSettingsTrait -{ - use ExportTrait; - - /** - * @var array - */ - protected array $selfSettings = []; - /** - * @var string - */ - protected string $moduleName = 'tx_xlsexport'; - /** - * @var array - */ - protected array $modTSconfig; - - /** - * @param int $currentId - */ - protected function loadTSconfig(int $currentId) - { - $TSconfig = BackendUtility::getPagesTSconfig($currentId); - $moduleConfigArrayName = sprintf('%s.', $this->moduleName); - if (array_key_exists($moduleConfigArrayName, $TSconfig['mod.'])) { - $this->modTSconfig = $TSconfig['mod.'][$moduleConfigArrayName]; - $this->selfSettings = array_merge_recursive($this->selfSettings, $this->modTSconfig['settings.']); - } - } - - /** - * doExport - * @param array $settings - * @param int $currentId - * @return StreamInterface - * @throws Exception - * @throws \Doctrine\DBAL\Exception - * @throws \PhpOffice\PhpSpreadsheet\Exception - * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception - */ - protected function doExport(array $settings, int $currentId): StreamInterface - { - $this->normalizeSettings($settings); - $hookArray = []; - if ( - array_key_exists('xlsexport', $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']) - && array_key_exists('alternateQueries', $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['xlsexport']) - && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['xlsexport']['alternateQueries']) - ) { - $hookArray = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['xlsexport']['alternateQueries']; - } - - $exportfieldnames = []; - $exportfields = []; - - foreach ($settings['exportfields.'] as $value) { - $exportfields[] = $value; - } - foreach ($settings['exportfieldnames.'] as $value) { - $exportfieldnames[] = $value; - } - $exportQuery = $settings['export']; - if (array_key_exists($settings['table'], $hookArray) && is_array($hookArray[$settings['table']])) { - foreach ($hookArray[$settings['table']] as $classObj) { - $hookObj = GeneralUtility::makeInstance($classObj); - if (method_exists($hookObj, 'alternateExportQuery')) { - trigger_error( - 'Usage of hooks inside XLS export is deprecated and will be removed in future versions. Use PSR-14 Event dispatching instead.', - E_USER_DEPRECATED - ); - $exportQuery = $hookObj->alternateExportQuery($exportQuery, $this, ''); - } - } - } - - $statement = sprintf($exportQuery, $currentId); - $dbQuery = $this->dbConnection->getQueryBuilderForTable($settings['table'])->getConnection(); - $result = $dbQuery->executeQuery($statement)->fetchAllAssociative(); - - $sheet = $this->loadSheet(); - - $this->rowCount = 1; - - $headerManipulated = false; - if (array_key_exists($settings['table'], $hookArray) && is_array($hookArray[$settings['table']])) { - foreach ($hookArray[$settings['table']] as $classObj) { - $hookObj = GeneralUtility::makeInstance($classObj); - if (method_exists($hookObj, 'alternateHeaderLine')) { - trigger_error( - 'Usage of hooks inside XLS export is deprecated and will be removed in future versions. Use PSR-14 Event dispatching instead.', - E_USER_DEPRECATED - ); - $hookObj->alternateHeaderLine($sheet, $this, $exportfieldnames, $this->rowCount); - $headerManipulated = true; - } - } - } - - if (!$headerManipulated) { - // Zeile mit den Spaltenbezeichungen - $this->writeHeader($sheet, $exportfieldnames); - } - - // Die Datensätze eintragen - - $this->writeExcel($sheet, $result, $exportfields, $settings['table'], (bool)$settings['autofilter'], $hookArray); - - $tempFile = GeneralUtility::tempnam('xlsexport_', '.xlsx'); - $objWriter = IOFactory::createWriter(self::$spreadSheet, 'Xlsx'); - $objWriter->save($tempFile); - return new Stream($tempFile); - } - - private function normalizeSettings(array &$settings): void - { - if (!array_key_exists('autofilter', $settings)) { - $settings['autofilter'] = false; - } - } -} diff --git a/Configuration/Backend/Modules.php b/Configuration/Backend/Modules.php new file mode 100644 index 0000000..08d4b4c --- /dev/null +++ b/Configuration/Backend/Modules.php @@ -0,0 +1,26 @@ +<?php + +declare(strict_types=1); + +use Calien\Xlsexport\Controller\XlsExportController; + +return [ + 'web_xlsexport' => [ + 'routes' => [ + '_default' => [ + 'target' => XlsExportController::class . '::index', + ], + 'export' => [ + 'path' => '/export', + 'target' => XlsExportController::class . '::export', + ], + ], + 'parent' => 'web', + 'access' => 'user', + 'position' => [ + 'after' => 'web_list', + ], + 'iconIdentifier' => 'mimetypes-excel', + 'labels' => 'LLL:EXT:xlsexport/Resources/Private/Language/locallang_db.xlf', + ], +]; diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index ea4170d..deb5de0 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -6,6 +6,3 @@ services: Calien\Xlsexport\: resource: '../Classes/*' - - Calien\Xlsexport\Controller\XlsExportController: - tags: ['backend.controller'] diff --git a/Configuration/TCA/Overrides/pages.php b/Configuration/TCA/Overrides/pages.php deleted file mode 100755 index 06a6134..0000000 --- a/Configuration/TCA/Overrides/pages.php +++ /dev/null @@ -1,14 +0,0 @@ -<?php - -declare(strict_types=1); -/** - * Created by: markus - * Created at: 25.03.20 18:37 - */ -(function () { - \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::registerPageTSConfigFile( - 'xlsexport', - 'Configuration/TSconfig/Page/exampleExport.tsconfig', - 'Excel Exporter Basis Definition' - ); -})(); diff --git a/Configuration/TSconfig/Page/exampleExport.tsconfig b/Configuration/TSconfig/Page/exampleExport.tsconfig index f8225da..344631a 100755 --- a/Configuration/TSconfig/Page/exampleExport.tsconfig +++ b/Configuration/TSconfig/Page/exampleExport.tsconfig @@ -1,64 +1,50 @@ -mod.tx_xlsexport { - settings { - exports { - tt_address { - label = Adressen - check ( - select count(uid) from tt_address - where pid=%d and deleted=0 and hidden=0 - ) - list ( - select uid,first_name,last_name from tt_address - where pid=%d and deleted=0 and hidden=0 - ) - export ( - select uid,first_name,middle_name,last_name, - address,building,room,city,zip,region,country,phone, - fax,email,www,title,company - from tt_address - where pid=%d and deleted=0 and hidden=0 - ) - # ignored - archive = - table = tt_address - exportfields { - 10 = uid - 20 = first_name - 30 = middle_name - 40 = last_name - 50 = address - 60 = building - 70 = room - 80 = city - 90 = zip - 100 = region - 110 = country - 120 = phone - 130 = fax - 140 = email - 150 = www - 160 = title - 170 = company - } - - exportfieldnames { - 10 = lfd. Nummer - 20 = Vorname - 30 = Mittelname - 40 = Nachname - 50 = Adresse - 60 = Gebäude - 70 = Raum - 80 = Stadt - 90 = PLZ - 100 = Region - 110 = Land - 120 = Telefon - 130 = Fax - 140 = E-Mail - 150 = Web - 160 = Titel - 170 = Firma +mod.web_xlsexport { + content { + # could be LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:my.label, too + label = Contents + table = tt_content + alias = content + # one of the possible formats from Phpspreadsheet Writers + format = xlsx + select { + 10 = tt_content.uid + 20 = header + 30 = CType + 40 = title + } + # one single field, or just * + count = * + fieldLabels { + 10 = ID + 20 = Titel + 30 = Typ + 40 = Seite + } + where { + 10 { + fieldName = tt_content.pid + # CURRENT_ID will be automatically replaced by the current selected page id + parameter = ###CURRENT_ID### + expressionType = eq + # one of Connection::PARAM_* + type = Connection::PARAM_INT + } + } + join { + 10 { + # Use Alias here + from = content + to = pages + toAlias = p + where { + 10 { + fieldName = tt_content.pid + parameter = pages.uid + expressionType = eq + # one of Connection::PARAM_* + type = Connection::PARAM_STR + isColumn = 1 + } } } } diff --git a/Configuration/page.tsconfig b/Configuration/page.tsconfig new file mode 100644 index 0000000..dc023d4 --- /dev/null +++ b/Configuration/page.tsconfig @@ -0,0 +1,46 @@ +templates.typo3/cms-backend { + 1731094874154 = calien/xlsexport:Resources/Private +} +mod.web_xlsexport { + content { + label = Contents + table = tt_content + select { + 10 = tt_content.uid + 20 = header + 30 = CType + 40 = title + } + count = * + fieldLabels { + 10 = ID + 20 = Titel + 30 = Typ + 40 = Seite + } + where { + 10 { + fieldName = tt_content.pid + parameter = ###CURRENT_ID### + expressionType = eq + type = Connection::PARAM_INT + } + } + join { + 10 { + from = tt_content + to = pages + where { + 10 { + fieldName = tt_content.pid + parameter = pages.uid + expressionType = eq + # one of Connection::PARAM_* + type = Connection::PARAM_STR + isColumn = 1 + } + } + } + } + } +} diff --git a/Resources/Private/Layouts/Default.html b/Resources/Private/Layouts/Default.html deleted file mode 100755 index 58fdb81..0000000 --- a/Resources/Private/Layouts/Default.html +++ /dev/null @@ -1,14 +0,0 @@ -<html - xmlns:f="http://typo3.org/ns/TYPO3/Fluid/ViewHelpers" - xmlns:be="http://typo3.org/ns/TYPO3/CMS/Backend/ViewHelpers" - xmlns="http://www.w3.org/1999/xhtml" - lang="en" - f:schemaLocation="https://fluidtypo3.org/schemas/fluid-master.xsd" data-namespace-typo3-fluid="true"> -<be:moduleLayout name="Advanced Excel exporter" title="XLS Exporter"> - <f:flashMessages /> - <f:be.pageRenderer pageTitle="XLS Exporter" /> - <f:render section="Buttons" optional="true"/> - <f:render section="Headline" optional="true"/> - <f:render section="Content"/> -</be:moduleLayout> -</html> diff --git a/Resources/Private/Partials/.gitkeep b/Resources/Private/Partials/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Resources/Private/Partials/List.html b/Resources/Private/Partials/List.html index 1fff9c4..9c7fa9e 100644 --- a/Resources/Private/Partials/List.html +++ b/Resources/Private/Partials/List.html @@ -31,9 +31,11 @@ </th> </tr> </thead> - <f:for each="{datasets}" as="dataset"> + <f:for each="{datasets}" as="dataset" key="configuration"> <tr> - <td class="col-title col-responsive nowrap">{dataset.label}</td> + <td class="col-title col-responsive nowrap"> + <f:translate key="{dataset.label}" default="{dataset.label}" /> + </td> <td class="nowrap"> {dataset.count} {f:translate(key: 'index.records', extensionName: 'xlsexport')} </td> @@ -43,14 +45,14 @@ </div> <f:if condition="{dataset.count} > 0"> <f:then> - <f:link.action - action="export" + <f:be.link + route="web_xlsexport.export" class="btn btn-default" - arguments="{config: '{dataset.config}', id: '{id}'}" + parameters="{configuration: '{configuration}', id: '{pageId}'}" > <core:icon identifier="actions-download"/> {f:translate(key: 'index.export', extensionName: 'xlsexport')} - </f:link.action> + </f:be.link> </f:then> <f:else> <span class="btn btn-default disabled"> diff --git a/Resources/Private/Templates/XlsExport/Export.html b/Resources/Private/Templates/XlsExport/Export.html deleted file mode 100755 index 7c83d16..0000000 --- a/Resources/Private/Templates/XlsExport/Export.html +++ /dev/null @@ -1,16 +0,0 @@ -<html - xmlns:f="http://typo3.org/ns/TYPO3Fluid/Fluid/ViewHelpers" - xmlns="http://www.w3.org/1999/xhtml" - lang="en" - f:schemaLocation="https://fluidtypo3.org/schemas/fluid-master.xsd" - data-namespace-typo3-fluid="true" -> -<f:layout name="Default" /> -<f:section name="Content"> - <p>{f:translate(key: 'export.message.done')}</p> - <p>{f:translate(key: 'export.message.file')} <f:link.external uri="{fileurl}">{fileurl}</f:link.external></p> - <f:if condition="{noarchive}"> - <p>{f:translate(key: 'export.message.noarchive')}</p> - </f:if> -</f:section> -</html> diff --git a/Resources/Private/Templates/XlsExport/Index.html b/Resources/Private/Templates/XlsExport/Index.html index 13efd27..d6817d2 100755 --- a/Resources/Private/Templates/XlsExport/Index.html +++ b/Resources/Private/Templates/XlsExport/Index.html @@ -7,29 +7,20 @@ f:schemaLocation="https://fluidtypo3.org/schemas/fluid-master.xsd" data-namespace-typo3-fluid="true" > -<f:layout name="Default"/> +<f:layout name="Module"/> <f:section name="Headline"> <h1>XLS Exporter</h1> <f:be.pagePath/> </f:section> <f:section name="Content"> - <f:if condition="{id}"> + <f:if condition="{noConfig}"> <f:then> - <f:if condition="{noconfig}"> - <f:then> - <f:be.infobox title="{f:translate(key: 'noconfig.title')}" state="-1"> - {f:translate(key: 'noconfig.body') -> f:format.raw()} - </f:be.infobox> - </f:then> - <f:else> - <f:render partial="List" arguments="{_all}"/> - </f:else> - </f:if> + <f:be.infobox title="{f:translate(key: 'noconfig.title', extensionName: 'xlsexport')}" state="-1"> + {f:translate(key: 'noconfig.body', extensionName: 'xlsexport') -> f:format.raw()} + </f:be.infobox> </f:then> <f:else> - <f:be.infobox title="" state="-1"> - {f:translate(key: 'index.selectPage', extensionName: 'xlsexport')} - </f:be.infobox> + <f:render partial="List" arguments="{_all}"/> </f:else> </f:if> </f:section> diff --git a/Tests/Functional/Service/DatabaseQueryTypoScriptParserTest.php b/Tests/Functional/Service/DatabaseQueryTypoScriptParserTest.php new file mode 100644 index 0000000..8c0d398 --- /dev/null +++ b/Tests/Functional/Service/DatabaseQueryTypoScriptParserTest.php @@ -0,0 +1,174 @@ +<?php + +declare(strict_types=1); + +namespace Calien\Xlsexport\Tests\Functional\Service; + +use Calien\Xlsexport\Exception\ExpressionTypeNotValidException; +use Calien\Xlsexport\Exception\TypeIsNotAllowedAsQuoteException; +use Calien\Xlsexport\Service\DatabaseQueryTypoScriptParser; +use Doctrine\DBAL\Query\Expression\CompositeExpression; +use PHPUnit\Framework\Attributes\Test; +use TYPO3\CMS\Core\Database\Query\QueryBuilder; +use TYPO3\CMS\Core\Information\Typo3Version; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +final class DatabaseQueryTypoScriptParserTest extends FunctionalTestCase +{ + #[Test] + public function simpleQueryArrayIsParsedCorrect(): void + { + $tsConfig = [ + 'table' => 'pages', + 'select' => [ + 'uid', + 'pid', + 'title', + ], + 'where' => [ + [ + 'fieldName' => 'pid', + 'parameter' => 1, + 'type' => 'Connection::PARAM_INT', + 'expressionType' => 'eq', + ], + ], + ]; + + $subject = new DatabaseQueryTypoScriptParser(); + + $statement = $subject->buildQueryBuilderFromArray($tsConfig); + + self::assertInstanceOf(QueryBuilder::class, $statement); + + $connectionParams = $statement->getConnection()->getParams(); + $escapeCharacter = match ($connectionParams['driver'] ?? '') { + 'pdo_pgsql', 'pdo_sqlite' => '"', + default => '`' + }; + + if ((new Typo3Version())->getMajorVersion() <= 12) { + $parts = $statement->getQueryParts(); + + self::assertIsArray($parts['select']); + self::assertContains(sprintf('%1$suid%1$s', $escapeCharacter), $parts['select']); + self::assertContains(sprintf('%1$spid%1$s', $escapeCharacter), $parts['select']); + self::assertContains(sprintf('%1$stitle%1$s', $escapeCharacter), $parts['select']); + self::assertInstanceOf(CompositeExpression::class, $parts['where']); + self::assertEquals('AND', $parts['where']->getType()); + self::assertEquals(1, $parts['where']->count()); + self::assertEquals(sprintf('%1$spid%1$s = :dcValue1', $escapeCharacter), (string)$parts['where']); + self::assertIsArray($parts['from']); + $from = array_pop($parts['from']); + self::assertIsArray($from); + self::assertEquals(['alias' => null, 'table' => sprintf('%1$spages%1$s', $escapeCharacter)], $from); + } else { + self::assertIsArray($statement->getSelect()); + self::assertContains('"uid"', $statement->getSelect()); + self::assertContains('"pid"', $statement->getSelect()); + self::assertContains('"title"', $statement->getSelect()); + self::assertEquals('"pid" = :dcValue1', (string)$statement->getWhere()); + self::assertIsArray($statement->getFrom()); + $fromArray = $statement->getFrom(); + $from = array_pop($fromArray); + self::assertEquals('"pages"', $from->table); + self::assertNull($from->alias); + } + } + + #[Test] + public function invalidExpressionTypeThrowsException(): void + { + $tsConfig = [ + 'table' => 'pages', + 'select' => [ + 'uid', + 'pid', + 'title', + ], + 'where' => [ + [ + 'fieldName' => 'pid', + 'parameter' => 1, + 'type' => 'Connection::PARAM_INT', + 'expressionType' => 'not-allowed-expression-type', + ], + ], + ]; + + $subject = new DatabaseQueryTypoScriptParser(); + + self::expectException(ExpressionTypeNotValidException::class); + $subject->buildQueryBuilderFromArray($tsConfig); + } + + #[Test] + public function invalidFieldTypeThrowsException(): void + { + $tsConfig = [ + 'table' => 'pages', + 'select' => [ + 'uid', + 'pid', + 'title', + ], + 'where' => [ + [ + 'fieldName' => 'pid', + 'parameter' => [1], + 'type' => 'Connection::PARAM_BOOL', + 'expressionType' => 'in', + ], + ], + ]; + + $subject = new DatabaseQueryTypoScriptParser(); + + self::expectException(TypeIsNotAllowedAsQuoteException::class); + $subject->buildQueryBuilderFromArray($tsConfig); + } + + #[Test] + public function equiJoinBuildWorksCorrect(): void + { + $tsConfig = [ + 'table' => 'pages', + 'select' => [ + 'uid', + 'pid', + 'title', + ], + 'where' => [ + [ + 'fieldName' => 'pid', + 'parameter' => 1, + 'type' => 'Connection::PARAM_INT', + 'expressionType' => 'eq', + ], + ], + 'join' => [ + [ + 'from' => 'pages', + 'to' => 'tt_content', + 'where' => [ + [ + 'fieldName' => 'pages.uid', + 'parameter' => 'tt_content.pid', + 'type' => 'Connection::PARAM_STR', + 'expressionType' => 'eq', + 'isColumn' => true, + ], + ], + ], + ], + ]; + + $subject = new DatabaseQueryTypoScriptParser(); + + $statement = $subject->buildQueryBuilderFromArray($tsConfig); + + self::assertInstanceOf(QueryBuilder::class, $statement); + + //$parts = $statement->getQueryParts(); + } +} diff --git a/Tests/Unit/DummyTest.php b/Tests/Unit/DummyTest.php new file mode 100644 index 0000000..7226103 --- /dev/null +++ b/Tests/Unit/DummyTest.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +namespace Calien\Xlsexport\Tests\Unit; + +use PHPUnit\Framework\Attributes\Test; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +class DummyTest extends UnitTestCase +{ + #[Test] + public function dummy(): void + { + self::assertTrue(true); + } +} diff --git a/composer.json b/composer.json index fc4e0b4..df33a40 100755 --- a/composer.json +++ b/composer.json @@ -23,10 +23,10 @@ "typo3/cms-core": "^12.4 || ^13.4" }, "require-dev": { - "armin/editorconfig-cli": "^1.5", "friendsofphp/php-cs-fixer": "^3.0", + "helhum/typo3-console": "^8.2", "phpstan/phpstan": "^1.3", - "ssch/typo3-rector": "^2.11", + "saschaegerer/phpstan-typo3": "^1.10", "typo3/testing-framework": "^8.0" }, "extra": { @@ -40,6 +40,11 @@ "Calien\\Xlsexport\\": "Classes/" } }, + "autoload-dev": { + "psr-4": { + "Calien\\Xlsexport\\Tests\\": "Tests/" + } + }, "config": { "allow-plugins": { "typo3/cms-composer-installers": true, diff --git a/ext_localconf.php b/ext_localconf.php index 59835df..b1f70e1 100755 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,4 +1,4 @@ <?php -(function () { +(static function (): void { })(); diff --git a/ext_tables.php b/ext_tables.php index c8cc980..b1f70e1 100755 --- a/ext_tables.php +++ b/ext_tables.php @@ -1,4 +1,4 @@ <?php -(static function () { +(static function (): void { })(); diff --git a/ext_typoscript_setup.typoscript b/ext_typoscript_setup.typoscript deleted file mode 100755 index 321a468..0000000 --- a/ext_typoscript_setup.typoscript +++ /dev/null @@ -1,13 +0,0 @@ -module.tx_xlsexport { - view { - layoutRootPaths { - 20 = EXT:xlsexport/Resources/Private/Layouts/ - } - partialRootPaths { - 20 = EXT:xlsexport/Resources/Private/Partials/ - } - templateRootPaths { - 20 = EXT:xlsexport/Resources/Private/Templates/ - } - } -} diff --git a/rector.php b/rector.php deleted file mode 100644 index 2a169d7..0000000 --- a/rector.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php - -declare(strict_types=1); - -use Rector\Config\RectorConfig; -use Rector\PostRector\Rector\NameImportingPostRector; -use Rector\TypeDeclaration\Rector\ClassMethod\AddVoidReturnTypeWhereNoReturnRector; -use Rector\ValueObject\PhpVersion; -use Ssch\TYPO3Rector\CodeQuality\General\ConvertImplicitVariablesToExplicitGlobalsRector; -use Ssch\TYPO3Rector\CodeQuality\General\ExtEmConfRector; -use Ssch\TYPO3Rector\Configuration\Typo3Option; -use Ssch\TYPO3Rector\Set\Typo3LevelSetList; -use Ssch\TYPO3Rector\Set\Typo3SetList; - -return RectorConfig::configure() - ->withPaths([ - __DIR__ . '/Build', - __DIR__ . '/Classes', - __DIR__ . '/Configuration', - __DIR__ . '/ext_emconf.php', - __DIR__ . '/ext_localconf.php', - __DIR__ . '/ext_tables.php', - ]) - // uncomment to reach your current PHP version - // ->withPhpSets() - ->withPhpVersion(PhpVersion::PHP_81) - ->withSets([ - Typo3SetList::CODE_QUALITY, - Typo3SetList::GENERAL, - Typo3LevelSetList::UP_TO_TYPO3_12, - ]) - # To have a better analysis from PHPStan, we teach it here some more things - ->withPHPStanConfigs([ - Typo3Option::PHPSTAN_FOR_RECTOR_PATH - ]) - ->withRules([ - AddVoidReturnTypeWhereNoReturnRector::class, - ConvertImplicitVariablesToExplicitGlobalsRector::class, - ]) - ->withConfiguredRule(ExtEmConfRector::class, [ - ExtEmConfRector::PHP_VERSION_CONSTRAINT => '8.1.0-8.4.99', - ExtEmConfRector::TYPO3_VERSION_CONSTRAINT => '12.4.0-13.4.99', - ExtEmConfRector::ADDITIONAL_VALUES_TO_BE_REMOVED => [] - ]) - # If you use withImportNames(), you should consider excluding some TYPO3 files. - ->withSkip([ - // @see https://github.com/sabbelasichon/typo3-rector/issues/2536 - __DIR__ . '/**/Configuration/ExtensionBuilder/*', - NameImportingPostRector::class => [ - 'ext_localconf.php', // This line can be removed since TYPO3 11.4, see https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/11.4/Important-94280-MoveContentsOfExtPhpIntoLocalScopes.html - 'ext_tables.php', // This line can be removed since TYPO3 11.4, see https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/11.4/Important-94280-MoveContentsOfExtPhpIntoLocalScopes.html - 'ClassAliasMap.php', - ] - ]) -;