diff --git a/.github/workflows/tasks.yml b/.github/workflows/tasks.yml index f085e6d..dd00264 100644 --- a/.github/workflows/tasks.yml +++ b/.github/workflows/tasks.yml @@ -40,11 +40,46 @@ jobs: - run: composer update --with typo3/cms-core:^${{ matrix.typo3 }} - run: ./vendor/bin/grumphp run --ansi + unit-tests: + name: "unit php: ${{ matrix.php }} TYPO3: ${{ matrix.typo3 }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: [ '8.2', '8.3', '8.4', '8.5' ] + typo3: [ '13', '14' ] + steps: + - name: Setup PHP with PECL extension + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + - uses: actions/checkout@v6 + - run: composer update --with typo3/cms-core:^${{ matrix.typo3 }} + - run: CI=true ./Build/Scripts/runTests.sh -p ${{ matrix.php }} -s unit + + functional-tests: + name: "functional php: ${{ matrix.php }} TYPO3: ${{ matrix.typo3 }} DB: ${{ matrix.dbms }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: [ '8.2', '8.3', '8.4', '8.5' ] + typo3: [ '13', '14' ] + dbms: [ 'sqlite', 'mariadb', 'mysql', 'postgres' ] + steps: + - name: Setup PHP with PECL extension + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + - uses: actions/checkout@v6 + - run: composer update --with typo3/cms-core:^${{ matrix.typo3 }} + - run: CI=true ./Build/Scripts/runTests.sh -p ${{ matrix.php }} -d ${{ matrix.dbms }} -s functional + ter-release: name: Publish new version to TER runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') - needs: [ lint-php ] + needs: [ lint-php, unit-tests, functional-tests ] env: TYPO3_API_TOKEN: ${{ secrets.TYPO3_API_TOKEN }} diff --git a/.gitignore b/.gitignore index 03089d8..1076919 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.idea/ +/.phpunit.cache/ /var/ /vendor/ /composer.lock diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh new file mode 100755 index 0000000..5f02819 --- /dev/null +++ b/Build/Scripts/runTests.sh @@ -0,0 +1,287 @@ +#!/usr/bin/env bash + +set -eu + +if [ "${CI:-}" != "true" ]; then + trap 'echo "runTests.sh SIGINT signal emitted"; cleanUp; exit 2' SIGINT +fi + +cleanUp() { + # remove the infinite recursion symlink + rm -rf ./public/typo3temp/var/tests/functional-*/typo3conf/ext/visual_editor + if [ -n "${NETWORK_CREATED:-}" ]; then + ATTACHED_CONTAINERS=$(${CONTAINER_BIN} ps --filter network=${NETWORK} --format='{{.Names}}' 2>/dev/null || true) + for ATTACHED_CONTAINER in ${ATTACHED_CONTAINERS}; do + ${CONTAINER_BIN} kill ${ATTACHED_CONTAINER} >/dev/null 2>&1 || true + done + ${CONTAINER_BIN} network rm ${NETWORK} >/dev/null 2>&1 || true + fi +} + +waitFor() { + local HOST=${1} + local PORT=${2} + [[ -n "${3:-}" ]] && echo -n "Startup wait of $3 ... " && sleep "$3" && echo "done" + local TESTCOMMAND=" + COUNT=0; + while ! nc -z ${HOST} ${PORT}; do + if [ \"\${COUNT}\" -gt 30 ]; 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 +} + +handleDbmsOptions() { + case ${DBMS} in + mariadb) + if [ -z "${DATABASE_DRIVER}" ]; then + DATABASE_DRIVER="mysqli" + fi + if [ "${DATABASE_DRIVER}" != "mysqli" ] && [ "${DATABASE_DRIVER}" != "pdo_mysql" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + exit 1 + fi + if [ -z "${DBMS_VERSION}" ]; then + DBMS_VERSION="10.11" + fi + ;; + mysql) + if [ -z "${DATABASE_DRIVER}" ]; then + DATABASE_DRIVER="mysqli" + fi + if [ "${DATABASE_DRIVER}" != "mysqli" ] && [ "${DATABASE_DRIVER}" != "pdo_mysql" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + exit 1 + fi + if [ -z "${DBMS_VERSION}" ]; then + DBMS_VERSION="8.4" + fi + ;; + postgres) + if [ -n "${DATABASE_DRIVER}" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + exit 1 + fi + if [ -z "${DBMS_VERSION}" ]; then + DBMS_VERSION="16" + fi + ;; + sqlite) + if [ -n "${DATABASE_DRIVER}" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + exit 1 + fi + if [ -n "${DBMS_VERSION}" ]; then + echo "Invalid combination -d ${DBMS} -i ${DBMS_VERSION}" >&2 + exit 1 + fi + ;; + *) + echo "Invalid option -d ${DBMS}" >&2 + exit 1 + ;; + esac +} + +loadHelp() { + read -r -d '' HELP < Test suite to run + -b Container runtime + -p <8.2|8.3|8.4|8.5> PHP version (default: 8.2) + -d + Functional DBMS (default: sqlite) + -a DB driver for mysql/mariadb + -i Specific DBMS version + -x Enable xdebug + -y Xdebug port (default: 9003) + -h Show help + +Examples: + ./Build/Scripts/runTests.sh -s unit + ./Build/Scripts/runTests.sh -s functional + ./Build/Scripts/runTests.sh -s functional -d sqlite -- --filter LocalizationServiceTest +EOF +} + +if ! type "docker" >/dev/null 2>&1 && ! type "podman" >/dev/null 2>&1; then + echo "This script relies on docker or podman. Please install one of them." >&2 + exit 1 +fi + +THIS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd "$THIS_SCRIPT_DIR" || exit 1 +cd ../../ || exit 1 +CORE_ROOT="${PWD}" + +TEST_SUITE="help" +DBMS="sqlite" +DBMS_VERSION="" +PHP_VERSION="8.2" +PHP_XDEBUG_ON=0 +PHP_XDEBUG_PORT=9003 +DATABASE_DRIVER="" +CONTAINER_BIN="" +CONTAINER_INTERACTIVE="-it --init" +CONTAINER_HOST="host.docker.internal" +HOST_UID=$(id -u) +HOST_PID=$(id -g) +USERSET="" +SUFFIX=$(echo $RANDOM) +NETWORK="visual-editor-${SUFFIX}" +NETWORK_CREATED="" +NETWORK_PARAM="" + +if [ "${CI:-}" = "true" ]; then + CONTAINER_INTERACTIVE="" +fi + +OPTIND=1 +while getopts "a:b:s:d:i:p:xy:h" OPT; do + case ${OPT} in + s) TEST_SUITE=${OPTARG} ;; + b) CONTAINER_BIN=${OPTARG} ;; + a) DATABASE_DRIVER=${OPTARG} ;; + d) DBMS=${OPTARG} ;; + i) DBMS_VERSION=${OPTARG} ;; + p) PHP_VERSION=${OPTARG} ;; + x) PHP_XDEBUG_ON=1 ;; + y) PHP_XDEBUG_PORT=${OPTARG} ;; + h) TEST_SUITE="help" ;; + *) TEST_SUITE="help" ;; + esac +done + +handleDbmsOptions + +if [[ -z "${CONTAINER_BIN}" ]]; then + if type "docker" >/dev/null 2>&1; then + CONTAINER_BIN="docker" + else + CONTAINER_BIN="podman" + fi +fi + +if ! type "${CONTAINER_BIN}" >/dev/null 2>&1; then + echo "Selected container environment \"${CONTAINER_BIN}\" not found." >&2 + exit 1 +fi + +if [ "$(uname)" != "Darwin" ] && [ "${CONTAINER_BIN}" = "docker" ]; then + USERSET="--user ${HOST_UID}" +fi + +IMAGE_PHP="ghcr.io/typo3/core-testing-$(echo "php${PHP_VERSION}" | sed -e 's/\.//'):latest" +IMAGE_ALPINE="docker.io/alpine:3.8" +IMAGE_MARIADB="docker.io/mariadb:${DBMS_VERSION}" +IMAGE_MYSQL="docker.io/mysql:${DBMS_VERSION}" +IMAGE_POSTGRES="docker.io/postgres:${DBMS_VERSION}-alpine" + +shift $((OPTIND - 1)) + +mkdir -p .cache public/typo3temp/var/tests + +if [ "${TEST_SUITE}" = "functional" ] && [ "${DBMS}" != "sqlite" ]; then + if [ "${CONTAINER_BIN}" = "docker" ]; then + NETWORK_PARAM="--network bridge" + else + ${CONTAINER_BIN} network create ${NETWORK} >/dev/null + NETWORK_CREATED=1 + NETWORK_PARAM="--network ${NETWORK}" + fi +fi + +if [ "${CONTAINER_BIN}" = "docker" ]; then + CONTAINER_COMMON_PARAMS="${CONTAINER_INTERACTIVE} --rm --add-host ${CONTAINER_HOST}:host-gateway ${USERSET} -e TYPO3_PATH_ROOT=${CORE_ROOT}/public -e TYPO3_PATH_WEB=${CORE_ROOT}/public -v ${CORE_ROOT}:${CORE_ROOT} -w ${CORE_ROOT}" +else + CONTAINER_HOST="host.containers.internal" + CONTAINER_COMMON_PARAMS="${CONTAINER_INTERACTIVE} --rm -e TYPO3_PATH_ROOT=${CORE_ROOT}/public -e TYPO3_PATH_WEB=${CORE_ROOT}/public -v ${CORE_ROOT}:${CORE_ROOT} -w ${CORE_ROOT}" +fi + +if [ -n "${NETWORK_PARAM}" ]; then + CONTAINER_COMMON_PARAMS="${CONTAINER_COMMON_PARAMS} ${NETWORK_PARAM}" +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_EXIT_CODE=1 +case ${TEST_SUITE} in + unit) + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name unit-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${IMAGE_PHP} vendor/bin/phpunit -c Build/phpunit/UnitTests.xml "$@" + SUITE_EXIT_CODE=$? + ;; + functional) + COMMAND=(vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml --exclude-group not-${DBMS} "$@") + case ${DBMS} in + mariadb) + ${CONTAINER_BIN} run --rm --name mariadb-func-${SUFFIX} ${NETWORK_PARAM} -d -e MYSQL_ROOT_PASSWORD=funcp --tmpfs /var/lib/mysql/:rw,noexec,nosuid ${IMAGE_MARIADB} >/dev/null + DATABASE_HOST=mariadb-func-${SUFFIX} + if [ "${CONTAINER_BIN}" = "docker" ]; then + DATABASE_HOST=$(${CONTAINER_BIN} inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mariadb-func-${SUFFIX}) + fi + waitFor ${DATABASE_HOST} 3306 + CONTAINERPARAMS="-e typo3DatabaseDriver=${DATABASE_DRIVER} -e typo3DatabaseName=func_test -e typo3DatabaseUsername=root -e typo3DatabaseHost=${DATABASE_HOST} -e typo3DatabasePassword=funcp" + ${CONTAINER_BIN} run --rm ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + mysql) + ${CONTAINER_BIN} run --rm --name mysql-func-${SUFFIX} ${NETWORK_PARAM} -d -e MYSQL_ROOT_PASSWORD=funcp --tmpfs /var/lib/mysql/:rw,noexec,nosuid ${IMAGE_MYSQL} >/dev/null + DATABASE_HOST=mysql-func-${SUFFIX} + if [ "${CONTAINER_BIN}" = "docker" ]; then + DATABASE_HOST=$(${CONTAINER_BIN} inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mysql-func-${SUFFIX}) + fi + waitFor ${DATABASE_HOST} 3306 2 + CONTAINERPARAMS="-e typo3DatabaseDriver=${DATABASE_DRIVER} -e typo3DatabaseName=func_test -e typo3DatabaseUsername=root -e typo3DatabaseHost=${DATABASE_HOST} -e typo3DatabasePassword=funcp" + ${CONTAINER_BIN} run --rm ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + postgres) + ${CONTAINER_BIN} run --rm --name postgres-func-${SUFFIX} ${NETWORK_PARAM} -d -e POSTGRES_PASSWORD=funcp -e POSTGRES_USER=funcu --tmpfs /var/lib/postgresql/data:rw,noexec,nosuid ${IMAGE_POSTGRES} >/dev/null + DATABASE_HOST=postgres-func-${SUFFIX} + if [ "${CONTAINER_BIN}" = "docker" ]; then + DATABASE_HOST=$(${CONTAINER_BIN} inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' postgres-func-${SUFFIX}) + fi + waitFor ${DATABASE_HOST} 5432 + CONTAINERPARAMS="-e typo3DatabaseDriver=pdo_pgsql -e typo3DatabaseName=func_test -e typo3DatabaseUsername=funcu -e typo3DatabaseHost=${DATABASE_HOST} -e typo3DatabasePassword=funcp" + ${CONTAINER_BIN} run --rm ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + sqlite) + rm -rf "${CORE_ROOT}/public/typo3temp/var/tests/functional-sqlite-dbs" + mkdir -p "${CORE_ROOT}/public/typo3temp/var/tests/functional-sqlite-dbs/" + CONTAINERPARAMS="-e typo3DatabaseDriver=pdo_sqlite" + ${CONTAINER_BIN} run --rm ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + esac + ;; + help) + loadHelp + echo "${HELP}" + SUITE_EXIT_CODE=0 + ;; + *) + loadHelp + echo "${HELP}" >&2 + SUITE_EXIT_CODE=1 + ;; +esac + +cleanUp +exit ${SUITE_EXIT_CODE} diff --git a/Build/phpunit/FunctionalTests.xml b/Build/phpunit/FunctionalTests.xml new file mode 100644 index 0000000..ffcf336 --- /dev/null +++ b/Build/phpunit/FunctionalTests.xml @@ -0,0 +1,28 @@ + + + + ../../Tests/Functional/ + + + + + + + diff --git a/Build/phpunit/FunctionalTestsBootstrap.php b/Build/phpunit/FunctionalTestsBootstrap.php new file mode 100644 index 0000000..69e352a --- /dev/null +++ b/Build/phpunit/FunctionalTestsBootstrap.php @@ -0,0 +1,14 @@ +defineOriginalRootPath(); + + $webRoot = $testbase->getWebRoot(); + $testbase->createDirectory($webRoot . 'typo3temp/var/tests'); + $testbase->createDirectory($webRoot . 'typo3temp/var/transient'); +})(); diff --git a/Build/phpunit/UnitTests.xml b/Build/phpunit/UnitTests.xml new file mode 100644 index 0000000..994da9c --- /dev/null +++ b/Build/phpunit/UnitTests.xml @@ -0,0 +1,28 @@ + + + + ../../Tests/Unit/ + + + + + + + diff --git a/Build/phpunit/UnitTestsBootstrap.php b/Build/phpunit/UnitTestsBootstrap.php new file mode 100644 index 0000000..5c93d21 --- /dev/null +++ b/Build/phpunit/UnitTestsBootstrap.php @@ -0,0 +1,58 @@ +getWebRoot(), '/')); + } + + if (!getenv('TYPO3_PATH_WEB')) { + putenv('TYPO3_PATH_WEB=' . rtrim($testbase->getWebRoot(), '/')); + } + + $testbase->defineSitePath(); + + $requestType = SystemEnvironmentBuilder::REQUESTTYPE_BE | SystemEnvironmentBuilder::REQUESTTYPE_CLI; + SystemEnvironmentBuilder::run(0, $requestType); + + $testbase->createDirectory(Environment::getPublicPath() . '/typo3conf/ext'); + $testbase->createDirectory(Environment::getPublicPath() . '/typo3temp/assets'); + $testbase->createDirectory(Environment::getPublicPath() . '/typo3temp/var/tests'); + $testbase->createDirectory(Environment::getPublicPath() . '/typo3temp/var/transient'); + + $classLoader = require $testbase->getPackagesPath() . '/autoload.php'; + Bootstrap::initializeClassLoader($classLoader); + + $configurationManager = new ConfigurationManager(); + $GLOBALS['TYPO3_CONF_VARS'] = $configurationManager->getDefaultConfiguration(); + + $cache = new PhpFrontend( + 'core', + new NullBackend('production', []) + ); + $packageManager = Bootstrap::createPackageManager( + UnitTestPackageManager::class, + Bootstrap::createPackageCache($cache) + ); + GeneralUtility::setSingletonInstance(PackageManager::class, $packageManager); + ExtensionManagementUtility::setPackageManager($packageManager); + + $testbase->dumpClassLoadingInformation(); + + GeneralUtility::purgeInstances(); +})(); diff --git a/Build/settings.php b/Build/settings.php new file mode 100644 index 0000000..697b431 --- /dev/null +++ b/Build/settings.php @@ -0,0 +1,32 @@ + [ + 'debug' => true, + 'installToolPassword' => 'foo', + ], + 'DB' => [ + 'Connections' => [ + 'Default' => [ + 'charset' => 'utf8mb4', + 'dbname' => getenv('typo3DatabaseName') !== false ? getenv('typo3DatabaseName') . '_at' : '', + 'driver' => getenv('typo3DatabaseDriver') ?: 'mysqli', + 'host' => getenv('typo3DatabaseHost') ?: '', + 'password' => getenv('typo3DatabasePassword') ?: '', + 'port' => (int)(getenv('typo3DatabasePort') ?: 3306), + 'defaultTableOptions' => [ + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + ], + 'user' => getenv('typo3DatabaseUsername') ?: '', + ], + ], + ], + 'SYS' => [ + 'encryptionKey' => '2db7840443af8743e79865b25fcb7ea3c2684074f0924e676c90ddb31cad71fa5178e7c98de90dbc8b42bcc8d352402d', + 'sitename' => 'visual_editor-tests', + 'trustedHostsPattern' => '.*.*', + ], +]; diff --git a/Classes/Service/LocalizationService.php b/Classes/Service/LocalizationService.php index aafded6..a5bb61c 100644 --- a/Classes/Service/LocalizationService.php +++ b/Classes/Service/LocalizationService.php @@ -15,7 +15,7 @@ */ public function tryTranslation(string $label, ?array $arguments = null, Locale|string|null $languageKey = null): string { - $languageKey ??= $this->getBackendUserLanguage(); + $languageKey ??= $this->getBackendUserLanguage(); try { return LocalizationUtility::translate($label, null, $arguments, $languageKey) @@ -27,11 +27,6 @@ public function tryTranslation(string $label, ?array $arguments = null, Locale|s public function getBackendUserLanguage(): ?string { - $backendUserAuthentication = $GLOBALS['BE_USER']; - if ($backendUserAuthentication?->user['lang'] !== null) { - return $backendUserAuthentication->user['lang']; - } - - return null; + return $GLOBALS['BE_USER']?->user['lang'] ?? null; } } diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Command/ListPostCommand.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Command/ListPostCommand.php new file mode 100644 index 0000000..5c63b0c --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Command/ListPostCommand.php @@ -0,0 +1,40 @@ +title($this->getDescription()); + + $repository = GeneralUtility::makeInstance(PostRepository::class); + $result = []; + $io->success(sprintf('Found %d posts:', count($repository->findAll()))); + + return Command::SUCCESS; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Controller/BlogController.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Controller/BlogController.php new file mode 100644 index 0000000..a92561e --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Controller/BlogController.php @@ -0,0 +1,132 @@ +defaultViewObjectName = JsonView::class; + } + + public function listAction(): ResponseInterface + { + $blogs = $this->blogRepository->findAll(); + $value = []; + $value[$this->getRuntimeIdentifier()] = $this->getStructure($blogs); + + $this->view->assign('value', $value); + + return $this->htmlResponse(); + } + + public function detailsAction(?Blog $blog = null): ResponseInterface + { + return $this->htmlResponse($blog ? $blog->getTitle() : ''); + } + + public function testSingleAction(Blog $blog): ResponseInterface + { + return $this->htmlResponse($blog->getTitle()); + } + + public function testFormAction(): ResponseInterface + { + return $this->htmlResponse('testFormAction'); + } + + public function testForwardAction(#[IgnoreValidation] Post $blogPost): ForwardResponse + { + return (new ForwardResponse('testForwardTarget'))->withArguments(['blogPost' => $blogPost]); + } + + public function testForwardTargetAction(Post $blogPost): ResponseInterface + { + return $this->htmlResponse('testForwardTargetAction'); + } + + public function testRelatedObjectAction(Blog $blog, ?Post $blogPost = null): ResponseInterface + { + return $this->htmlResponse('testRelatedObject'); + } + + /** + * Disable the default error flash message, otherwise we get an error because the flash message + * session handling is not available during functional tests. + */ + protected function getErrorFlashMessage(): bool + { + return false; + } + + /** + * @param \Iterator|\TYPO3\CMS\Extbase\DomainObject\AbstractEntity[] $iterator + */ + protected function getStructure(\Iterator|array $iterator): array + { + $structure = []; + + if (!$iterator instanceof \Iterator) { + $iterator = [$iterator]; + } + + foreach ($iterator as $entity) { + $dataMap = $this->dataMapFactory->buildDataMap(get_class($entity)); + $identifier = $dataMap->tableName . ':' . $entity->getUid(); + $properties = ObjectAccess::getGettableProperties($entity); + + $structureItem = []; + foreach ($properties as $propertyName => $propertyValue) { + $columnMap = $dataMap->getColumnMap($propertyName); + if ($columnMap !== null) { + $propertyName = $columnMap->columnName; + } + if ($propertyValue instanceof \Iterator) { + $structureItem[$propertyName] = $this->getStructure($propertyValue); + } else { + $structureItem[$propertyName] = $propertyValue; + } + } + $structure[$identifier] = $structureItem; + } + + return $structure; + } + + protected function getRuntimeIdentifier(): string + { + $arguments = []; + foreach ($this->request->getArguments() as $argumentName => $argumentValue) { + $arguments[] = $argumentName . '=' . $argumentValue; + } + return $this->request->getControllerActionName() . '(' . implode(', ', $arguments) . ')'; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Controller/BlogPostEditingController.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Controller/BlogPostEditingController.php new file mode 100644 index 0000000..0b0fd5e --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Controller/BlogPostEditingController.php @@ -0,0 +1,97 @@ +blogRepository->findAll(); + $this->view->assign('blogs', $blogs); + return $this->htmlResponse(); + } + + public function viewAction(Blog $blog): ResponseInterface + { + $this->view->assign('blog', $blog); + return $this->htmlResponse(); + } + + /** + * Note that we ignore validation intentionally here, so that + * the action can take in a not-validated blog to be able to report + * errors after the errorAction() redirection! + */ + public function editAction(#[IgnoreValidation] Blog $blog): ResponseInterface + { + $categories = $this->categoryRepository->findAll(); + $categoriesSelect = []; + foreach ($categories as $category) { + $categoriesSelect[$category->getUid()] = $category->getTitle(); + } + $this->view->assignMultiple([ + 'categories' => $categories, + 'categoriesSelect' => $categoriesSelect, + 'blog' => $blog, + ]); + return $this->htmlResponse(); + } + + public function newAction(): ResponseInterface + { + $blog = new Blog(); + $this->view->assignMultiple([ + 'blog' => $blog, + 'categories' => $this->categoryRepository->findAll(), + ]); + return $this->htmlResponse(); + } + + public function persistAction(Blog $blog): ResponseInterface + { + // IMPORTANT: This is just an example case for testing purposes. This is missing any + // kind of access control or permission gating. NEVER do this in production without it. + $this->blogRepository->update($blog); + return $this->redirect('list'); + } + + public function createAction(Blog $blog): ResponseInterface + { + // IMPORTANT: This is just an example case for testing purposes. This is missing any + // kind of access control or permission gating. NEVER do this in production without it. + $blog->setPid((int)$this->settings['pidNewRecords']); + $this->blogRepository->add($blog); + $this->persistenceManager->persistAll(); + return $this->redirect('list'); + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Controller/ContentController.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Controller/ContentController.php new file mode 100644 index 0000000..7668dd1 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Controller/ContentController.php @@ -0,0 +1,132 @@ +defaultViewObjectName = JsonView::class; + } + + public function listAction(): ResponseInterface + { + $content = $this->contentRepository->findAll(); + $value = []; + $value[$this->getRuntimeIdentifier()] = $this->getStructure($content); + // this is required, so we don't try to json_encode content of the image + /** @var JsonView $view */ + $view = $this->view; + $view->setConfiguration([ + 'value' => [ + '_descendAll' => [ + '_descendAll' => [ + '_descendAll' => [ + '_descendAll' => [ + '_descendAll' => [ + '_exclude' => ['contents'], + ], + ], + ], + ], + ], + ], + ]); + $view->assign('value', $value); + + return $this->jsonResponse(); + } + + /** + * @throws \RuntimeException + */ + public function processRequest(RequestInterface $request): ResponseInterface + { + try { + return parent::processRequest($request); + } catch (Exception $exception) { + throw new \RuntimeException( + $this->getRuntimeIdentifier() . ': ' . $exception->getMessage() . ' (' . $exception->getCode() . ')', + 1476122223 + ); + } + } + + /** + * @param \Iterator|\TYPO3\CMS\Extbase\DomainObject\AbstractEntity[] $iterator + */ + protected function getStructure(\Iterator|array $iterator): array + { + $structure = []; + + if (!$iterator instanceof \Iterator) { + $iterator = [$iterator]; + } + + foreach ($iterator as $entity) { + $dataMap = $this->dataMapFactory->buildDataMap(get_class($entity)); + $identifier = $dataMap->tableName . ':' . $entity->getUid(); + $properties = ObjectAccess::getGettableProperties($entity); + + $structureItem = []; + foreach ($properties as $propertyName => $propertyValue) { + $columnMap = $dataMap->getColumnMap($propertyName); + if ($columnMap !== null) { + $propertyName = $columnMap->columnName; + } + if ($propertyValue instanceof \Iterator) { + $structureItem[$propertyName] = $this->getStructure($propertyValue); + } else { + $structureItem[$propertyName] = $propertyValue; + } + } + //let's flatten the structure and put file reference properties level up, so we can use StructureHasRecordConstraint + if ( + $entity instanceof FileReference + && isset($structureItem['originalResource']) + && $structureItem['originalResource'] instanceof \TYPO3\CMS\Core\Resource\FileReference + ) { + $structureItem = $structureItem['originalResource']->getProperties(); + } + $structure[$identifier] = $structureItem; + } + + return $structure; + } + + protected function getRuntimeIdentifier(): string + { + $arguments = []; + foreach ($this->request->getArguments() as $argumentName => $argumentValue) { + $arguments[] = $argumentName . '=' . $argumentValue; + } + return $this->request->getControllerActionName() . '(' . implode(', ', $arguments) . ')'; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Administrator.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Administrator.php new file mode 100644 index 0000000..df8fb6e --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Administrator.php @@ -0,0 +1,309 @@ + + */ + protected ObjectStorage $usergroup; + + protected string $name = ''; + + protected string $firstName = ''; + + protected string $middleName = ''; + + protected string $lastName = ''; + + protected string $address = ''; + + protected string $telephone = ''; + + protected string $fax = ''; + + protected string $email = ''; + + protected string $title = ''; + + protected string $zip = ''; + + protected string $city = ''; + + protected string $country = ''; + + protected string $www = ''; + + protected string $company = ''; + + protected ObjectStorage $image; + + protected ?\DateTime $lastlogin; + + public function __construct(string $username = '', string $password = '') + { + $this->username = $username; + $this->password = $password; + $this->initializeObject(); + } + + /** + * Called again with initialize object, as fetching an entity from the DB does not use the constructor + */ + public function initializeObject(): void + { + $this->usergroup = new ObjectStorage(); + $this->image = new ObjectStorage(); + } + + public function setUsername(string $username): void + { + $this->username = $username; + } + + public function getUsername(): string + { + return $this->username; + } + + public function setPassword(string $password): void + { + $this->password = $password; + } + + public function getPassword(): string + { + return $this->password; + } + + /** + * Sets the usergroups. Keep in mind that the property is called "usergroup" + * although it can hold several usergroups. + * + * @param ObjectStorage $usergroup + */ + public function setUsergroup(ObjectStorage $usergroup): void + { + $this->usergroup = $usergroup; + } + + /** + * Adds a usergroup to the frontend user + */ + public function addUsergroup(FrontendUserGroup $usergroup): void + { + $this->usergroup->attach($usergroup); + } + + /** + * Removes a usergroup from the frontend user + */ + public function removeUsergroup(FrontendUserGroup $usergroup): void + { + $this->usergroup->detach($usergroup); + } + + /** + * Returns the usergroups. Keep in mind that the property is called "usergroup" + * although it can hold several usergroups. + * + * @return ObjectStorage + */ + public function getUsergroup(): ObjectStorage + { + return $this->usergroup; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getName(): string + { + return $this->name; + } + + public function setFirstName(string $firstName): void + { + $this->firstName = $firstName; + } + + public function getFirstName(): string + { + return $this->firstName; + } + + public function setMiddleName(string $middleName): void + { + $this->middleName = $middleName; + } + + public function getMiddleName(): string + { + return $this->middleName; + } + + public function setLastName(string $lastName): void + { + $this->lastName = $lastName; + } + + public function getLastName(): string + { + return $this->lastName; + } + + public function setAddress(string $address): void + { + $this->address = $address; + } + + public function getAddress(): string + { + return $this->address; + } + + public function setTelephone(string $telephone): void + { + $this->telephone = $telephone; + } + + public function getTelephone(): string + { + return $this->telephone; + } + + public function setFax(string $fax): void + { + $this->fax = $fax; + } + + public function getFax(): string + { + return $this->fax; + } + + public function setEmail(string $email): void + { + $this->email = $email; + } + + public function getEmail(): string + { + return $this->email; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setZip(string $zip): void + { + $this->zip = $zip; + } + + public function getZip(): string + { + return $this->zip; + } + + public function setCity(string $city): void + { + $this->city = $city; + } + + public function getCity(): string + { + return $this->city; + } + + public function setCountry(string $country): void + { + $this->country = $country; + } + + public function getCountry(): string + { + return $this->country; + } + + public function setWww(string $www): void + { + $this->www = $www; + } + + public function getWww(): string + { + return $this->www; + } + + public function setCompany(string $company): void + { + $this->company = $company; + } + + public function getCompany(): string + { + return $this->company; + } + + /** + * @param ObjectStorage $image + */ + public function setImage(ObjectStorage $image): void + { + $this->image = $image; + } + + /** + * @return ObjectStorage + */ + public function getImage(): ObjectStorage + { + return $this->image; + } + + public function setLastlogin(\DateTime $lastlogin): void + { + $this->lastlogin = $lastlogin; + } + + public function getLastlogin(): ?\DateTime + { + return $this->lastlogin; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Blog.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Blog.php new file mode 100644 index 0000000..d67377e --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Blog.php @@ -0,0 +1,189 @@ + 1, 'maximum' => 80])] + protected string $title = ''; + + /** + * The blog's subtitle + */ + protected ?string $subtitle = null; + + /** + * A short description of the blog + */ + #[Validate(validator: 'StringLength', options: ['minimum' => 1, 'maximum' => 150])] + protected string $description = ''; + + /** + * A logo + * + * @var ObjectStorage + */ + protected ObjectStorage $logo; + + /** + * The posts of this blog + * + * @var ObjectStorage + */ + #[Lazy] + #[Cascade('remove')] + protected ObjectStorage $posts; + + /** + * @var ObjectStorage + */ + protected ObjectStorage $categories; + + /** + * The blog's administrator + */ + #[Lazy] + protected Administrator|LazyLoadingProxy|null $administrator = null; + + public function __construct() + { + $this->posts = new ObjectStorage(); + $this->categories = new ObjectStorage(); + $this->logo = new ObjectStorage(); + } + + public function setSubtitle(?string $subtitle): void + { + $this->subtitle = $subtitle; + } + + public function getSubtitle(): ?string + { + return $this->subtitle; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getTitle(): string + { + return $this->title; + } + + /** + * @param ObjectStorage $logo + */ + public function setLogo(ObjectStorage $logo): void + { + $this->logo = $logo; + } + + /** + * @return ObjectStorage + */ + public function getLogo(): ObjectStorage + { + return $this->logo; + } + + public function setDescription(string $description): void + { + $this->description = $description; + } + + public function getDescription(): string + { + return $this->description; + } + + public function addPost(Post $post): void + { + $this->posts->attach($post); + } + + public function removePost(Post $postToRemove): void + { + $this->posts->detach($postToRemove); + } + + public function removeAllPosts(): void + { + $this->posts = new ObjectStorage(); + } + + /** + * @return ObjectStorage + */ + public function getPosts(): ObjectStorage + { + return $this->posts; + } + + public function addCategory(Category $category): void + { + $this->categories->attach($category); + } + + /** + * @param ObjectStorage $categories + */ + public function setCategories(ObjectStorage $categories): void + { + $this->categories = $categories; + } + + /** + * @return ObjectStorage + */ + public function getCategories(): ObjectStorage + { + return $this->categories; + } + + public function removeCategory(Category $category): void + { + $this->categories->detach($category); + } + + public function setAdministrator(Administrator $administrator): void + { + $this->administrator = $administrator; + } + + public function getAdministrator(): Administrator|LazyLoadingProxy|null + { + return $this->administrator; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Category.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Category.php new file mode 100644 index 0000000..51340b2 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Category.php @@ -0,0 +1,70 @@ +title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getDescription(): string + { + return $this->description; + } + + public function setDescription(string $description): void + { + $this->description = $description; + } + + public function getParent(): ?Category + { + if ($this->parent instanceof LazyLoadingProxy) { + $this->parent->_loadRealInstance(); + } + return $this->parent; + } + + public function setParent(Category $parent): void + { + $this->parent = $parent; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Comment.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Comment.php new file mode 100644 index 0000000..eea6d60 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Comment.php @@ -0,0 +1,98 @@ + 500])] + protected string $content = ''; + + public function __construct() + { + $this->date = new \DateTime(); + } + + public function setDate(\DateTime $date): void + { + $this->date = $date; + } + + public function getDate(): \DateTime + { + return $this->date; + } + + public function setAuthor(string $author): void + { + $this->author = $author; + } + + public function getAuthor(): string + { + return $this->author; + } + + /** + * Sets the author's email for this comment + */ + public function setEmail(string $email): void + { + $this->email = $email; + } + + /** + * Getter for author's email + */ + public function getEmail(): string + { + return $this->email; + } + + public function setContent(string $content): void + { + $this->content = $content; + } + + public function getContent(): string + { + return $this->content; + } + + /** + * Returns this comment as a formatted string + */ + public function __toString(): string + { + return $this->author . ' (' . $this->email . ') said on ' . $this->date->format('Y-m-d') . ':' . chr(10) . + $this->content . chr(10); + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/CustomDate.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/CustomDate.php new file mode 100644 index 0000000..a3a1cd4 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/CustomDate.php @@ -0,0 +1,22 @@ +datetimeText; + } + + public function setDatetimeText(\DateTime $datetimeText): void + { + $this->datetimeText = $datetimeText; + } + + public function getDatetimeInt(): ?\DateTime + { + return $this->datetimeInt; + } + + public function setDatetimeInt(?\DateTime $datetimeInt): void + { + $this->datetimeInt = $datetimeInt; + } + + public function getDatetimeDatetime(): ?\DateTime + { + return $this->datetimeDatetime; + } + + public function setDatetimeDatetime(?\DateTime $datetimeDatetime): void + { + $this->datetimeDatetime = $datetimeDatetime; + } + + public function getCustomDate(): ?CustomDate + { + return $this->customDate; + } + + public function setCustomDate(CustomDate $customDate): void + { + $this->customDate = $customDate; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/DateTimeImmutableExample.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/DateTimeImmutableExample.php new file mode 100644 index 0000000..a93f81d --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/DateTimeImmutableExample.php @@ -0,0 +1,86 @@ +datetimeImmutableText; + } + + public function setDatetimeImmutableText(\DateTimeImmutable $datetimeImmutableText): void + { + $this->datetimeImmutableText = $datetimeImmutableText; + } + + public function getDatetimeImmutableInt(): ?\DateTimeImmutable + { + return $this->datetimeImmutableInt; + } + + public function setDatetimeImmutableInt(\DateTimeImmutable $datetimeImmutableInt): void + { + $this->datetimeImmutableInt = $datetimeImmutableInt; + } + + public function getDatetimeImmutableDatetime(): ?\DateTimeImmutable + { + return $this->datetimeImmutableDatetime; + } + + public function setDatetimeImmutableDatetime(\DateTimeImmutable $datetimeImmutableDatetime): void + { + $this->datetimeImmutableDatetime = $datetimeImmutableDatetime; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Enum/Salutation.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Enum/Salutation.php new file mode 100644 index 0000000..f1387b2 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Enum/Salutation.php @@ -0,0 +1,25 @@ + + */ + protected ObjectStorage $subgroup; + + public function __construct(string $title = '') + { + $this->setTitle($title); + $this->subgroup = new ObjectStorage(); + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setDescription(string $description): void + { + $this->description = $description; + } + + public function getDescription(): string + { + return $this->description; + } + + /** + * Sets the subgroups. Keep in mind that the property is called "subgroup" + * although it can hold several subgroups. + * + * @param ObjectStorage $subgroup An object storage containing the subgroups to add + */ + public function setSubgroup(ObjectStorage $subgroup): void + { + $this->subgroup = $subgroup; + } + + public function addSubgroup(FrontendUserGroup $subgroup): void + { + $this->subgroup->attach($subgroup); + } + + public function removeSubgroup(FrontendUserGroup $subgroup): void + { + $this->subgroup->detach($subgroup); + } + + /** + * Returns the subgroups. Keep in mind that the property is called "subgroup" + * although it can hold several subgroups. + * + * @return ObjectStorage An object storage containing the subgroups + */ + public function getSubgroup(): ObjectStorage + { + return $this->subgroup; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Info.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Info.php new file mode 100644 index 0000000..eb6831e --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Info.php @@ -0,0 +1,46 @@ +name = $name; + } + + public function getName(): string + { + return $this->name; + } + + /** + * Returns this info as a formatted string + */ + public function __toString(): string + { + return $this->name; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/NoTcaEntity.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/NoTcaEntity.php new file mode 100644 index 0000000..7e25c57 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/NoTcaEntity.php @@ -0,0 +1,34 @@ +title = $title; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Person.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Person.php new file mode 100644 index 0000000..a55bfe5 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Person.php @@ -0,0 +1,165 @@ + + */ + protected ObjectStorage $tags; + + /** + * @var ObjectStorage + */ + protected ObjectStorage $tagsSpecial; + + public function __construct(string $firstname = '', string $lastname = '', string $email = '') + { + $this->setFirstname($firstname); + $this->setLastname($lastname); + $this->setEmail($email); + + $this->tags = new ObjectStorage(); + $this->tagsSpecial = new ObjectStorage(); + } + + public function setFirstname(string $firstname): void + { + $this->firstname = $firstname; + } + + public function getFirstname(): string + { + return $this->firstname; + } + + public function setLastname(string $lastname): void + { + $this->lastname = $lastname; + } + + public function getLastname(): string + { + return $this->lastname; + } + + public function getFullName(): string + { + return $this->firstname . ' ' . $this->lastname; + } + + public function setEmail(string $email): void + { + $this->email = $email; + } + + public function getEmail(): string + { + return $this->email; + } + + public function getSalutation(): Enum\Salutation + { + return $this->salutation; + } + + public function setSalutation(Enum\Salutation $salutation): void + { + $this->salutation = $salutation; + } + + /** + * @return ObjectStorage + */ + public function getTags(): ObjectStorage + { + return $this->tags; + } + + /** + * @param ObjectStorage $tags + */ + public function setTags(ObjectStorage $tags): void + { + $this->tags = $tags; + } + + public function addTag(Tag $tag): void + { + $this->tags->attach($tag); + } + + public function removeTag(Tag $tag): void + { + $this->tags->detach($tag); + } + + /** + * @return ObjectStorage + */ + public function getTagsSpecial(): ObjectStorage + { + return $this->tagsSpecial; + } + + /** + * @param ObjectStorage $tagsSpecial + */ + public function setTagsSpecial(ObjectStorage $tagsSpecial): void + { + $this->tagsSpecial = $tagsSpecial; + } + + public function addTagSpecial(Tag $tag): void + { + $this->tagsSpecial->attach($tag); + } + + public function removeTagSpecial(Tag $tag): void + { + $this->tagsSpecial->detach($tag); + } + + public function getCountry(): ?Country + { + return $this->country; + } + + public function setCountry(?Country $country): void + { + $this->country = $country; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Post.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Post.php new file mode 100644 index 0000000..0ea2694 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Post.php @@ -0,0 +1,398 @@ + 3, 'maximum' => 50])] + protected string $title = ''; + + protected \DateTime $date; + + protected ?\DateTime $archiveDate = null; + + protected ?Person $author = null; + + protected ?Person $secondAuthor = null; + + protected ?Person $reviewer = null; + + #[Validate(validator: 'StringLength', options: ['minimum' => 3])] + protected string $content = ''; + + /** + * @var ObjectStorage + */ + protected ObjectStorage $tags; + + /** + * @var ObjectStorage + */ + protected ObjectStorage $categories; + + /** + * @var ObjectStorage + */ + #[Lazy] + #[Cascade('remove')] + protected ObjectStorage $comments; + + /** + * @var ObjectStorage + */ + #[Lazy] + protected ObjectStorage $relatedPosts; + + /** + * 1:1 relation stored as CSV value in this class + */ + protected ?Info $additionalName = null; + + /** + * 1:1 relation stored as foreign key in Info class + */ + protected ?Info $additionalInfo = null; + + /** + * 1:n relation stored as CSV value + * @var ObjectStorage + */ + #[Lazy] + protected ObjectStorage $additionalComments; + + public function __construct() + { + $this->tags = new ObjectStorage(); + $this->categories = new ObjectStorage(); + $this->comments = new ObjectStorage(); + $this->relatedPosts = new ObjectStorage(); + $this->date = new \DateTime(); + $this->additionalComments = new ObjectStorage(); + } + + /** + * Sets the blog this post is part of + */ + public function setBlog(Blog $blog): void + { + $this->blog = $blog; + } + + /** + * Returns the blog this post is part of + */ + public function getBlog(): ?Blog + { + return $this->blog; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setDate(\DateTime $date): void + { + $this->date = $date; + } + + public function getDate(): \DateTime + { + return $this->date; + } + + public function getArchiveDate(): ?\DateTime + { + return $this->archiveDate; + } + + public function setArchiveDate(?\DateTime $archiveDate): void + { + $this->archiveDate = $archiveDate; + } + + /** + * @param ObjectStorage $tags + */ + public function setTags(ObjectStorage $tags): void + { + $this->tags = $tags; + } + + public function addTag(Tag $tag): void + { + $this->tags->attach($tag); + } + + public function removeTag(Tag $tag): void + { + $this->tags->detach($tag); + } + + public function removeAllTags(): void + { + $this->tags = new ObjectStorage(); + } + + /** + * Getter for tags + * Note: We return a clone of the tags because they must not be modified as they are Value Objects + * + * @return ObjectStorage A storage holding objects + */ + public function getTags(): ObjectStorage + { + return clone $this->tags; + } + + public function addCategory(Category $category): void + { + $this->categories->attach($category); + } + + /** + * @param ObjectStorage $categories + */ + public function setCategories(ObjectStorage $categories): void + { + $this->categories = $categories; + } + + /** + * @return ObjectStorage + */ + public function getCategories(): ObjectStorage + { + return $this->categories; + } + + public function removeCategory(Category $category): void + { + $this->categories->detach($category); + } + + public function setAuthor(?Person $author): void + { + $this->author = $author; + } + + public function getAuthor(): ?Person + { + return $this->author; + } + + public function getSecondAuthor(): ?Person + { + return $this->secondAuthor; + } + + public function setSecondAuthor(?Person $secondAuthor): void + { + $this->secondAuthor = $secondAuthor; + } + + public function getReviewer(): ?Person + { + return $this->reviewer; + } + + public function setReviewer(?Person $reviewer): void + { + $this->reviewer = $reviewer; + } + + /** + * Sets the content for this post + * + * @param string $content + */ + public function setContent($content): void + { + $this->content = $content; + } + + /** + * Getter for content + */ + public function getContent(): string + { + return $this->content; + } + + /** + * Setter for the comments to this post + * + * @param ObjectStorage $comments An Object Storage of related Comment instances + */ + public function setComments(ObjectStorage $comments): void + { + $this->comments = $comments; + } + + /** + * Adds a comment to this post + */ + public function addComment(Comment $comment): void + { + $this->comments->attach($comment); + } + + /** + * Removes Comment from this post + */ + public function removeComment(Comment $commentToDelete): void + { + $this->comments->detach($commentToDelete); + } + + /** + * Remove all comments from this post + */ + public function removeAllComments(): void + { + $comments = clone $this->comments; + $this->comments->removeAll($comments); + } + + /** + * Returns the comments to this post + * + * @return ObjectStorage holding instances of Comment + */ + public function getComments(): ObjectStorage + { + return $this->comments; + } + + /** + * Setter for the related posts + * + * @param ObjectStorage $relatedPosts An Object Storage containing related Posts instances + */ + public function setRelatedPosts(ObjectStorage $relatedPosts): void + { + $this->relatedPosts = $relatedPosts; + } + + /** + * Adds a related post + */ + public function addRelatedPost(Post $post): void + { + $this->relatedPosts->attach($post); + } + + /** + * Remove all related posts + */ + public function removeAllRelatedPosts(): void + { + $relatedPosts = clone $this->relatedPosts; + $this->relatedPosts->removeAll($relatedPosts); + } + + /** + * Returns the related posts + * + * @return ObjectStorage holding instances of Post + */ + public function getRelatedPosts(): ObjectStorage + { + return $this->relatedPosts; + } + + public function getAdditionalName(): ?Info + { + return $this->additionalName; + } + + public function setAdditionalName(Info $additionalName): void + { + $this->additionalName = $additionalName; + } + + public function getAdditionalInfo(): ?Info + { + return $this->additionalInfo; + } + + public function setAdditionalInfo(Info $additionalInfo): void + { + $this->additionalInfo = $additionalInfo; + } + + /** + * @return ObjectStorage + */ + public function getAdditionalComments(): ObjectStorage + { + return $this->additionalComments; + } + + /** + * @param ObjectStorage $additionalComments + */ + public function setAdditionalComments(ObjectStorage $additionalComments): void + { + $this->additionalComments = $additionalComments; + } + + public function addAdditionalComment(Comment $comment): void + { + $this->additionalComments->attach($comment); + } + + public function removeAllAdditionalComments(): void + { + $comments = clone $this->additionalComments; + $this->additionalComments->removeAll($comments); + } + + public function removeAdditionalComment(Comment $comment): void + { + $this->additionalComments->detach($comment); + } + + /** + * Returns this post as a formatted string + */ + public function __toString(): string + { + return $this->title . chr(10) . + ' written on ' . $this->date->format('Y-m-d') . chr(10) . + ' by ' . $this->author->getFullName() . chr(10) . + wordwrap($this->content, 70, chr(10)) . chr(10) . + implode(', ', $this->tags->toArray()); + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/RegistryEntry.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/RegistryEntry.php new file mode 100644 index 0000000..7f21607 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/RegistryEntry.php @@ -0,0 +1,38 @@ +name; + } + + public function setName(string $name): void + { + $this->name = $name; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/RestrictedComment.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/RestrictedComment.php new file mode 100644 index 0000000..2633b07 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/RestrictedComment.php @@ -0,0 +1,87 @@ + 500])] + protected string $content = ''; + + // Note: Simple string access, no model relation + protected string $customfegroup = ''; + + protected bool $customhidden = false; + protected ?\DateTime $customstarttime = null; + protected ?\DateTime $customendtime = null; + + public function setContent(string $content): void + { + $this->content = $content; + } + + public function getContent(): string + { + return $this->content; + } + + public function getCustomfegroup(): string + { + return $this->customfegroup; + } + + public function setCustomfegroup(string $customfegroup): void + { + $this->customfegroup = $customfegroup; + } + + public function getCustomhidden(): bool + { + return $this->customhidden; + } + + public function setCustomhidden(bool $customhidden): void + { + $this->customhidden = $customhidden; + } + + public function getCustomstarttime(): ?\DateTime + { + return $this->customstarttime; + } + + public function setCustomstarttime(?\DateTime $customstarttime): void + { + $this->customstarttime = $customstarttime; + } + + public function getCustomendtime(): ?\DateTime + { + return $this->customendtime; + } + + public function setCustomendtime(?\DateTime $customendtime): void + { + $this->customendtime = $customendtime; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Tag.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Tag.php new file mode 100644 index 0000000..f098c41 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Tag.php @@ -0,0 +1,46 @@ +name = $name; + } + + public function getName(): string + { + return $this->name; + } + + /** + * Returns this tag as a formatted string + */ + public function __toString(): string + { + return $this->getName(); + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/TtContent.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/TtContent.php new file mode 100644 index 0000000..7b46ea8 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/TtContent.php @@ -0,0 +1,105 @@ + + */ + #[Lazy] + protected ObjectStorage $image; + + /** + * @var ObjectStorage + */ + protected ObjectStorage $categories; + + public function __construct() + { + $this->image = new ObjectStorage(); + $this->categories = new ObjectStorage(); + } + + public function getHeader(): string + { + return $this->header; + } + + public function setHeader(string $header): void + { + $this->header = $header; + } + + /** + * @return ObjectStorage + */ + public function getImage(): ObjectStorage + { + return $this->image; + } + + /** + * @param ObjectStorage $image + */ + public function setImage(ObjectStorage $image): void + { + $this->image = $image; + } + + public function addCategory(Category $category): void + { + $this->categories->attach($category); + } + + /** + * @param ObjectStorage $categories + */ + public function setCategories(ObjectStorage $categories): void + { + $this->categories = $categories; + } + + /** + * @return ObjectStorage + */ + public function getCategories(): ObjectStorage + { + return $this->categories; + } + + public function removeCategory(Category $category): void + { + $this->categories->detach($category); + } + + /** + * Returns this as a formatted string + */ + public function __toString(): string + { + return $this->getHeader(); + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/TtContentWithCType.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/TtContentWithCType.php new file mode 100644 index 0000000..f7068ac --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/TtContentWithCType.php @@ -0,0 +1,47 @@ +header; + } + + public function setHeader(string $header): void + { + $this->header = $header; + } + + public function getCtype(): string + { + return $this->ctype; + } + + public function setCtype(string $ctype): void + { + $this->ctype = $ctype; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/AdministratorRepository.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/AdministratorRepository.php new file mode 100644 index 0000000..81fc328 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/AdministratorRepository.php @@ -0,0 +1,29 @@ + + */ +class AdministratorRepository extends Repository +{ +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/BlogRepository.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/BlogRepository.php new file mode 100644 index 0000000..300af5f --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/BlogRepository.php @@ -0,0 +1,34 @@ + + */ +class BlogRepository extends Repository +{ + protected $defaultOrderings = [ + 'crdate' => QueryInterface::ORDER_DESCENDING, + 'uid' => QueryInterface::ORDER_DESCENDING, + ]; +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/CategoryRepository.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/CategoryRepository.php new file mode 100644 index 0000000..1bcc36f --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/CategoryRepository.php @@ -0,0 +1,31 @@ + QueryInterface::ORDER_ASCENDING, + ]; +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/DateExampleRepository.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/DateExampleRepository.php new file mode 100644 index 0000000..da4b31e --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/DateExampleRepository.php @@ -0,0 +1,29 @@ + + */ +class DateExampleRepository extends Repository +{ +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/DateTimeImmutableExampleRepository.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/DateTimeImmutableExampleRepository.php new file mode 100644 index 0000000..3fd2d44 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/DateTimeImmutableExampleRepository.php @@ -0,0 +1,29 @@ + + */ +class DateTimeImmutableExampleRepository extends Repository +{ +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/PersonRepository.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/PersonRepository.php new file mode 100644 index 0000000..408cb75 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/PersonRepository.php @@ -0,0 +1,31 @@ + + */ +class PersonRepository extends Repository +{ + protected $defaultOrderings = ['lastname' => QueryInterface::ORDER_ASCENDING]; +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/PostRepository.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/PostRepository.php new file mode 100644 index 0000000..74c5070 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/PostRepository.php @@ -0,0 +1,174 @@ + + */ +class PostRepository extends Repository +{ + protected $defaultOrderings = ['date' => QueryInterface::ORDER_DESCENDING]; + + /** + * Finds all posts by the specified blog + * + * @param Blog $blog The blog the post must refer to + */ + public function findAllByBlog(Blog $blog): QueryResultInterface + { + $query = $this->createQuery(); + return $query + ->matching( + $query->equals('blog', $blog) + ) + ->execute(); + } + + /** + * Finds posts by the specified tag and blog + * + * @param Blog $blog The blog the post must refer to + */ + public function findByTagAndBlog(string $tag, Blog $blog): QueryResultInterface + { + $query = $this->createQuery(); + return $query + ->matching( + $query->logicalAnd( + $query->equals('blog', $blog), + $query->equals('tags.name', $tag) + ) + ) + ->execute(); + } + + /** + * Finds all remaining posts of the blog + */ + public function findRemaining(Post $post): QueryResultInterface + { + $blog = $post->getBlog(); + $query = $this->createQuery(); + return $query + ->matching( + $query->logicalAnd( + $query->equals('blog', $blog), + $query->logicalNot( + $query->equals('uid', $post->getUid()) + ) + ) + ) + ->execute(); + } + + /** + * Finds the previous of the given post + * + * @param Post $post The reference post + */ + public function findPrevious(Post $post): Post + { + $query = $this->createQuery(); + return $query + ->matching( + $query->lessThan('date', $post->getDate()) + ) + ->execute() + ->getFirst(); + } + + /** + * Finds the post next to the given post + * + * @param Post $post The reference post + */ + public function findNext(Post $post): Post + { + $query = $this->createQuery(); + return $query + ->matching( + $query->greaterThan('date', $post->getDate()) + ) + ->execute() + ->getFirst(); + } + + /** + * Finds most recent posts by the specified blog + * + * @param Blog $blog The blog the post must refer to + * @param int $limit The number of posts to return at max + * @return QueryResultInterface The posts + */ + public function findRecentByBlog(Blog $blog, int $limit = 5): QueryResultInterface + { + $query = $this->createQuery(); + return $query + ->matching( + $query->equals('blog', $blog) + ) + ->setLimit((int)$limit) + ->execute(); + } + + /** + * Find posts by category + */ + public function findByCategory(int $categoryUid): QueryResultInterface + { + $query = $this->createQuery(); + return $query + ->matching( + $query->contains('categories', $categoryUid) + ) + ->execute(); + } + + /** + * Find posts by categories (OR combined) + * @param array $categoryUids + */ + public function findByCategories(array $categoryUids): QueryResultInterface + { + $query = $this->createQuery(); + return $query + ->matching( + $query->in('categories.uid', $categoryUids) + ) + ->execute(); + } + + public function findAllSortedByCategory(array $uids): QueryResultInterface + { + $q = $this->createQuery(); + $q->matching($q->in('uid', $uids)); + $q->setOrderings([ + 'categories.title' => QueryInterface::ORDER_ASCENDING, + 'uid' => QueryInterface::ORDER_ASCENDING, + ]); + return $q->execute(); + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/RegistryEntryRepository.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/RegistryEntryRepository.php new file mode 100644 index 0000000..061a3eb --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/RegistryEntryRepository.php @@ -0,0 +1,29 @@ + + */ +class RegistryEntryRepository extends Repository +{ +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/RestrictedCommentRepository.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/RestrictedCommentRepository.php new file mode 100644 index 0000000..bc1db28 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/RestrictedCommentRepository.php @@ -0,0 +1,32 @@ + + */ +class RestrictedCommentRepository extends Repository +{ + protected $defaultOrderings = ['date' => QueryInterface::ORDER_DESCENDING]; +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/TtContentRepository.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/TtContentRepository.php new file mode 100644 index 0000000..eb700a2 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/TtContentRepository.php @@ -0,0 +1,29 @@ + + */ +class TtContentRepository extends Repository +{ +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Validator/PostValidator.php b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Validator/PostValidator.php new file mode 100644 index 0000000..6f9670a --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Validator/PostValidator.php @@ -0,0 +1,43 @@ +getTitle() === '77') { + $error = new Error('Title custom validation failed', 1480872650); + $this->result->forProperty('title')->addError($error); + } + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/Extbase/Persistence/Classes.php b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/Extbase/Persistence/Classes.php new file mode 100644 index 0000000..4945f2c --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/Extbase/Persistence/Classes.php @@ -0,0 +1,44 @@ + [ + 'tableName' => 'fe_users', + 'recordType' => \TYPO3Tests\BlogExample\Domain\Model\Administrator::class, + ], + \TYPO3Tests\BlogExample\Domain\Model\Category::class => [ + 'tableName' => 'sys_category', + ], + \TYPO3Tests\BlogExample\Domain\Model\TtContent::class => [ + 'tableName' => 'tt_content', + 'properties' => [ + 'uid' => [ + 'fieldName' => 'uid', + ], + 'pid' => [ + 'fieldName' => 'pid', + ], + 'header' => [ + 'fieldName' => 'header', + ], + ], + ], + \TYPO3Tests\BlogExample\Domain\Model\TtContentWithCType::class => [ + 'tableName' => 'tt_content', + 'properties' => [ + 'uid' => [ + 'fieldName' => 'uid', + ], + 'pid' => [ + 'fieldName' => 'pid', + ], + 'header' => [ + 'fieldName' => 'header', + ], + ], + ], + \TYPO3Tests\BlogExample\Domain\Model\FrontendUserGroup::class => [ + 'tableName' => 'fe_groups', + ], +]; diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/Services.yaml b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/Services.yaml new file mode 100644 index 0000000..d06fc41 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/Services.yaml @@ -0,0 +1,51 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + TYPO3Tests\BlogExample\: + resource: '../Classes/*' + exclude: + - '../Classes/Command/*' + - '../Classes/Controller/*' + - '../Classes/Domain/*' + + TYPO3Tests\BlogExample\Domain\Repository\AdministratorRepository: + public: true + calls: + - method: injectPersistenceManager + arguments: + - '@TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface' + - method: injectEventDispatcher + arguments: + - '@Psr\EventDispatcher\EventDispatcherInterface' + - method: injectFeatures + arguments: + - '@TYPO3\CMS\Core\Configuration\Features' + + TYPO3Tests\BlogExample\Domain\Repository\BlogRepository: + public: true + calls: + - method: injectPersistenceManager + arguments: + - '@TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface' + - method: injectEventDispatcher + arguments: + - '@Psr\EventDispatcher\EventDispatcherInterface' + - method: injectFeatures + arguments: + - '@TYPO3\CMS\Core\Configuration\Features' + + TYPO3Tests\BlogExample\Domain\Repository\PostRepository: + public: true + calls: + - method: injectPersistenceManager + arguments: + - '@TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface' + - method: injectEventDispatcher + arguments: + - '@Psr\EventDispatcher\EventDispatcherInterface' + - method: injectFeatures + arguments: + - '@TYPO3\CMS\Core\Configuration\Features' diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/Overrides/fe_users.php b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/Overrides/fe_users.php new file mode 100644 index 0000000..f4a54b2 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/Overrides/fe_users.php @@ -0,0 +1,10 @@ + 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:fe_users.tx_extbase_type.TYPO3Tests\BlogExample\Domain\Model\Administrator', 'value' => 'TYPO3Tests\BlogExample\Domain\Model\Administrator']; +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/Overrides/sys_template.php b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/Overrides/sys_template.php new file mode 100644 index 0000000..9568b58 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/Overrides/sys_template.php @@ -0,0 +1,8 @@ + [ + 'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_blog', + 'label' => 'title', + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'versioningWS' => true, + 'languageField' => 'sys_language_uid', + 'transOrigPointerField' => 'l18n_parent', + 'transOrigDiffSourceField' => 'l18n_diffsource', + 'delete' => 'deleted', + 'enablecolumns' => [ + 'disabled' => 'hidden', + 'fe_group' => 'fe_group', + ], + 'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_blog.gif', + ], + 'columns' => [ + 'categories' => [ + 'config' => [ + 'type' => 'category', + ], + ], + 'title' => [ + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_blog.title', + 'config' => [ + 'type' => 'input', + 'size' => 20, + 'required' => true, + 'eval' => 'trim', + ], + ], + 'subtitle' => [ + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.subtitle', + 'config' => [ + 'type' => 'input', + 'size' => 20, + 'eval' => 'trim', + 'nullable' => true, + ], + ], + 'description' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_blog.description', + 'config' => [ + 'type' => 'text', + 'required' => true, + 'rows' => 30, + 'cols' => 80, + ], + ], + 'logo' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_blog.logo', + 'config' => [ + 'type' => 'file', + ], + ], + 'posts' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_blog.posts', + 'config' => [ + 'type' => 'inline', + 'foreign_table' => 'tx_blogexample_domain_model_post', + 'foreign_field' => 'blog', + 'foreign_sortby' => 'sorting', + 'appearance' => [ + 'collapseAll' => 1, + 'expandSingle' => 1, + ], + ], + ], + 'administrator' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_blog.administrator', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'foreign_table' => 'fe_users', + 'foreign_table_where' => "AND {#fe_users}.{#tx_extbase_type}='TYPO3Tests\\\\BlogExample\\\\Domain\\\\Model\\\\Administrator'", + 'items' => [ + ['label' => '--none--', 'value' => 0], + ], + 'fieldControl' => [ + 'editPopup' => [ + 'disabled' => false, + ], + 'addRecord' => [ + 'disabled' => false, + 'options' => [ + 'setValue' => 'prepend', + ], + ], + ], + 'default' => 0, + ], + ], + ], + 'types' => [ + '1' => ['showitem' => 'sys_language_uid, hidden, fe_group, title, description, logo, posts, administrator, categories'], + ], + 'palettes' => [ + '1' => ['showitem' => ''], + ], +]; diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_comment.php b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_comment.php new file mode 100644 index 0000000..9c807e8 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_comment.php @@ -0,0 +1,70 @@ + [ + 'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_comment', + 'label' => 'date', + 'label_alt' => 'author', + 'label_alt_force' => true, + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'delete' => 'deleted', + 'enablecolumns' => [ + 'disabled' => 'hidden', + ], + 'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_comment.gif', + 'versioningWS' => true, + ], + 'columns' => [ + 'date' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_comment.date', + 'config' => [ + 'type' => 'datetime', + 'dbType' => 'datetime', + 'size' => 12, + 'required' => true, + 'default' => time(), + ], + ], + 'author' => [ + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_comment.author', + 'config' => [ + 'type' => 'input', + 'size' => 20, + 'required' => true, + 'eval' => 'trim', + ], + ], + 'email' => [ + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_comment.email', + 'config' => [ + 'type' => 'email', + 'size' => 20, + 'required' => true, + ], + ], + 'content' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_comment.content', + 'config' => [ + 'type' => 'text', + 'rows' => 30, + 'cols' => 80, + ], + ], + 'post' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + ], + 'types' => [ + '1' => ['showitem' => 'hidden, date, author, email, content'], + ], + 'palettes' => [ + '1' => ['showitem' => ''], + ], +]; diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_dateexample.php b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_dateexample.php new file mode 100644 index 0000000..c6cc33f --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_dateexample.php @@ -0,0 +1,59 @@ + [ + 'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_dateexample', + 'label' => 'uid', + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'delete' => 'deleted', + 'enablecolumns' => [ + 'disabled' => 'hidden', + 'fe_group' => 'fe_group', + ], + 'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_dateexample.gif', + ], + 'columns' => [ + 'datetime_text' => [ + 'exclude' => true, + 'label' => 'type=datetime, db=text', + 'config' => [ + 'type' => 'datetime', + ], + ], + 'datetime_int' => [ + 'exclude' => true, + 'label' => 'type=datetime, db=int', + 'config' => [ + 'type' => 'datetime', + ], + ], + 'datetime_datetime' => [ + 'exclude' => true, + 'label' => 'type=datetime, db=datetime', + 'config' => [ + 'type' => 'datetime', + 'dbType' => 'datetime', + 'nullable' => true, + ], + ], + 'custom_date' => [ + 'exclude' => true, + 'label' => 'type=datetime dbType=date', + 'config' => [ + 'type' => 'datetime', + 'dbType' => 'date', + 'format' => 'date', + 'nullable' => true, + ], + ], + ], + 'types' => [ + '1' => ['showitem' => 'datetime_text, datetime_int, datetime_datetime'], + ], + 'palettes' => [ + '1' => ['showitem' => ''], + ], +]; diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_datetimeimmutableexample.php b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_datetimeimmutableexample.php new file mode 100644 index 0000000..eeb14bd --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_datetimeimmutableexample.php @@ -0,0 +1,38 @@ + [ + 'title' => 'DateTimeImmutable Example', + 'label' => 'uid', + 'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_dateexample.gif', + ], + 'columns' => [ + 'datetime_immutable_text' => [ + 'exclude' => true, + 'label' => 'type=datetime, db=text', + 'config' => [ + 'type' => 'datetime', + ], + ], + 'datetime_immutable_int' => [ + 'exclude' => true, + 'label' => 'type=datetime, db=int', + 'config' => [ + 'type' => 'datetime', + ], + ], + 'datetime_immutable_datetime' => [ + 'exclude' => true, + 'label' => 'type=datetime, db=datetime', + 'config' => [ + 'type' => 'datetime', + 'dbType' => 'datetime', + ], + ], + ], + 'types' => [ + '1' => ['showitem' => 'datetime_immutable_text, datetime_immutable_int, datetime_immutable_datetime'], + ], +]; diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_info.php b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_info.php new file mode 100644 index 0000000..fe909c2 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_info.php @@ -0,0 +1,41 @@ + [ + 'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_info', + 'label' => 'name', + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'versioningWS' => true, + 'languageField' => 'sys_language_uid', + 'transOrigPointerField' => 'l18n_parent', + 'transOrigDiffSourceField' => 'l18n_diffsource', + 'delete' => 'deleted', + 'sortby' => 'sorting', + 'enablecolumns' => [ + 'disabled' => 'hidden', + ], + 'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_tag.gif', + ], + 'columns' => [ + 'name' => [ + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_info.name', + 'config' => [ + 'type' => 'input', + 'size' => 20, + 'required' => true, + 'eval' => 'trim', + ], + ], + 'post' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + ], + 'types' => [ + 0 => ['showitem' => 'sys_language_uid, l18n_parent, hidden, name'], + ], +]; diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_person.php b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_person.php new file mode 100644 index 0000000..bce8049 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_person.php @@ -0,0 +1,102 @@ + [ + 'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_person', + 'label' => 'lastname', + 'label_alt' => 'firstname', + 'label_alt_force' => true, + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'versioningWS' => true, + 'languageField' => 'sys_language_uid', + 'transOrigPointerField' => 'l10n_parent', + 'prependAtCopy' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.prependAtCopy', + 'delete' => 'deleted', + 'enablecolumns' => [ + 'disabled' => 'hidden', + ], + 'extbase' => [ + 'enableHistoryTracking' => false, + ], + 'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_person.gif', + ], + 'columns' => [ + 'firstname' => [ + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_person.firstname', + 'config' => [ + 'type' => 'input', + 'size' => 20, + 'required' => true, + 'eval' => 'trim', + ], + ], + 'lastname' => [ + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_person.lastname', + 'config' => [ + 'type' => 'input', + 'size' => 20, + 'required' => true, + 'eval' => 'trim', + ], + ], + 'country' => [ + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_person.country', + 'config' => [ + 'type' => 'country', + 'labelField' => 'iso2', + 'default' => 'AT', + ], + ], + 'email' => [ + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_person.email', + 'config' => [ + 'type' => 'email', + 'size' => 20, + 'required' => true, + ], + ], + 'salutation' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'tags' => [ + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_person.tags', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectMultipleSideBySide', + 'foreign_table' => 'tx_blogexample_domain_model_tag', + 'MM' => 'tx_blogexample_domain_model_tag_mm', + 'MM_match_fields' => [ + 'fieldname' => 'tags', + 'tablenames' => 'tx_blogexample_domain_model_person', + ], + 'MM_opposite_field' => 'items', + ], + ], + 'tags_special' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_person.tags_special', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectMultipleSideBySide', + 'foreign_table' => 'tx_blogexample_domain_model_tag', + 'MM' => 'tx_blogexample_domain_model_tag_mm', + 'MM_match_fields' => [ + 'fieldname' => 'tags_special', + 'tablenames' => 'tx_blogexample_domain_model_person', + ], + 'MM_opposite_field' => 'items', + ], + ], + ], + 'types' => [ + '1' => ['showitem' => 'sys_language_uid, firstname, lastname, country, email, salutation, avatar, tags, tags_special'], + ], + 'palettes' => [ + '1' => ['showitem' => ''], + ], +]; diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php new file mode 100644 index 0000000..bafeb99 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php @@ -0,0 +1,218 @@ + [ + 'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post', + 'label' => 'title', + 'label_alt' => 'author', + 'label_alt_force' => true, + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'versioningWS' => true, + 'languageField' => 'sys_language_uid', + 'transOrigPointerField' => 'l18n_parent', + 'transOrigDiffSourceField' => 'l18n_diffsource', + 'delete' => 'deleted', + 'sortby' => 'sorting', + 'enablecolumns' => [ + 'disabled' => 'hidden', + ], + 'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_post.gif', + ], + 'types' => [ + '1' => ['showitem' => 'sys_language_uid, hidden, blog, title, date, archive_date, author, second_author, content, tags, comments, related_posts, additional_name, additional_info, additional_comments, categories'], + ], + 'columns' => [ + 'categories' => [ + 'config' => [ + 'type' => 'category', + ], + ], + 'blog' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.blog', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'foreign_table' => 'tx_blogexample_domain_model_blog', + ], + ], + 'title' => [ + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.title', + 'config' => [ + 'type' => 'input', + 'size' => 20, + 'required' => true, + 'eval' => 'trim', + ], + ], + 'date' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.date', + 'config' => [ + 'type' => 'datetime', + 'size' => 12, + 'required' => true, + 'default' => time(), + ], + ], + 'archive_date' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.archive_date', + 'config' => [ + 'type' => 'datetime', + 'size' => 12, + ], + ], + 'author' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.author', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + ['label' => '--none--', 'value' => 0], + ], + 'foreign_table' => 'tx_blogexample_domain_model_person', + 'fieldControl' => [ + 'editPopup' => [ + 'disabled' => false, + ], + 'addRecord' => [ + 'disabled' => false, + 'options' => [ + 'setValue' => 'prepend', + ], + ], + ], + 'default' => 0, + ], + ], + 'second_author' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.second_author', + 'config' => [ + 'type' => 'group', + 'allowed' => 'tx_blogexample_domain_model_person', + 'relationship' => 'manyToOne', + 'fieldControl' => [ + 'editPopup' => [ + 'disabled' => false, + ], + 'addRecord' => [ + 'disabled' => false, + ], + 'listModule' => [ + 'disabled' => false, + ], + ], + 'default' => 0, + ], + ], + 'reviewer' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.reviewer', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'foreign_table' => 'tx_blogexample_domain_model_person', + 'fieldControl' => [ + 'editPopup' => [ + 'disabled' => false, + ], + 'addRecord' => [ + 'disabled' => false, + 'options' => [ + 'setValue' => 'prepend', + ], + ], + ], + ], + ], + 'content' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.content', + 'config' => [ + 'type' => 'text', + 'rows' => 30, + 'cols' => 80, + ], + ], + 'tags' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.tags', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectMultipleSideBySide', + 'foreign_table' => 'tx_blogexample_domain_model_tag', + 'MM' => 'tx_blogexample_domain_model_tag_mm', + 'MM_match_fields' => [ + 'fieldname' => 'tags', + 'tablenames' => 'tx_blogexample_domain_model_post', + ], + 'MM_opposite_field' => 'items', + ], + ], + 'comments' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.comments', + 'config' => [ + 'type' => 'inline', + 'foreign_table' => 'tx_blogexample_domain_model_comment', + 'foreign_field' => 'post', + 'foreign_default_sortby' => 'uid desc', + 'size' => 10, + 'autoSizeMax' => 30, + 'multiple' => 0, + 'appearance' => [ + 'collapseAll' => 1, + 'expandSingle' => 1, + ], + ], + ], + 'related_posts' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.related', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectMultipleSideBySide', + 'size' => 10, + 'foreign_table' => 'tx_blogexample_domain_model_post', + 'foreign_table_where' => 'AND ###THIS_UID### != {#tx_blogexample_domain_model_post}.{#uid}', + 'MM' => 'tx_blogexample_post_post_mm', + ], + ], + 'additional_name' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.additional_name', + 'config' => [ + 'type' => 'inline', // this will store the info uid in the additional_name field (CSV) + 'foreign_table' => 'tx_blogexample_domain_model_info', + 'relationship' => 'manyToOne', + 'default' => 0, + ], + ], + 'additional_info' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.additional_info', + 'config' => [ + 'type' => 'inline', // this will store the post uid in the post field of the info table + 'foreign_table' => 'tx_blogexample_domain_model_info', + 'foreign_field' => 'post', + 'relationship' => 'manyToOne', + 'default' => 0, + ], + ], + 'additional_comments' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.additional_comments', + 'config' => [ + 'type' => 'inline', // this will store the comments uids in the additional_comments field (CSV) + 'foreign_table' => 'tx_blogexample_domain_model_comment', + 'maxitems' => 200, + ], + ], + ], +]; diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_registryentry.php b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_registryentry.php new file mode 100644 index 0000000..2d57b61 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_registryentry.php @@ -0,0 +1,14 @@ + [ + 'name' => [ + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_registryentry.name', + 'config' => [ + 'type' => 'input', + ], + ], + ], +]; diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_restrictedcomment.php b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_restrictedcomment.php new file mode 100644 index 0000000..e00f49f --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_restrictedcomment.php @@ -0,0 +1,48 @@ + [ + 'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_restrictedcomment', + 'label' => 'content', + 'tstamp' => 'customtstamp', + 'crdate' => 'customcrdate', + 'delete' => 'customdeleted', + 'languageField' => 'customsyslanguageuid', + 'translationSource' => 'custom_l10182342n_source', + 'transOrigPointerField' => 'custom_l10182342n_parent', + 'transOrigDiffSourceField' => 'custom_l10182342n_diff', + 'type' => 'custom_ctype', + 'enablecolumns' => [ + 'disabled' => 'customhidden', + 'starttime' => 'customstarttime', + 'endtime' => 'customendtime', + 'fe_group' => 'customfegroup', + ], + 'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_comment.gif', + ], + 'columns' => [ + 'content' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_comment.content', + 'config' => [ + 'type' => 'text', + 'rows' => 30, + 'cols' => 80, + ], + ], + 'custom_ctype' => [ + 'config' => [ + 'type' => 'passthrough', + 'default' => '', + ], + ], + ], + 'types' => [ + '1' => ['showitem' => 'customhidden, customstartime, customendtime, customfegroup, content'], + ], + 'palettes' => [ + '1' => ['showitem' => ''], + ], +]; diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_tag.php b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_tag.php new file mode 100644 index 0000000..fed5508 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_tag.php @@ -0,0 +1,57 @@ + [ + 'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_tag', + 'label' => 'name', + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'versioningWS' => true, + 'languageField' => 'sys_language_uid', + 'transOrigPointerField' => 'l18n_parent', + 'transOrigDiffSourceField' => 'l18n_diffsource', + 'delete' => 'deleted', + 'enablecolumns' => [ + 'disabled' => 'hidden', + ], + 'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_tag.gif', + ], + 'columns' => [ + 'name' => [ + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_tag.name', + 'config' => [ + 'type' => 'input', + 'size' => 20, + 'required' => true, + 'eval' => 'trim', + ], + ], + 'items' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_tag.items', + 'config' => [ + 'type' => 'group', + 'allowed' => 'tx_blogexample_domain_model_person,tx_blogexample_domain_model_post', + 'size' => 10, + 'MM' => 'tx_blogexample_domain_model_tag_mm', + 'MM_oppositeUsage' => [ + 'tx_blogexample_domain_model_person' => [ + 'tags', + 'tags_special', + ], + 'tx_blogexample_domain_model_post' => [ + 'tags', + ], + ], + ], + ], + ], + 'types' => [ + '1' => ['showitem' => 'sys_language_uid, hidden, name, items'], + ], + 'palettes' => [ + '1' => ['showitem' => ''], + ], +]; diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TypoScript/Frontend/setup.typoscript b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TypoScript/Frontend/setup.typoscript new file mode 100644 index 0000000..de24936 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TypoScript/Frontend/setup.typoscript @@ -0,0 +1,21 @@ +config { + no_cache = 1 + debug = 1 + admPanel = 0 + disableAllHeaderCode = 1 +} +page = PAGE +page { + typeNum = 0 + 10 < styles.content.get + 20 < styles.content.get + 20 { + select.where = {#colPos}=1 + slide = -1 + } +} +plugin.tx_blogexample { + persistence { + storagePid = 3 + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TypoScript/constants.typoscript b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TypoScript/constants.typoscript new file mode 100644 index 0000000..1eb471f --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TypoScript/constants.typoscript @@ -0,0 +1,10 @@ +plugin.tx_blogexample { + settings { + # cat=plugin.tx_blogexample/a; type=int+; label=Editor FE Usergroup uid:Enter the uid of the FE Usergroup that should be allowed to edit Blogs and Post in the frontend + editorUsergroupUid = 1 + } + persistence { + # cat=plugin.tx_blogexample//a; type=int+; label=Default storage PID + storagePid = + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TypoScript/setup.typoscript b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TypoScript/setup.typoscript new file mode 100644 index 0000000..29686a0 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TypoScript/setup.typoscript @@ -0,0 +1,16 @@ + # Plugin configuration +plugin.tx_blogexample { + settings { + # maximum number of posts to display per page + postsPerPage = 3 + # Editor FE Usergroup uid + editorUsergroupUid = {$plugin.tx_blogexample.settings.editorUsergroupUid} + # Plaintext page type number + plaintextPageType = {$plugin.tx_blogexample.settings.plaintextPageType} + # When creating new entries, where to store records + pidNewRecords = {$plugin.tx_blogexample.persistence.storagePid} + } + persistence { + storagePid = {$plugin.tx_blogexample.persistence.storagePid} + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Language/locallang_db.xlf b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Language/locallang_db.xlf new file mode 100644 index 0000000..d6cd882 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Language/locallang_db.xlf @@ -0,0 +1,167 @@ + + + +
+ + + Blog + + + Blog title + + + Short description + + + + Posts + + + Administrator + + + Post + + + Related to + + + Title + + + Title + + + Date + + + Archive date + + + Author + + + Second author + + + Reviewer + + + Content + + + Votes + + + Published + + + Tags + + + Comments + + + Related posts + + + Additional info (inline 1:1 foreign_field) + + + Additional name (inline 1:1 csv) + + + Additional comments (inline 1:n csv) + + + Person + + + Firstname + + + Lastname + + + Country + + + E-Mail + + + Avatar + + + Tags + + + Special tags + + + Comment + + + Date + + + Author + + + Reviewer + + + Email + + + Content + + + Restricted Comment + + + Disabled (custom) + + + Starttime (custom) + + + Endtime (custom) + + + Fe-Group (custom) + + + Content + + + Registry Entry Key + + + Tag + + + Name + + + Related items + + + Additional Info + + + Additional name + + + Post - parent + + + Date example + + + Blog Admin (BlogExample) + + + + diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Templates/BlogPostEditing/Edit.fluid.html b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Templates/BlogPostEditing/Edit.fluid.html new file mode 100644 index 0000000..706e8d5 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Templates/BlogPostEditing/Edit.fluid.html @@ -0,0 +1,52 @@ +
+ + + + +
+
    + + +
  • + {propertyName}: {propertyError.message} +
  • +
    +
    +
+
+
+
+
+ + +
+ + +
+ +
+ +
    + +
  • #{category.uid} - {category.title}
  • +
    +
+

Currently set:

+
    + +
  • #{category.uid} - {category.title}
  • +
    +
+ + +
+ + +
+ +

+ back to list +

diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Templates/BlogPostEditing/List.fluid.html b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Templates/BlogPostEditing/List.fluid.html new file mode 100644 index 0000000..7291c2f --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Templates/BlogPostEditing/List.fluid.html @@ -0,0 +1,19 @@ +
    + +
  • {blog.title} +
      + +
    • {category.title}
    • +
      +
    + + edit + view +
  • +
    + +
+ +

+ create new +

diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Templates/BlogPostEditing/New.fluid.html b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Templates/BlogPostEditing/New.fluid.html new file mode 100644 index 0000000..6ae44db --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Templates/BlogPostEditing/New.fluid.html @@ -0,0 +1,40 @@ +
+ + + +
+
    + + +
  • + {propertyName}: {propertyError.message} +
  • +
    +
    +
+
+
+
+
+ + +
+ + +
+ +
+ + + +
+ + +
+ +

+ back to list +

diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Templates/BlogPostEditing/View.fluid.html b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Templates/BlogPostEditing/View.fluid.html new file mode 100644 index 0000000..a64d894 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Templates/BlogPostEditing/View.fluid.html @@ -0,0 +1,6 @@ +

{blog.title}

+

#{blog.uid}

+ +

+ back to list +

diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/Extension.svg b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/Extension.svg new file mode 100644 index 0000000..bca7a17 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/Extension.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_blog.gif b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_blog.gif new file mode 100644 index 0000000..e1b396e Binary files /dev/null and b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_blog.gif differ diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_comment.gif b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_comment.gif new file mode 100644 index 0000000..6c46bfe Binary files /dev/null and b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_comment.gif differ diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_dateexample.gif b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_dateexample.gif new file mode 100644 index 0000000..a8af198 Binary files /dev/null and b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_dateexample.gif differ diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_person.gif b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_person.gif new file mode 100644 index 0000000..fb089de Binary files /dev/null and b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_person.gif differ diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_post.gif b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_post.gif new file mode 100644 index 0000000..858ee46 Binary files /dev/null and b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_post.gif differ diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_tag.gif b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_tag.gif new file mode 100644 index 0000000..bc373bf Binary files /dev/null and b/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_tag.gif differ diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/composer.json b/Tests/Functional/Fixtures/Extensions/blog_example/composer.json new file mode 100644 index 0000000..6b1e66f --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/composer.json @@ -0,0 +1,19 @@ +{ + "name": "typo3tests/blog-example", + "type": "typo3-cms-extension", + "description": "A test extension for extbase", + "license": "GPL-2.0-or-later", + "require": { + "typo3/cms-core": "*@dev" + }, + "extra": { + "typo3/cms": { + "extension-key": "blog_example" + } + }, + "autoload": { + "psr-4": { + "TYPO3Tests\\BlogExample\\": "Classes/" + } + } +} diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/ext_emconf.php b/Tests/Functional/Fixtures/Extensions/blog_example/ext_emconf.php new file mode 100644 index 0000000..9652fa6 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/ext_emconf.php @@ -0,0 +1,21 @@ + 'Functional test related extension derived from blog_example', + 'description' => 'A test related extension used to verify various extbase features', + 'category' => 'example', + 'author' => 'TYPO3 core team', + 'author_company' => '', + 'author_email' => '', + 'state' => 'stable', + 'version' => '14.2.0', + 'constraints' => [ + 'depends' => [ + 'typo3' => '14.2.0', + ], + 'conflicts' => [], + 'suggests' => [], + ] +]; diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/ext_localconf.php b/Tests/Functional/Fixtures/Extensions/blog_example/ext_localconf.php new file mode 100644 index 0000000..f417e30 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/ext_localconf.php @@ -0,0 +1,38 @@ + ['list', 'details', 'testSingle', 'testForm', 'testForward', 'testForwardTarget', 'testRelatedObject'], + ], + pluginType: ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT, +); +ExtensionUtility::configurePlugin( + 'BlogExample', + 'Content', + [ + ContentController::class => ['list'], + ], + pluginType: ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT, +); +ExtensionUtility::configurePlugin( + 'BlogExample', + 'BlogPostEditing', + [ + BlogPostEditingController::class => ['list', 'view', 'edit', 'persist', 'new', 'create'], + ], + [ + BlogPostEditingController::class => ['list', 'view', 'edit', 'persist', 'new', 'create'], + ], + pluginType: ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT, +); diff --git a/Tests/Functional/Fixtures/Extensions/blog_example/ext_tables.sql b/Tests/Functional/Fixtures/Extensions/blog_example/ext_tables.sql new file mode 100644 index 0000000..4fd6282 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/blog_example/ext_tables.sql @@ -0,0 +1,23 @@ +# Table for NoTcaEntity - intentionally has no TCA definition to test history tracker fallback +CREATE TABLE tx_blogexample_domain_model_notcaentity ( + uid int(11) NOT NULL AUTO_INCREMENT, + pid int(11) DEFAULT 0 NOT NULL, + title varchar(255) DEFAULT '' NOT NULL, + PRIMARY KEY (uid), + KEY parent (pid) +); + +CREATE TABLE tx_blogexample_domain_model_person ( + # type=passthrough needs manual configuration + salutation varchar(4) DEFAULT '' NOT NULL, +); + +# @deprecated Can be removed as soon as int / native type is enforced +CREATE TABLE tx_blogexample_domain_model_dateexample ( + datetime_text varchar(255) DEFAULT '' NOT NULL, +); + +# @deprecated Can be removed as soon as int / native type is enforced +CREATE TABLE tx_blogexample_domain_model_datetimeimmutableexample ( + datetime_immutable_text varchar(255) DEFAULT '' NOT NULL, +); diff --git a/Tests/Functional/Service/Fixtures/convert_model_to_raw_record.csv b/Tests/Functional/Service/Fixtures/convert_model_to_raw_record.csv new file mode 100644 index 0000000..934fda1 --- /dev/null +++ b/Tests/Functional/Service/Fixtures/convert_model_to_raw_record.csv @@ -0,0 +1,19 @@ +"pages" +,"uid","pid","sorting","title","deleted","perms_everybody" +,1,0,128,"Root",0,15 +fe_users,,,,, +,uid,pid,username,password,name,tx_extbase_type,usergroup,disable,deleted +,1,1,admin,password,John Doe,TYPO3Tests\BlogExample\Domain\Model\Administrator,"1,2",0,0 +fe_groups,,,, +,uid,pid,title,hidden,deleted +,1,1,"Group A",0,0 +,2,1,"Group B",0,0 +tx_blogexample_domain_model_blog,,, +,uid,pid,title,description,posts,categories,administrator,sys_language_uid,l18n_parent,t3ver_oid,t3ver_state,t3ver_wsid +,1,1,"New Blog Post","This is a new blog post.",2,0,1,0,0,0,0,0 +,2,1,"Neuer Blog Post","Dies ist ein neuer Blog Post.",0,0,1,1,1,0,0,0 +,3,1,"New Blog Post (WS)","This is a new blog post (workspace draft).",0,0,1,0,0,1,0,1 +tx_blogexample_domain_model_post,,, +,uid,pid,blog,title,date,hidden,deleted +,1,1,1,"First post for blog 1",1710000000,0,0 +,2,1,1,"Second post for blog 1",1710003600,0,0 diff --git a/Tests/Functional/Service/LocalizationServiceTest.php b/Tests/Functional/Service/LocalizationServiceTest.php new file mode 100644 index 0000000..f1a6bdf --- /dev/null +++ b/Tests/Functional/Service/LocalizationServiceTest.php @@ -0,0 +1,147 @@ +tryTranslation( + 'LLL:EXT:visual_editor/Resources/Private/Language/locallang.xlf:save', + null, + 'en' + ) + ); + } + + public function testTryTranslationUsesBackendUserLanguageWhenNoLanguageIsGiven(): void + { + $backendUser = new BackendUserAuthentication(); + $backendUser->user['lang'] = 'de'; + $GLOBALS['BE_USER'] = $backendUser; + + $subject = new LocalizationService(); + + self::assertSame( + 'Speichern', + $subject->tryTranslation('LLL:EXT:visual_editor/Resources/Private/Language/locallang.xlf:save') + ); + } + + public function testTryTranslationReturnsLabelKeyWhenTranslationNotFound(): void + { + $subject = new LocalizationService(); + + $label = 'LLL:EXT:visual_editor/Resources/Private/Language/locallang.xlf:nonexistent'; + self::assertSame( + $label, + $subject->tryTranslation($label, null, 'en') + ); + } + + public function testTryTranslationWithArgumentsSubstitution(): void + { + $subject = new LocalizationService(); + + self::assertSame( + 'Save 5 changes', + $subject->tryTranslation( + 'LLL:EXT:visual_editor/Resources/Private/Language/locallang.xlf:save.changes', + [5], + 'en' + ) + ); + } + + public function testTryTranslationWithFallback(): void + { + $subject = new LocalizationService(); + + $result = $subject->tryTranslation( + 'LLL:EXT:visual_editor/Resources/Private/Language/locallang.xlf:save', + null, + 'fr', + ); + + self::assertEquals('Save', $result); + } + + public function testTryTranslationReturnsOriginalLabelWhenBackendUserNotSet(): void + { + $subject = new LocalizationService(); + + $result = $subject->tryTranslation( + 'LLL:EXT:visual_editor/Resources/Private/Language/locallang.xlf:save' + ); + + self::assertEquals('Save', $result); + } + + public function testGetBackendUserLanguageReturnsLanguageWhenBackendUserExists(): void + { + $backendUser = new BackendUserAuthentication(); + $backendUser->user['lang'] = 'de'; + $GLOBALS['BE_USER'] = $backendUser; + + $subject = new LocalizationService(); + + self::assertSame('de', $subject->getBackendUserLanguage()); + } + + public function testGetBackendUserLanguageReturnsNullWhenBackendUserNotSet(): void + { + $subject = new LocalizationService(); + + self::assertNull($subject->getBackendUserLanguage()); + } + + public function testGetBackendUserLanguageReturnsNullWhenLanguageNotSetInUser(): void + { + $backendUser = new BackendUserAuthentication(); + $GLOBALS['BE_USER'] = $backendUser; + + $subject = new LocalizationService(); + + self::assertNull($subject->getBackendUserLanguage()); + } + + public function testTryTranslationLoadsMultipleTranslationsWithDifferentKeys(): void + { + $subject = new LocalizationService(); + + self::assertSame( + 'Save', + $subject->tryTranslation('LLL:EXT:visual_editor/Resources/Private/Language/locallang.xlf:save', null, 'en') + ); + + self::assertSame( + 'Saving ...', + $subject->tryTranslation('LLL:EXT:visual_editor/Resources/Private/Language/locallang.xlf:saving', null, 'en') + ); + + self::assertSame( + 'reset changes', + $subject->tryTranslation('LLL:EXT:visual_editor/Resources/Private/Language/locallang.xlf:frontend.resetChanges', null, 'en') + ); + } +} diff --git a/Tests/Functional/Service/ModelToRawRecordServiceTest.php b/Tests/Functional/Service/ModelToRawRecordServiceTest.php new file mode 100644 index 0000000..441cc03 --- /dev/null +++ b/Tests/Functional/Service/ModelToRawRecordServiceTest.php @@ -0,0 +1,144 @@ +importCSVDataSet(__DIR__ . '/Fixtures/convert_model_to_raw_record.csv'); + } + + #[Test] + public function mapsAdministratorToFeUsersAndAddsTypeField(): void + { + $administrator = $this->get(AdministratorRepository::class)->findByUid(1); + self::assertInstanceOf(Administrator::class, $administrator); + + $record = $this->get(ModelToRawRecordService::class)->modelToRawRecord($administrator); + + self::assertSame('fe_users.' . Administrator::class, $record->getFullType()); + self::assertSame('John Doe', $record->get('name')); + + // basic identity + computed properties + self::assertSame($administrator->getUid(), $record->getUid()); + self::assertSame($administrator->getPid(), $record->getPid()); + self::assertSame($administrator->_getProperty('_versionedUid'), $record->getComputedProperties()->getVersionedUid()); + self::assertSame($administrator->_getProperty('_localizedUid'), $record->getComputedProperties()->getLocalizedUid()); + + // record type field should be set for Administrator mapping (Classes.php defines recordType) + self::assertSame(Administrator::class, $record->get('tx_extbase_type')); + + // fe_users doesn't have a language column in the data map -> must not create an empty/null key + self::assertFalse($record->has('sys_language_uid')); + } + + #[Test] + public function mapsBlogScalarsAndLanguageMetadata(): void + { + $blog = $this->get(BlogRepository::class)->findByUid(1); + self::assertInstanceOf(Blog::class, $blog); + + $record = $this->get(ModelToRawRecordService::class)->modelToRawRecord($blog); + + self::assertSame('tx_blogexample_domain_model_blog', $record->getFullType()); + self::assertSame('New Blog Post', $record->get('title')); + self::assertSame('This is a new blog post.', $record->get('description')); + + self::assertSame($blog->getUid(), $record->getUid()); + self::assertSame($blog->getPid(), $record->getPid()); + self::assertSame($blog->_getProperty('_versionedUid'), $record->getComputedProperties()->getVersionedUid()); + self::assertSame($blog->_getProperty('_localizedUid'), $record->getComputedProperties()->getLocalizedUid()); + + // language uid is mapped to sys_language_uid for this table + self::assertSame(0, $record->get('sys_language_uid')); + + // record type field should not be added for Blog (no recordType in mapping) + self::assertFalse($record->has('tx_extbase_type')); + + self::assertFalse($record->has('posts')); + self::assertFalse($record->has('categories')); + self::assertFalse($record->has('administrator')); + self::assertFalse($record->has('logo')); + } + + #[Test] + public function includesNullableScalarsAndSkipsRelationsOnNewBlog(): void + { + $blog = new Blog(); + $blog->setPid(1); + $blog->setTitle('t'); + $blog->setDescription('d'); + $blog->setSubtitle(null); + + $record = $this->get(ModelToRawRecordService::class)->modelToRawRecord($blog); + + self::assertSame('tx_blogexample_domain_model_blog', $record->getFullType()); + self::assertSame(0, $record->getUid()); + self::assertSame(1, $record->getPid()); + self::assertSame('t', $record->get('title')); + self::assertSame('d', $record->get('description')); + self::assertTrue($record->has('subtitle'), 'The subtitle property should be included in the raw record even if it is null, as it is a scalar value and has a column mapping.'); + self::assertNull($record->get('subtitle')); + self::assertFalse($record->has('posts')); + self::assertFalse($record->has('logo')); + self::assertFalse($record->has('categories')); + self::assertTrue($record->has('administrator')); + self::assertNull($record->get('administrator')); + } + + #[Test] + public function throwsForTtContentWithoutRequiredTypeField(): void + { + $content = new TtContent(); + $content->setPid(1); + $content->setHeader('Hello'); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Missing typeField "CType" in record of requested table "tt_content".'); + + $this->get(ModelToRawRecordService::class)->modelToRawRecord($content); + } + + #[Test] + public function mapsPostScalarsAndSkipsRelations(): void + { + $post = $this->get(PostRepository::class)->findByUid(1); + self::assertInstanceOf(Post::class, $post); + + $record = $this->get(ModelToRawRecordService::class)->modelToRawRecord($post); + + self::assertSame('tx_blogexample_domain_model_post', $record->getFullType()); + + self::assertSame($post->getUid(), $record->getUid()); + self::assertSame($post->getPid(), $record->getPid()); + self::assertSame($post->_getProperty('_versionedUid'), $record->getComputedProperties()->getVersionedUid()); + self::assertSame($post->_getProperty('_localizedUid'), $record->getComputedProperties()->getLocalizedUid()); + + self::assertSame('First post for blog 1', $record->get('title')); + self::assertFalse($record->has('date')); + self::assertFalse($record->has('blog')); + self::assertFalse($record->has('categories')); + self::assertFalse($record->has('related_posts')); + } +} diff --git a/Tests/Unit/BackwardsCompatibility/ContentAreaTest.php b/Tests/Unit/BackwardsCompatibility/ContentAreaTest.php new file mode 100644 index 0000000..5aa743a --- /dev/null +++ b/Tests/Unit/BackwardsCompatibility/ContentAreaTest.php @@ -0,0 +1,47 @@ +getColPos()); + self::assertSame('main', $subject->getName()); + self::assertSame( + ['tx_container_parent' => 17], + $subject->getConfiguration() + ); + self::assertSame(['text', 'textmedia'], $subject->getAllowedContentTypes()); + self::assertSame(['shortcut'], $subject->getDisallowedContentTypes()); + } + + #[Test] + public function returnsEmptyListsByDefault(): void + { + $subject = new ContentArea( + colPos: 0, + name: 'empty', + tx_container_parent: 0 + ); + + self::assertSame([], $subject->getAllowedContentTypes()); + self::assertSame([], $subject->getDisallowedContentTypes()); + self::assertSame(['tx_container_parent' => 0], $subject->getConfiguration()); + } +} diff --git a/composer.json b/composer.json index 33209df..3ff6e2c 100644 --- a/composer.json +++ b/composer.json @@ -18,19 +18,23 @@ ], "require": { "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", - "typo3/cms-backend": "^13.4.22 || ^14.2.0 || 14.2.x-dev", - "typo3/cms-core": "^13.4.22 || ^14.2.0 || 14.2.x-dev", - "typo3/cms-extbase": "^13.4.22 || ^14.2.0 || 14.2.x-dev", - "typo3/cms-fluid": "^13.4.22 || ^14.2.0 || 14.2.x-dev", - "typo3/cms-frontend": "^13.4.22 || ^14.2.0 || 14.2.x-dev", - "typo3/cms-rte-ckeditor": "^13.4.22 || ^14.2.0 || 14.2.x-dev" + "typo3/cms-backend": "^13.4.22 || ^14.3.0", + "typo3/cms-core": "^13.4.22 || ^14.3.0", + "typo3/cms-extbase": "^13.4.22 || ^14.3.0", + "typo3/cms-fluid": "^13.4.22 || ^14.3.0", + "typo3/cms-frontend": "^13.4.22 || ^14.3.0", + "typo3/cms-rte-ckeditor": "^13.4.22 || ^14.3.0" }, "require-dev": { "b13/container": "^3.2.3", - "pluswerk/grumphp-config": "^10.2.6", + "phpunit/phpunit": "^11.5.55", + "pluswerk/grumphp-config": "^10.2.7", "saschaegerer/phpstan-typo3": "^2.1.1 || ^3.0.1", "ssch/typo3-rector": "^3.14.1", - "typo3fluid/fluid": "^4.6.0 || 4.6.x-dev || ^5" + "typo3/cms-install": "^13.4.0 || ^14.3.0", + "typo3/cms-workspaces": "^13.4.0 || ^14.3.0", + "typo3/testing-framework": "^9.5.0", + "typo3fluid/fluid": "^4.6.0 || ^5.3.1" }, "suggest": { "wapplersystems/multisite-belogin": "You should install this extension, if you have multiple domains in your TYPO3 installation." @@ -40,6 +44,11 @@ "TYPO3\\CMS\\VisualEditor\\": "Classes/" } }, + "autoload-dev": { + "psr-4": { + "TYPO3\\CMS\\VisualEditor\\Tests\\": "Tests/" + } + }, "config": { "allow-plugins": { "ergebnis/composer-normalize": true, @@ -52,6 +61,7 @@ }, "extra": { "typo3/cms": { + "cms-package-dir": "{$vendor-dir}/typo3/cms", "extension-key": "visual_editor" } }, @@ -66,6 +76,8 @@ "post-autoload-dump": [ "sh -c 'MAJOR=$(typo3 --version | awk '\\''{split($3, v, \".\"); print v[1]}'\\''); ln -sfn \"phpstan-baseline-${MAJOR}.neon\" phpstan-baseline.neon'" ], - "test:js": "node --test $(find Resources/Public/JavaScript -name \"*.test.js\" | sort)" + "test:functional": "./Build/Scripts/runTests.sh -s functional", + "test:js": "node --test $(find Resources/Public/JavaScript -name \"*.test.js\" | sort)", + "test:unit": "phpunit -c Build/phpunit/UnitTests.xml" } } diff --git a/grumphp.yml b/grumphp.yml index 624db91..3f38dcb 100644 --- a/grumphp.yml +++ b/grumphp.yml @@ -1,5 +1,5 @@ imports: - - { resource: vendor/pluswerk/grumphp-config/grumphp.yml } + - { resource: ./vendor/pluswerk/grumphp-config/grumphp.yml } parameters: convention.process_timeout: 240 diff --git a/phpstan-baseline-14.neon b/phpstan-baseline-14.neon index 2f7ab14..f60ad0b 100644 --- a/phpstan-baseline-14.neon +++ b/phpstan-baseline-14.neon @@ -1,5 +1,11 @@ parameters: ignoreErrors: + - + message: '#^Class TYPO3\\CMS\\Core\\Cache\\Backend\\NullBackend does not have a constructor and must be instantiated without any parameters\.$#' + identifier: new.noConstructor + count: 1 + path: Build/phpunit/UnitTestsBootstrap.php + - message: '#^Cannot call method getRequestUri\(\) on TYPO3\\CMS\\Core\\Http\\NormalizedParams\|null\.$#' identifier: method.nonObject diff --git a/phpstan.neon b/phpstan.neon index 95ac665..da04e56 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,6 +8,8 @@ parameters: typo3: requestGetAttributeMapping: pageContext: TYPO3\CMS\Backend\Context\PageContext|null + excludePaths: + - Tests/Functional/Fixtures/Extensions/blog_example services: - diff --git a/rector.php b/rector.php index b4f9302..3630909 100644 --- a/rector.php +++ b/rector.php @@ -33,13 +33,11 @@ [ ...RectorSettings::skip(), - StringableForToStringRector::class, /** * rector should not touch these files */ - //__DIR__ . '/src/Example', - //__DIR__ . '/src/Example.php', + __DIR__ . '/Tests/Functional/Fixtures/Extensions/blog_example', SafeDeclareStrictTypesRector::class => [ __DIR__ . '/ext_emconf.php', ],