From a631cd876ded33bfdaf7b6f364da12d361df01e7 Mon Sep 17 00:00:00 2001 From: Daniel Gohlke Date: Fri, 27 Mar 2026 10:48:21 +0100 Subject: [PATCH 1/5] [TASK] Add LogInterface for orders Relates: #752 --- Build/phpstan-baseline.neon | 53 ------ Classes/Domain/Log/DatabaseWriter.php | 57 ++++++ Classes/Domain/Log/LogService.php | 27 +++ Classes/Domain/Log/LogServiceInterface.php | 12 ++ Classes/Domain/Log/Model/Log.php | 90 ++++++++++ Classes/Domain/Log/Model/LogInterface.php | 49 +++++ .../Domain/Log/Repository/LogRepository.php | 58 ++++++ Classes/Service/MailHandler.php | 64 ++++++- .../12.0/Feature-752-AddLogForOrders.rst | 21 +++ Documentation/Changelog/12.0/Index.rst | 20 +++ Documentation/Changelog/Index.rst | 1 + ext_localconf.php | 167 +++++++++++------- ext_tables.sql | 18 ++ 13 files changed, 519 insertions(+), 118 deletions(-) create mode 100644 Classes/Domain/Log/DatabaseWriter.php create mode 100644 Classes/Domain/Log/LogService.php create mode 100644 Classes/Domain/Log/LogServiceInterface.php create mode 100644 Classes/Domain/Log/Model/Log.php create mode 100644 Classes/Domain/Log/Model/LogInterface.php create mode 100644 Classes/Domain/Log/Repository/LogRepository.php create mode 100644 Documentation/Changelog/12.0/Feature-752-AddLogForOrders.rst create mode 100644 Documentation/Changelog/12.0/Index.rst diff --git a/Build/phpstan-baseline.neon b/Build/phpstan-baseline.neon index 2e11cf9b..d393686c 100644 --- a/Build/phpstan-baseline.neon +++ b/Build/phpstan-baseline.neon @@ -5472,56 +5472,3 @@ parameters: count: 1 path: ../ext_emconf.php - - - message: '#^Cannot access an offset on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''1588829280'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''MAIL'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''SYS'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''cart'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''fluid'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''namespaces'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''partialRootPaths'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''templateRootPaths'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php diff --git a/Classes/Domain/Log/DatabaseWriter.php b/Classes/Domain/Log/DatabaseWriter.php new file mode 100644 index 00000000..f993928c --- /dev/null +++ b/Classes/Domain/Log/DatabaseWriter.php @@ -0,0 +1,57 @@ +getData(); + + $log = $recordData['log'] ?? null; + if (($log instanceof LogInterface) === false) { + return $this; + } + unset($recordData['log']); + + $fieldValues = [ + 'identifier' => $log->getIdentifier(), + 'logLevel' => $log->getLogLevel(), + 'arguments' => $this->jsonEncodeWithThrowable($log->getArguments()), + 'request_id' => $record->getRequestId(), + 'time_micro' => $record->getCreated(), + 'level' => $record->getLevel(), + 'message' => $record->getMessage(), + 'data' => $this->jsonEncodeWithThrowable($recordData), + ]; + + $logRepository = GeneralUtility::makeInstance(LogRepository::class); + $logRepository->insert($fieldValues); + + return $this; + } + + public function jsonEncodeWithThrowable(array $dataToEncode): string + { + $data = ''; + if (!empty($dataToEncode)) { + // Fold an exception into the message, and string-ify it into recordData so it can be jsonified. + if (isset($dataToEncode['exception']) && $dataToEncode['exception'] instanceof Throwable) { + $dataToEncode['exception'] = (string)$dataToEncode['exception']; + } + $data = '- ' . json_encode($dataToEncode); + } + + return $data; + } +} diff --git a/Classes/Domain/Log/LogService.php b/Classes/Domain/Log/LogService.php new file mode 100644 index 00000000..7217d568 --- /dev/null +++ b/Classes/Domain/Log/LogService.php @@ -0,0 +1,27 @@ +logger->log( + $log->getLogLevel(), + $log->getMessage(), + [ + 'log' => $log, + ] + ); + } +} diff --git a/Classes/Domain/Log/LogServiceInterface.php b/Classes/Domain/Log/LogServiceInterface.php new file mode 100644 index 00000000..9d45842e --- /dev/null +++ b/Classes/Domain/Log/LogServiceInterface.php @@ -0,0 +1,12 @@ +logLevel; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getArguments(): array + { + return $this->arguments; + } + +} diff --git a/Classes/Domain/Log/Model/LogInterface.php b/Classes/Domain/Log/Model/LogInterface.php new file mode 100644 index 00000000..1633ba10 --- /dev/null +++ b/Classes/Domain/Log/Model/LogInterface.php @@ -0,0 +1,49 @@ +queryBuilder = $connectionPool + ->getQueryBuilderForTable(self::TABLE_NAME) + ; + } + + public function insert( + array $fieldValues, + ): void { + // for cleanup of table + $fieldValues['crdate'] = time(); + + $queryBuilder = clone $this->queryBuilder; + $queryBuilder + ->insert(self::TABLE_NAME) + ->values($fieldValues) + ->executeStatement() + ; + } + + public function findAllByIdentifier( + string $identifier, + ): array { + $queryBuilder = clone $this->queryBuilder; + $queryBuilder + ->select('*') + ->from(self::TABLE_NAME) + ->where( + $queryBuilder->expr()->eq( + 'identifier', + $queryBuilder->createNamedParameter($identifier) + ) + ) + ; + + return $queryBuilder + ->executeQuery() + ->fetchAllAssociative(); + } + +} diff --git a/Classes/Service/MailHandler.php b/Classes/Service/MailHandler.php index f18a2640..eede674f 100644 --- a/Classes/Service/MailHandler.php +++ b/Classes/Service/MailHandler.php @@ -9,6 +9,9 @@ * LICENSE file that was distributed with this source code. */ +use Exception; +use Extcode\Cart\Domain\Log\LogServiceInterface; +use Extcode\Cart\Domain\Log\Model\Log; use Extcode\Cart\Domain\Model\Cart\Cart; use Extcode\Cart\Domain\Model\Order\AddressInterface; use Extcode\Cart\Domain\Model\Order\Item; @@ -45,7 +48,8 @@ class MailHandler implements SingletonInterface public function __construct( private readonly ConfigurationManagerInterface $configurationManager, private readonly EventDispatcherInterface $eventDispatcher, - private readonly MailerInterface $mailer + private readonly MailerInterface $mailer, + private readonly LogServiceInterface $logService, ) { $this->setPluginSettings(); } @@ -332,7 +336,29 @@ public function sendBuyerMail(Item $orderItem): void $email->setRequest($GLOBALS['TYPO3_REQUEST']); } - $this->mailer->send($email); + try { + $this->mailer->send($email); + $this->logService->write( + Log::info( + (string) $orderItem->getUid(), + 'Mail was send to buyer.', + [ + 'time' => time(), + ] + ) + ); + } catch (Exception $e) { + $this->logService->write( + Log::error( + (string) $orderItem->getUid(), + 'Mail could not send to buyer.', + [ + 'time' => time(), + 'exception' => $e->__toString(), + ] + ) + ); + } } /** @@ -380,7 +406,29 @@ public function sendSellerMail(Item $orderItem): void $email->setRequest($GLOBALS['TYPO3_REQUEST']); } - $this->mailer->send($email); + try { + $this->mailer->send($email); + $this->logService->write( + Log::info( + (string) $orderItem->getUid(), + 'Mail was send to seller.', + [ + 'time' => time(), + ] + ) + ); + } catch (Exception $e) { + $this->logService->write( + Log::error( + (string) $orderItem->getUid(), + 'Mail could not send to seller.', + [ + 'time' => time(), + 'exception' => $e->__toString(), + ] + ) + ); + } } public function addAttachments(string $type, Item $orderItem, FluidEmail $email): void @@ -394,6 +442,16 @@ public function addAttachments(string $type, Item $orderItem, FluidEmail $email) foreach ($attachments as $attachment) { if (file_exists($attachment)) { $email->attachFromPath($attachment); + } else { + $this->logService->write( + Log::warning( + (string) $orderItem->getUid(), + 'Mail could add attachment ' . $attachment . ' to mail.', + [ + 'time' => time(), + ] + ) + ); } } } diff --git a/Documentation/Changelog/12.0/Feature-752-AddLogForOrders.rst b/Documentation/Changelog/12.0/Feature-752-AddLogForOrders.rst new file mode 100644 index 00000000..cc2e5d72 --- /dev/null +++ b/Documentation/Changelog/12.0/Feature-752-AddLogForOrders.rst @@ -0,0 +1,21 @@ +.. include:: ../../Includes.rst.txt + +=========================================== +Feature: #752 - Add LogInterface for orders +=========================================== + +See `Issue 752 `__ + +Description +=========== + +Currently there is no logging for an order. An editor can +change the shipping status, but nobody knows, when this is +happend. + +Impact +====== + +No direct impact. + +.. index:: API diff --git a/Documentation/Changelog/12.0/Index.rst b/Documentation/Changelog/12.0/Index.rst new file mode 100644 index 00000000..dec9057d --- /dev/null +++ b/Documentation/Changelog/12.0/Index.rst @@ -0,0 +1,20 @@ +.. include:: ../../Includes.rst.txt + +12.0 Changes +============ + +**Table of contents** + +.. contents:: + :local: + :depth: 1 + +Features +-------- + +.. toctree:: + :maxdepth: 1 + :titlesonly: + :glob: + + Feature-* diff --git a/Documentation/Changelog/Index.rst b/Documentation/Changelog/Index.rst index f4c73d9d..bc90b960 100644 --- a/Documentation/Changelog/Index.rst +++ b/Documentation/Changelog/Index.rst @@ -10,6 +10,7 @@ ChangeLog :maxdepth: 5 :titlesonly: + 12.0/Index 11.7/Index 11.3/Index 11.1/Index diff --git a/ext_localconf.php b/ext_localconf.php index 3d5add58..426e4017 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -11,73 +11,116 @@ use Extcode\Cart\Controller\Cart\PaymentController; use Extcode\Cart\Controller\Cart\ProductController; use Extcode\Cart\Controller\Cart\ShippingController; +use Extcode\Cart\Domain\Log\DatabaseWriter; +use TYPO3\CMS\Core\Log\LogLevel; +use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Extbase\Utility\ExtensionUtility; -// configure plugins +(static function (string $extKey) { + if (is_array($GLOBALS['TYPO3_CONF_VARS'] ?? null) === false) { + throw new Exception('$GLOBALS[\'TYPO3_CONF_VARS\'] is not an array', 1774601240); + } -ExtensionUtility::configurePlugin( - 'Cart', - 'MiniCart', - [ - CartPreviewController::class => 'show', - CurrencyController::class => 'update', - ], - [ - CartPreviewController::class => 'show', - CurrencyController::class => 'update', - ] -); + ArrayUtility::mergeRecursiveWithOverrule( + $GLOBALS['TYPO3_CONF_VARS'], + [ + 'LOG' => [ + 'Extcode' => [ + 'Cart' => [ + 'Domain' => [ + 'Log' => [ + 'LogService' => [ + 'writerConfiguration' => [ + LogLevel::INFO => [ + DatabaseWriter::class => [], + ], + ], + ], + ], + ], + ], + ], + ], + // view paths for TYPO3 Mail API + 'MAIL' => [ + 'templateRootPaths' => [ + '1588829280' => 'EXT:cart/Resources/Private/Templates/', + ], + 'partialRootPaths' => [ + '1588829280' => 'EXT:cart/Resources/Private/Partials/', + ], + ], + 'SYS' => [ + 'fluid' => [ + 'namespaces' => [ + 'cart' => [ + 1 => 'Extcode\\Cart\\ViewHelpers', + ], + ], + ], + ], + ] + ); -ExtensionUtility::configurePlugin( - 'Cart', - 'Cart', - [ - CartController::class => 'show, clear, update', - CountryController::class => 'update', - CouponController::class => 'add, remove', - CurrencyController::class => 'update', - OrderController::class => 'show, create', - PaymentController::class => 'update', - ProductController::class => 'add, remove', - ShippingController::class => 'update', - ], - [ - CartController::class => 'show, clear, update', - CountryController::class => 'update', - CouponController::class => 'add, remove', - CurrencyController::class => 'update', - OrderController::class => 'show, create', - PaymentController::class => 'update', - ProductController::class => 'add, remove', - ShippingController::class => 'update', - ] -); + // configure plugins + ExtensionUtility::configurePlugin( + 'Cart', + 'MiniCart', + [ + CartPreviewController::class => 'show', + CurrencyController::class => 'update', + ], + [ + CartPreviewController::class => 'show', + CurrencyController::class => 'update', + ] + ); -ExtensionUtility::configurePlugin( - 'Cart', - 'Currency', - [ - CurrencyController::class => 'edit, update', - ], - [ - CurrencyController::class => 'edit, update', - ] -); + ExtensionUtility::configurePlugin( + 'Cart', + 'Cart', + [ + CartController::class => 'show, clear, update', + CountryController::class => 'update', + CouponController::class => 'add, remove', + CurrencyController::class => 'update', + OrderController::class => 'show, create', + PaymentController::class => 'update', + ProductController::class => 'add, remove', + ShippingController::class => 'update', + ], + [ + CartController::class => 'show, clear, update', + CountryController::class => 'update', + CouponController::class => 'add, remove', + CurrencyController::class => 'update', + OrderController::class => 'show, create', + PaymentController::class => 'update', + ProductController::class => 'add, remove', + ShippingController::class => 'update', + ] + ); -ExtensionUtility::configurePlugin( - 'Cart', - 'Order', - [ - \Extcode\Cart\Controller\Order\OrderController::class => 'list, show', - ], - [ - \Extcode\Cart\Controller\Order\OrderController::class => 'list, show', - ] -); + ExtensionUtility::configurePlugin( + 'Cart', + 'Currency', + [ + CurrencyController::class => 'edit, update', + ], + [ + CurrencyController::class => 'edit, update', + ] + ); -// register "cart:" namespace -$GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']['cart'][] = 'Extcode\\Cart\\ViewHelpers'; + ExtensionUtility::configurePlugin( + 'Cart', + 'Order', + [ + \Extcode\Cart\Controller\Order\OrderController::class => 'list, show', + ], + [ + \Extcode\Cart\Controller\Order\OrderController::class => 'list, show', + ] + ); -// view paths for TYPO3 Mail API -$GLOBALS['TYPO3_CONF_VARS']['MAIL']['templateRootPaths']['1588829280'] = 'EXT:cart/Resources/Private/Templates/'; -$GLOBALS['TYPO3_CONF_VARS']['MAIL']['partialRootPaths']['1588829280'] = 'EXT:cart/Resources/Private/Partials/'; +})('cart'); diff --git a/ext_tables.sql b/ext_tables.sql index acc70928..56aeba4f 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -269,3 +269,21 @@ CREATE TABLE tx_cart_domain_model_tag ( INDEX `parent` (pid), INDEX `t3ver_oid` (t3ver_oid,t3ver_wsid), ); + +# +# Table structure for table 'tx_cart_domain_model_log' +# +CREATE TABLE tx_cart_domain_model_log ( + uid int(11) NOT NULL auto_increment, + indentifier char(40) DEFAULT '' NOT NULL, + + request_id varchar(13) DEFAULT '' NOT NULL, + crdate int(11) DEFAULT '0' NOT NULL, + time_micro float DEFAULT '0' NOT NULL, + level varchar(10) DEFAULT 'info' NOT NULL, + message text, + data text, + + PRIMARY KEY (uid), +); + From ffab4a62c23e42bcd79f4b7bc777e1f8fba518b0 Mon Sep 17 00:00:00 2001 From: Daniel Gohlke Date: Fri, 27 Mar 2026 14:02:14 +0100 Subject: [PATCH 2/5] [TASK] Add first test for logging in MailHandler Relates: #752 --- Build/phpstan-baseline.neon | 90 ++++++++++ Classes/Domain/Log/DatabaseWriter.php | 3 +- Classes/Domain/Log/Model/Log.php | 4 +- Classes/Domain/Log/Model/LogInterface.php | 6 +- .../Domain/Log/Repository/LogRepository.php | 5 +- Classes/Service/MailHandler.php | 18 +- Tests/Functional/Service/MailHandlerTest.php | 155 ++++++++++++++++++ ext_tables.sql | 36 ++-- 8 files changed, 281 insertions(+), 36 deletions(-) create mode 100644 Tests/Functional/Service/MailHandlerTest.php diff --git a/Build/phpstan-baseline.neon b/Build/phpstan-baseline.neon index d393686c..a93ad23f 100644 --- a/Build/phpstan-baseline.neon +++ b/Build/phpstan-baseline.neon @@ -1038,6 +1038,96 @@ parameters: count: 1 path: ../Classes/Domain/Finisher/Form/AddToCartFinisherInterface.php + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\DatabaseWriter\:\:jsonEncodeWithThrowable\(\) has parameter \$dataToEncode with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/DatabaseWriter.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\Log\:\:__construct\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/Log.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\Log\:\:error\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/Log.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\Log\:\:getArguments\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/Log.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\Log\:\:info\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/Log.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\Log\:\:notice\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/Log.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\Log\:\:warning\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/Log.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\LogInterface\:\:__construct\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/LogInterface.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\LogInterface\:\:error\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/LogInterface.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\LogInterface\:\:getArguments\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/LogInterface.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\LogInterface\:\:info\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/LogInterface.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\LogInterface\:\:notice\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/LogInterface.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\LogInterface\:\:warning\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/LogInterface.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Repository\\LogRepository\:\:findAllByIdentifier\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Repository/LogRepository.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Repository\\LogRepository\:\:insert\(\) has parameter \$fieldValues with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Repository/LogRepository.php + - message: '#^Method Extcode\\Cart\\Domain\\Model\\Cart\:\:getCart\(\) should return Extcode\\Cart\\Domain\\Model\\Cart\\Cart\|null but returns mixed\.$#' identifier: return.type diff --git a/Classes/Domain/Log/DatabaseWriter.php b/Classes/Domain/Log/DatabaseWriter.php index f993928c..1f01a5b3 100644 --- a/Classes/Domain/Log/DatabaseWriter.php +++ b/Classes/Domain/Log/DatabaseWriter.php @@ -26,12 +26,11 @@ public function writeLog(LogRecord $record): WriterInterface $fieldValues = [ 'identifier' => $log->getIdentifier(), - 'logLevel' => $log->getLogLevel(), + 'message' => $record->getMessage(), 'arguments' => $this->jsonEncodeWithThrowable($log->getArguments()), 'request_id' => $record->getRequestId(), 'time_micro' => $record->getCreated(), 'level' => $record->getLevel(), - 'message' => $record->getMessage(), 'data' => $this->jsonEncodeWithThrowable($recordData), ]; diff --git a/Classes/Domain/Log/Model/Log.php b/Classes/Domain/Log/Model/Log.php index a1e0fd7d..042e923a 100644 --- a/Classes/Domain/Log/Model/Log.php +++ b/Classes/Domain/Log/Model/Log.php @@ -9,7 +9,7 @@ final readonly class Log implements LogInterface { public function __construct( - private LogLevel $logLevel, + private string $logLevel, private string $identifier, private string $message, private array $arguments = [], @@ -67,7 +67,7 @@ public static function error( ); } - public function getLogLevel(): LogLevel + public function getLogLevel(): string { return $this->logLevel; } diff --git a/Classes/Domain/Log/Model/LogInterface.php b/Classes/Domain/Log/Model/LogInterface.php index 1633ba10..45337bd5 100644 --- a/Classes/Domain/Log/Model/LogInterface.php +++ b/Classes/Domain/Log/Model/LogInterface.php @@ -4,12 +4,10 @@ namespace Extcode\Cart\Domain\Log\Model; -use TYPO3\CMS\Core\Log\LogLevel; - interface LogInterface { public function __construct( - LogLevel $logLevel, + string $logLevel, string $identifier, string $message, array $arguments = [], @@ -39,7 +37,7 @@ public static function error( array $arguments = [], ): self; - public function getLogLevel(): LogLevel; + public function getLogLevel(): string; public function getIdentifier(): string; diff --git a/Classes/Domain/Log/Repository/LogRepository.php b/Classes/Domain/Log/Repository/LogRepository.php index 04e11677..c0a129f8 100644 --- a/Classes/Domain/Log/Repository/LogRepository.php +++ b/Classes/Domain/Log/Repository/LogRepository.php @@ -6,16 +6,17 @@ use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\QueryBuilder; +use TYPO3\CMS\Core\Utility\GeneralUtility; final readonly class LogRepository { - public const TABLE_NAME = 'tx_cart_domain_model_log'; + public const TABLE_NAME = 'tx_cart_domain_model_order_log'; private QueryBuilder $queryBuilder; public function __construct( - ConnectionPool $connectionPool, ) { + $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); $this->queryBuilder = $connectionPool ->getQueryBuilderForTable(self::TABLE_NAME) ; diff --git a/Classes/Service/MailHandler.php b/Classes/Service/MailHandler.php index eede674f..5a58d244 100644 --- a/Classes/Service/MailHandler.php +++ b/Classes/Service/MailHandler.php @@ -314,7 +314,7 @@ public function sendBuyerMail(Item $orderItem): void ->from($fromAddress) ->setTemplate('Mail/' . ucfirst($status) . '/Buyer') ->format(FluidEmail::FORMAT_HTML) - ->assign('settings', $this->pluginSettings['settings']) + ->assign('settings', $this->pluginSettings['settings'] ?? []) ->assign('cart', $this->cart) ->assign('orderItem', $orderItem); @@ -332,7 +332,7 @@ public function sendBuyerMail(Item $orderItem): void $this->addAttachments('buyer', $orderItem, $email); - if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) { + if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface) { $email->setRequest($GLOBALS['TYPO3_REQUEST']); } @@ -340,7 +340,7 @@ public function sendBuyerMail(Item $orderItem): void $this->mailer->send($email); $this->logService->write( Log::info( - (string) $orderItem->getUid(), + (string)$orderItem->getUid(), 'Mail was send to buyer.', [ 'time' => time(), @@ -350,7 +350,7 @@ public function sendBuyerMail(Item $orderItem): void } catch (Exception $e) { $this->logService->write( Log::error( - (string) $orderItem->getUid(), + (string)$orderItem->getUid(), 'Mail could not send to buyer.', [ 'time' => time(), @@ -382,7 +382,7 @@ public function sendSellerMail(Item $orderItem): void ->from($fromAddress) ->setTemplate('Mail/' . ucfirst($status) . '/Seller') ->format(FluidEmail::FORMAT_HTML) - ->assign('settings', $this->pluginSettings['settings']) + ->assign('settings', $this->pluginSettings['settings'] ?? []) ->assign('cart', $this->cart) ->assign('orderItem', $orderItem); @@ -402,7 +402,7 @@ public function sendSellerMail(Item $orderItem): void $this->addAttachments('seller', $orderItem, $email); - if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) { + if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface) { $email->setRequest($GLOBALS['TYPO3_REQUEST']); } @@ -410,7 +410,7 @@ public function sendSellerMail(Item $orderItem): void $this->mailer->send($email); $this->logService->write( Log::info( - (string) $orderItem->getUid(), + (string)$orderItem->getUid(), 'Mail was send to seller.', [ 'time' => time(), @@ -420,7 +420,7 @@ public function sendSellerMail(Item $orderItem): void } catch (Exception $e) { $this->logService->write( Log::error( - (string) $orderItem->getUid(), + (string)$orderItem->getUid(), 'Mail could not send to seller.', [ 'time' => time(), @@ -445,7 +445,7 @@ public function addAttachments(string $type, Item $orderItem, FluidEmail $email) } else { $this->logService->write( Log::warning( - (string) $orderItem->getUid(), + (string)$orderItem->getUid(), 'Mail could add attachment ' . $attachment . ' to mail.', [ 'time' => time(), diff --git a/Tests/Functional/Service/MailHandlerTest.php b/Tests/Functional/Service/MailHandlerTest.php new file mode 100644 index 00000000..949c4f08 --- /dev/null +++ b/Tests/Functional/Service/MailHandlerTest.php @@ -0,0 +1,155 @@ +testExtensionsToLoad[] = 'extcode/cart'; + $this->testExtensionsToLoad[] = 'typo3conf/ext/cart/Tests/Fixtures/cart_example'; + + $this->configurationToUseInTestInstance = [ + 'LOG' => [ + 'Extcode' => [ + 'Cart' => [ + 'Tests' => [ + 'writerConfiguration' => [ + LogLevel::INFO => [ + DatabaseWriter::class => [], + ], + ], + ], + ], + ], + ], + ]; + + parent::setUp(); + + $this->importPHPDataSet(__DIR__ . '/../../Fixtures/BaseDatabase.php'); + } + + #[Test] + public function logSucessAfterEmailWasSend(): void + { + $configurationManager = self::createStub(ConfigurationManagerInterface::class); + + $eventDispatcher = self::createStub(EventDispatcherInterface::class); + + $mailer = self::createStub(MailerInterface::class); + + $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); + $logService = GeneralUtility::makeInstance( + LogService::class, + $logger, + ); + + $mockBuilder = $this->getMockBuilder(MailHandler::class); + $mockBuilder->setConstructorArgs( + [ + $configurationManager, + $eventDispatcher, + $mailer, + $logService, + ] + ); + $mockBuilder->onlyMethods( + [ + 'getBuyerEmailFrom', + 'getBuyerEmailName', + ] + ); + $mailHandler = $mockBuilder->getMock(); + $mailHandler->method('getBuyerEmailFrom')->willReturn('buyerEmailFrom@example.com'); + $mailHandler->method('getBuyerEmailName')->willReturn('Buyer Email Name'); + + $billingAddress = self::createStub(BillingAddress::class); + $billingAddress->method('getEmail')->willReturn('billingAddress@example.com'); + + $payment = self::createStub(Payment::class); + $payment->method('getStatus')->willReturn('open'); + $orderItem = self::createStub(OrderItem::class); + $orderItem->method('getBillingAddress')->willReturn($billingAddress); + $orderItem->method('getPayment')->willReturn($payment); + $orderItem->method('getUid')->willReturn(142); + + $mailHandler->sendBuyerMail( + $orderItem + ); + + $logEntries = $this->getAllRecords('tx_cart_domain_model_order_log'); + self::assertCount( + 1, + $logEntries + ); + self::assertIsArray($logEntries[0]); + self::assertArrayHasKey( + 'level', + $logEntries[0] + ); + self::assertSame( + 'info', + $logEntries[0]['level'] + ); + self::assertArrayHasKey( + 'identifier', + $logEntries[0] + ); + self::assertSame( + '142', + $logEntries[0]['identifier'] + ); + self::assertArrayHasKey( + 'message', + $logEntries[0] + ); + self::assertSame( + 'Mail was send to buyer.', + $logEntries[0]['message'] + ); + self::assertArrayHasKey( + 'arguments', + $logEntries[0] + ); + self::assertIsString( + $logEntries[0]['arguments'] + ); + $arguments = json_decode( + ltrim($logEntries[0]['arguments'], '- '), + true + ); + self::assertIsArray( + $arguments + ); + self::assertArrayHasKey( + 'time', + $arguments + ); + } +} diff --git a/ext_tables.sql b/ext_tables.sql index 56aeba4f..8a32bf21 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -237,6 +237,25 @@ CREATE TABLE tx_cart_domain_model_coupon ( INDEX `parent` (pid), INDEX `t3ver_oid` (t3ver_oid,t3ver_wsid), ); +# +# Table structure for table 'tx_cart_domain_model_order_log' +# +CREATE TABLE tx_cart_domain_model_order_log ( + uid int(11) NOT NULL auto_increment, + identifier char(40) DEFAULT '' NOT NULL, + + request_id varchar(13) DEFAULT '' NOT NULL, + crdate int(11) DEFAULT '0' NOT NULL, + time_micro float DEFAULT '0' NOT NULL, + level varchar(10) DEFAULT 'info' NOT NULL, + message text, + data text, + + arguments mediumtext, + + PRIMARY KEY (uid), +); + # # Table structure for table 'tx_cart_domain_model_cart' # @@ -270,20 +289,3 @@ CREATE TABLE tx_cart_domain_model_tag ( INDEX `parent` (pid), INDEX `t3ver_oid` (t3ver_oid,t3ver_wsid), ); -# -# Table structure for table 'tx_cart_domain_model_log' -# -CREATE TABLE tx_cart_domain_model_log ( - uid int(11) NOT NULL auto_increment, - indentifier char(40) DEFAULT '' NOT NULL, - - request_id varchar(13) DEFAULT '' NOT NULL, - crdate int(11) DEFAULT '0' NOT NULL, - time_micro float DEFAULT '0' NOT NULL, - level varchar(10) DEFAULT 'info' NOT NULL, - message text, - data text, - - PRIMARY KEY (uid), -); - From 9fbb76810c56428bdca094ae8359473a55c3731a Mon Sep 17 00:00:00 2001 From: Daniel Gohlke Date: Sat, 28 Mar 2026 19:28:52 +0100 Subject: [PATCH 3/5] [TASK] Add own LogLevel enum and more tests Relates: #752 --- .../Backend/Order/PaymentController.php | 1 + .../Backend/Order/ShippingController.php | 1 + Classes/Domain/Log/DatabaseWriter.php | 6 +- Classes/Domain/Log/LogService.php | 2 +- Classes/Domain/Log/Model/Log.php | 42 ++- Classes/Domain/Log/Model/LogInterface.php | 23 +- Classes/Domain/Log/Model/LogLevel.php | 19 ++ Classes/Service/MailHandler.php | 30 +- Tests/Functional/Service/MailHandlerTest.php | 319 +++++++++++++++--- ext_tables.sql | 4 +- 10 files changed, 370 insertions(+), 77 deletions(-) create mode 100644 Classes/Domain/Log/Model/LogLevel.php diff --git a/Classes/Controller/Backend/Order/PaymentController.php b/Classes/Controller/Backend/Order/PaymentController.php index 76dc559c..d7be1247 100644 --- a/Classes/Controller/Backend/Order/PaymentController.php +++ b/Classes/Controller/Backend/Order/PaymentController.php @@ -26,6 +26,7 @@ public function __construct( public function updateAction(Payment $payment): ResponseInterface { + // todo: add logging here $this->paymentRepository->update($payment); $event = new UpdateServiceEvent($payment); diff --git a/Classes/Controller/Backend/Order/ShippingController.php b/Classes/Controller/Backend/Order/ShippingController.php index f2cfed3e..1ad1cc58 100644 --- a/Classes/Controller/Backend/Order/ShippingController.php +++ b/Classes/Controller/Backend/Order/ShippingController.php @@ -26,6 +26,7 @@ public function __construct( public function updateAction(Shipping $shipping): ResponseInterface { + // todo: add logging here $this->shippingRepository->update($shipping); $event = new UpdateServiceEvent($shipping); diff --git a/Classes/Domain/Log/DatabaseWriter.php b/Classes/Domain/Log/DatabaseWriter.php index 1f01a5b3..2aba1e54 100644 --- a/Classes/Domain/Log/DatabaseWriter.php +++ b/Classes/Domain/Log/DatabaseWriter.php @@ -25,8 +25,10 @@ public function writeLog(LogRecord $record): WriterInterface unset($recordData['log']); $fieldValues = [ - 'identifier' => $log->getIdentifier(), - 'message' => $record->getMessage(), + 'log_level' => $log->getLogLevel()->value, + 'item' => $log->getOrderItemId(), + 'type' => $log->getType(), + 'message' => $log->getMessage(), 'arguments' => $this->jsonEncodeWithThrowable($log->getArguments()), 'request_id' => $record->getRequestId(), 'time_micro' => $record->getCreated(), diff --git a/Classes/Domain/Log/LogService.php b/Classes/Domain/Log/LogService.php index 7217d568..167c4ec5 100644 --- a/Classes/Domain/Log/LogService.php +++ b/Classes/Domain/Log/LogService.php @@ -17,7 +17,7 @@ public function write( LogInterface $log ): void { $this->logger->log( - $log->getLogLevel(), + $log->getLogLevel()->value, $log->getMessage(), [ 'log' => $log, diff --git a/Classes/Domain/Log/Model/Log.php b/Classes/Domain/Log/Model/Log.php index 042e923a..7620a5e9 100644 --- a/Classes/Domain/Log/Model/Log.php +++ b/Classes/Domain/Log/Model/Log.php @@ -4,77 +4,89 @@ namespace Extcode\Cart\Domain\Log\Model; -use TYPO3\CMS\Core\Log\LogLevel; - final readonly class Log implements LogInterface { public function __construct( - private string $logLevel, - private string $identifier, + private LogLevel $logLevel, + private int $orderItemId, + private string $type, private string $message, private array $arguments = [], ) {} public static function info( - string $identifier, + int $orderItemId, + string $type, string $message, array $arguments = [], ): self { return new self( LogLevel::INFO, - $identifier, + $orderItemId, + $type, $message, $arguments, ); } public static function notice( - string $identifier, + int $orderItemId, + string $type, string $message, array $arguments = [], ): self { return new self( LogLevel::NOTICE, - $identifier, + $orderItemId, + $type, $message, $arguments, ); } public static function warning( - string $identifier, + int $orderItemId, + string $type, string $message, array $arguments = [], ): self { return new self( LogLevel::WARNING, - $identifier, + $orderItemId, + $type, $message, $arguments, ); } public static function error( - string $identifier, + int $orderItemId, + string $type, string $message, array $arguments = [], ): self { return new self( LogLevel::ERROR, - $identifier, + $orderItemId, + $type, $message, $arguments, ); } - public function getLogLevel(): string + public function getLogLevel(): LogLevel { return $this->logLevel; } - public function getIdentifier(): string + public function getOrderItemId(): int + { + return $this->orderItemId; + } + + public function getType(): string { - return $this->identifier; + return $this->type; } public function getMessage(): string diff --git a/Classes/Domain/Log/Model/LogInterface.php b/Classes/Domain/Log/Model/LogInterface.php index 45337bd5..bb4ce8a8 100644 --- a/Classes/Domain/Log/Model/LogInterface.php +++ b/Classes/Domain/Log/Model/LogInterface.php @@ -7,39 +7,46 @@ interface LogInterface { public function __construct( - string $logLevel, - string $identifier, + LogLevel $logLevel, + int $orderItemId, + string $type, string $message, array $arguments = [], ); public static function info( - string $identifier, + int $orderItemId, + string $type, string $message, array $arguments = [], ): self; public static function notice( - string $identifier, + int $orderItemId, + string $type, string $message, array $arguments = [], ): self; public static function warning( - string $identifier, + int $orderItemId, + string $type, string $message, array $arguments = [], ): self; public static function error( - string $identifier, + int $orderItemId, + string $type, string $message, array $arguments = [], ): self; - public function getLogLevel(): string; + public function getLogLevel(): LogLevel; - public function getIdentifier(): string; + public function getOrderItemId(): int; + + public function getType(): string; public function getMessage(): string; diff --git a/Classes/Domain/Log/Model/LogLevel.php b/Classes/Domain/Log/Model/LogLevel.php new file mode 100644 index 00000000..e5fe3667 --- /dev/null +++ b/Classes/Domain/Log/Model/LogLevel.php @@ -0,0 +1,19 @@ +mailer->send($email); $this->logService->write( Log::info( - (string)$orderItem->getUid(), + $this->getOrderItemUid($orderItem), + 'sendBuyerMail', 'Mail was send to buyer.', [ 'time' => time(), @@ -350,7 +352,8 @@ public function sendBuyerMail(Item $orderItem): void } catch (Exception $e) { $this->logService->write( Log::error( - (string)$orderItem->getUid(), + $this->getOrderItemUid($orderItem), + 'sendBuyerMail', 'Mail could not send to buyer.', [ 'time' => time(), @@ -366,6 +369,9 @@ public function sendBuyerMail(Item $orderItem): void */ public function sendSellerMail(Item $orderItem): void { + if (is_null($orderItem->getUid())) { + throw new InvalidArgumentException('Method should only called for persisted orders.', 1774715307); + } $sellerEmailTo = $this->getSellerEmailTo(); if (empty($this->getSellerEmailFrom()) || empty($sellerEmailTo)) { return; @@ -410,7 +416,8 @@ public function sendSellerMail(Item $orderItem): void $this->mailer->send($email); $this->logService->write( Log::info( - (string)$orderItem->getUid(), + $this->getOrderItemUid($orderItem), + 'sendSellerMail', 'Mail was send to seller.', [ 'time' => time(), @@ -420,7 +427,8 @@ public function sendSellerMail(Item $orderItem): void } catch (Exception $e) { $this->logService->write( Log::error( - (string)$orderItem->getUid(), + $this->getOrderItemUid($orderItem), + 'sendSellerMail', 'Mail could not send to seller.', [ 'time' => time(), @@ -445,7 +453,8 @@ public function addAttachments(string $type, Item $orderItem, FluidEmail $email) } else { $this->logService->write( Log::warning( - (string)$orderItem->getUid(), + $this->getOrderItemUid($orderItem), + 'addAttachments', 'Mail could add attachment ' . $attachment . ' to mail.', [ 'time' => time(), @@ -456,4 +465,15 @@ public function addAttachments(string $type, Item $orderItem, FluidEmail $email) } } } + + private function getOrderItemUid(Item $orderItem): int + { + $orderItemUid = $orderItem->getUid(); + + if (is_null($orderItemUid)) { + throw new InvalidArgumentException('Method should only called for persisted orders.', 1774715307); + } + + return $orderItemUid; + } } diff --git a/Tests/Functional/Service/MailHandlerTest.php b/Tests/Functional/Service/MailHandlerTest.php index 949c4f08..cf1b3eb9 100644 --- a/Tests/Functional/Service/MailHandlerTest.php +++ b/Tests/Functional/Service/MailHandlerTest.php @@ -10,6 +10,7 @@ */ use Codappix\Typo3PhpDatasets\TestingFramework; +use Exception; use Extcode\Cart\Domain\Log\DatabaseWriter; use Extcode\Cart\Domain\Log\LogService; use Extcode\Cart\Domain\Model\Order\BillingAddress; @@ -17,6 +18,8 @@ use Extcode\Cart\Domain\Model\Order\Payment; use Extcode\Cart\Service\MailHandler; use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\MockObject\MockBuilder; +use PHPUnit\Framework\MockObject\Stub; use Psr\EventDispatcher\EventDispatcherInterface; use TYPO3\CMS\Core\Log\LogLevel; use TYPO3\CMS\Core\Log\LogManager; @@ -56,29 +59,69 @@ public function setUp(): void } #[Test] - public function logSucessAfterEmailWasSend(): void + public function logSucessAfterEmailToBuyerWasSend(): void { - $configurationManager = self::createStub(ConfigurationManagerInterface::class); - - $eventDispatcher = self::createStub(EventDispatcherInterface::class); + $mockBuilder = $this->getMockBuilderForMailHandlerClass(); + $mockBuilder->onlyMethods( + [ + 'getBuyerEmailFrom', + 'getBuyerEmailName', + ] + ); + $mailHandler = $mockBuilder->getMock(); + $mailHandler->method('getBuyerEmailFrom')->willReturn('buyerEmailFrom@example.com'); + $mailHandler->method('getBuyerEmailName')->willReturn('Buyer Email Name'); - $mailer = self::createStub(MailerInterface::class); + $mailHandler->sendBuyerMail( + $this->createStubForOrderItem() + ); - $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); - $logService = GeneralUtility::makeInstance( - LogService::class, - $logger, + $logEntries = $this->getAllRecords('tx_cart_domain_model_order_log'); + self::assertCount( + 1, + $logEntries ); + self::assertIsArray($logEntries[0]); - $mockBuilder = $this->getMockBuilder(MailHandler::class); - $mockBuilder->setConstructorArgs( + self::assertArrayIsEqualToArrayIgnoringListOfKeys( [ - $configurationManager, - $eventDispatcher, - $mailer, - $logService, + 'log_level' => 'info', + 'item' => 142, + 'type' => 'sendBuyerMail', + 'message' => 'Mail was send to buyer.', + 'level' => 'info', + ], + $logEntries[0], + [ + 'uid', + 'request_id', + 'crdate', + 'time_micro', + 'data', + 'arguments', ] ); + + self::assertIsString( + $logEntries[0]['arguments'] + ); + $arguments = json_decode( + ltrim($logEntries[0]['arguments'], '- '), + true + ); + self::assertIsArray( + $arguments + ); + self::assertArrayHasKey( + 'time', + $arguments + ); + } + + #[Test] + public function logErrorIfEmailToBuyerCouldNotSend(): void + { + $mockBuilder = $this->getMockBuilderForMailHandlerClass(mailerThrowException: true); $mockBuilder->onlyMethods( [ 'getBuyerEmailFrom', @@ -89,18 +132,8 @@ public function logSucessAfterEmailWasSend(): void $mailHandler->method('getBuyerEmailFrom')->willReturn('buyerEmailFrom@example.com'); $mailHandler->method('getBuyerEmailName')->willReturn('Buyer Email Name'); - $billingAddress = self::createStub(BillingAddress::class); - $billingAddress->method('getEmail')->willReturn('billingAddress@example.com'); - - $payment = self::createStub(Payment::class); - $payment->method('getStatus')->willReturn('open'); - $orderItem = self::createStub(OrderItem::class); - $orderItem->method('getBillingAddress')->willReturn($billingAddress); - $orderItem->method('getPayment')->willReturn($payment); - $orderItem->method('getUid')->willReturn(142); - $mailHandler->sendBuyerMail( - $orderItem + $this->createStubForOrderItem() ); $logEntries = $this->getAllRecords('tx_cart_domain_model_order_log'); @@ -109,34 +142,165 @@ public function logSucessAfterEmailWasSend(): void $logEntries ); self::assertIsArray($logEntries[0]); - self::assertArrayHasKey( - 'level', - $logEntries[0] + + self::assertArrayIsEqualToArrayIgnoringListOfKeys( + [ + 'log_level' => 'error', + 'item' => 142, + 'type' => 'sendBuyerMail', + 'message' => 'Mail could not send to buyer.', + 'level' => 'error', + ], + $logEntries[0], + [ + 'uid', + 'request_id', + 'crdate', + 'time_micro', + 'data', + 'arguments', + ] ); - self::assertSame( - 'info', - $logEntries[0]['level'] + + self::assertIsString( + $logEntries[0]['arguments'] ); - self::assertArrayHasKey( - 'identifier', - $logEntries[0] + $arguments = json_decode( + ltrim($logEntries[0]['arguments'], '- '), + true + ); + self::assertIsArray( + $arguments ); - self::assertSame( - '142', - $logEntries[0]['identifier'] + self::assertArrayHasKey( + 'time', + $arguments ); self::assertArrayHasKey( - 'message', - $logEntries[0] + 'exception', + $arguments + ); + self::assertIsString( + $arguments['exception'] + ); + self::assertStringStartsWith( + 'Exception in ', + $arguments['exception'] + ); + self::assertStringContainsString( + '/cart/Tests/Functional/Service/MailHandlerTest.php', + $arguments['exception'] ); - self::assertSame( - 'Mail was send to buyer.', - $logEntries[0]['message'] + } + + #[Test] + public function logSucessAfterEmailToSellerWasSend(): void + { + $mockBuilder = $this->getMockBuilderForMailHandlerClass(); + $mockBuilder->onlyMethods( + [ + 'getSellerEmailTo', + 'getSellerEmailFrom', + 'getSellerEmailName', + ] + ); + $mailHandler = $mockBuilder->getMock(); + $mailHandler->method('getSellerEmailTo')->willReturn('sellerEmailTo@example.com'); + $mailHandler->method('getSellerEmailFrom')->willReturn('sellerEmailFrom@example.com'); + $mailHandler->method('getSellerEmailName')->willReturn('Seller Email Name'); + + $mailHandler->sendSellerMail( + $this->createStubForOrderItem() + ); + + $logEntries = $this->getAllRecords('tx_cart_domain_model_order_log'); + self::assertCount( + 1, + $logEntries + ); + self::assertIsArray($logEntries[0]); + + self::assertArrayIsEqualToArrayIgnoringListOfKeys( + [ + 'log_level' => 'info', + 'item' => 142, + 'type' => 'sendSellerMail', + 'message' => 'Mail was send to seller.', + 'level' => 'info', + ], + $logEntries[0], + [ + 'uid', + 'request_id', + 'crdate', + 'time_micro', + 'data', + 'arguments', + ] + ); + + self::assertIsString( + $logEntries[0]['arguments'] + ); + $arguments = json_decode( + ltrim($logEntries[0]['arguments'], '- '), + true + ); + self::assertIsArray( + $arguments ); self::assertArrayHasKey( - 'arguments', - $logEntries[0] + 'time', + $arguments + ); + } + + #[Test] + public function logErrorIfEmailToSellerCouldNotSend(): void + { + $mockBuilder = $this->getMockBuilderForMailHandlerClass(mailerThrowException: true); + $mockBuilder->onlyMethods( + [ + 'getSellerEmailTo', + 'getSellerEmailFrom', + 'getSellerEmailName', + ] + ); + $mailHandler = $mockBuilder->getMock(); + $mailHandler->method('getSellerEmailTo')->willReturn('sellerEmailTo@example.com'); + $mailHandler->method('getSellerEmailFrom')->willReturn('sellerEmailFrom@example.com'); + $mailHandler->method('getSellerEmailName')->willReturn('Seller Email Name'); + + $mailHandler->sendSellerMail( + $this->createStubForOrderItem() + ); + + $logEntries = $this->getAllRecords('tx_cart_domain_model_order_log'); + self::assertCount( + 1, + $logEntries + ); + self::assertIsArray($logEntries[0]); + + self::assertArrayIsEqualToArrayIgnoringListOfKeys( + [ + 'log_level' => 'error', + 'item' => 142, + 'type' => 'sendSellerMail', + 'message' => 'Mail could not send to seller.', + 'level' => 'error', + ], + $logEntries[0], + [ + 'uid', + 'request_id', + 'crdate', + 'time_micro', + 'data', + 'arguments', + ] ); + self::assertIsString( $logEntries[0]['arguments'] ); @@ -151,5 +315,70 @@ public function logSucessAfterEmailWasSend(): void 'time', $arguments ); + self::assertArrayHasKey( + 'exception', + $arguments + ); + self::assertIsString( + $arguments['exception'] + ); + self::assertStringStartsWith( + 'Exception in ', + $arguments['exception'] + ); + self::assertStringContainsString( + '/cart/Tests/Functional/Service/MailHandlerTest.php', + $arguments['exception'] + ); + } + + /** + * @return MockBuilder + */ + private function getMockBuilderForMailHandlerClass(bool $mailerThrowException = false): MockBuilder + { + $configurationManager = self::createStub(ConfigurationManagerInterface::class); + + $eventDispatcher = self::createStub(EventDispatcherInterface::class); + + $mailer = self::createStub(MailerInterface::class); + if ($mailerThrowException) { + $mailer->method('send')->willThrowException(new Exception()); + } + + $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); + $logService = GeneralUtility::makeInstance( + LogService::class, + $logger, + ); + + $mockBuilder = $this->getMockBuilder(MailHandler::class); + $mockBuilder->setConstructorArgs( + [ + $configurationManager, + $eventDispatcher, + $mailer, + $logService, + ] + ); + + return $mockBuilder; + } + + private function createStubForOrderItem(): OrderItem&Stub + { + $billingAddress = self::createStub(BillingAddress::class); + $billingAddress->method('getEmail')->willReturn('billingAddress@example.com'); + + $payment = self::createStub(Payment::class); + $payment->method('getStatus')->willReturn('open'); + + $orderItem = self::createStub(OrderItem::class); + $orderItem->method('getBillingAddress')->willReturn($billingAddress); + $orderItem->method('getPayment')->willReturn($payment); + $orderItem->method('getUid')->willReturn(142); + + return $orderItem; } + } diff --git a/ext_tables.sql b/ext_tables.sql index 8a32bf21..686dc4f6 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -242,11 +242,13 @@ CREATE TABLE tx_cart_domain_model_coupon ( # CREATE TABLE tx_cart_domain_model_order_log ( uid int(11) NOT NULL auto_increment, - identifier char(40) DEFAULT '' NOT NULL, + item int(11) DEFAULT '0' NOT NULL, + type varchar(255) DEFAULT '' NOT NULL, request_id varchar(13) DEFAULT '' NOT NULL, crdate int(11) DEFAULT '0' NOT NULL, time_micro float DEFAULT '0' NOT NULL, + log_level varchar(10) DEFAULT 'info' NOT NULL, level varchar(10) DEFAULT 'info' NOT NULL, message text, data text, From 422e37746c5d1cc19972bc9ad4b249240f3115e5 Mon Sep 17 00:00:00 2001 From: Daniel Gohlke Date: Sat, 28 Mar 2026 23:13:07 +0100 Subject: [PATCH 4/5] [TASK] Add Logging for updating payment and shipping Relates: #752 --- .../Backend/Order/PaymentController.php | 28 +++++++++++++++++-- .../Backend/Order/ShippingController.php | 28 +++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/Classes/Controller/Backend/Order/PaymentController.php b/Classes/Controller/Backend/Order/PaymentController.php index d7be1247..e8ea0607 100644 --- a/Classes/Controller/Backend/Order/PaymentController.php +++ b/Classes/Controller/Backend/Order/PaymentController.php @@ -12,22 +12,35 @@ */ use Extcode\Cart\Controller\Backend\ActionController; +use Extcode\Cart\Domain\Log\LogServiceInterface; +use Extcode\Cart\Domain\Log\Model\Log; use Extcode\Cart\Domain\Model\Order\Payment; use Extcode\Cart\Domain\Repository\Order\PaymentRepository; use Extcode\Cart\Event\Order\UpdateServiceEvent; +use InvalidArgumentException; use Psr\Http\Message\ResponseInterface; use TYPO3\CMS\Extbase\Utility\LocalizationUtility; class PaymentController extends ActionController { public function __construct( - protected PaymentRepository $paymentRepository + private readonly PaymentRepository $paymentRepository, + private readonly LogServiceInterface $logService, ) {} public function updateAction(Payment $payment): ResponseInterface { - // todo: add logging here $this->paymentRepository->update($payment); + $this->logService->write( + Log::info( + $this->getOrderItemUid($payment), + 'updatePayment', + 'Payment was set to ' . $payment->getStatus() . '.', + [ + 'time' => time(), + ] + ) + ); $event = new UpdateServiceEvent($payment); $this->eventDispatcher->dispatch($event); @@ -41,4 +54,15 @@ public function updateAction(Payment $payment): ResponseInterface return $this->redirect('show', 'Backend\Order\Order', null, ['orderItem' => $payment->getItem()]); } + + private function getOrderItemUid(Payment $payment): int + { + $orderItemUid = $payment->getItem()?->getUid(); + + if (is_null($orderItemUid)) { + throw new InvalidArgumentException('Method should only called for persisted orders.', 1774715307); + } + + return $orderItemUid; + } } diff --git a/Classes/Controller/Backend/Order/ShippingController.php b/Classes/Controller/Backend/Order/ShippingController.php index 1ad1cc58..f1b97aee 100644 --- a/Classes/Controller/Backend/Order/ShippingController.php +++ b/Classes/Controller/Backend/Order/ShippingController.php @@ -12,22 +12,35 @@ */ use Extcode\Cart\Controller\Backend\ActionController; +use Extcode\Cart\Domain\Log\LogServiceInterface; +use Extcode\Cart\Domain\Log\Model\Log; use Extcode\Cart\Domain\Model\Order\Shipping; use Extcode\Cart\Domain\Repository\Order\ShippingRepository; use Extcode\Cart\Event\Order\UpdateServiceEvent; +use InvalidArgumentException; use Psr\Http\Message\ResponseInterface; use TYPO3\CMS\Extbase\Utility\LocalizationUtility; class ShippingController extends ActionController { public function __construct( - protected ShippingRepository $shippingRepository + private readonly ShippingRepository $shippingRepository, + private readonly LogServiceInterface $logService, ) {} public function updateAction(Shipping $shipping): ResponseInterface { - // todo: add logging here $this->shippingRepository->update($shipping); + $this->logService->write( + Log::info( + $this->getOrderItemUid($shipping), + 'updateShipping', + 'Shipping was set to ' . $shipping->getStatus() . '.', + [ + 'time' => time(), + ] + ) + ); $event = new UpdateServiceEvent($shipping); $this->eventDispatcher->dispatch($event); @@ -41,4 +54,15 @@ public function updateAction(Shipping $shipping): ResponseInterface return $this->redirect('show', 'Backend\Order\Order', null, ['orderItem' => $shipping->getItem()]); } + + private function getOrderItemUid(Shipping $shipping): int + { + $orderItemUid = $shipping->getItem()?->getUid(); + + if (is_null($orderItemUid)) { + throw new InvalidArgumentException('Method should only called for persisted orders.', 1774715307); + } + + return $orderItemUid; + } } From 5baaebb84e2bdce43787f05ea89a444e813d29a3 Mon Sep 17 00:00:00 2001 From: Daniel Gohlke Date: Sat, 28 Mar 2026 23:14:07 +0100 Subject: [PATCH 5/5] [TASK] Update version numbers in several files Relates: #752 --- Documentation/Introduction/Index.rst | 8 -------- Documentation/Introduction/NoteOfThanks/Index.rst | 15 --------------- Documentation/Introduction/Sponsoring/Index.rst | 3 --- Documentation/guides.xml | 6 +++--- README.md | 10 +++++----- ext_emconf.php | 8 ++++---- 6 files changed, 12 insertions(+), 38 deletions(-) delete mode 100644 Documentation/Introduction/NoteOfThanks/Index.rst diff --git a/Documentation/Introduction/Index.rst b/Documentation/Introduction/Index.rst index 20694a56..49eb4266 100644 --- a/Documentation/Introduction/Index.rst +++ b/Documentation/Introduction/Index.rst @@ -86,13 +86,6 @@ Examples of websites which use this extension as e-commerce solution. `www.liebman-design-import.com `__ -.. figure:: ../Images/Examples/weingut-isele.de.png - :width: 640 - :alt: Cart of Weingut Isele - :class: with-shadow - - `www.weingut-isele.de `__ - **Table of contents** .. toctree:: @@ -101,4 +94,3 @@ Examples of websites which use this extension as e-commerce solution. Support/Index Sponsoring/Index - NoteOfThanks/Index diff --git a/Documentation/Introduction/NoteOfThanks/Index.rst b/Documentation/Introduction/NoteOfThanks/Index.rst deleted file mode 100644 index 0d5e7181..00000000 --- a/Documentation/Introduction/NoteOfThanks/Index.rst +++ /dev/null @@ -1,15 +0,0 @@ -.. include:: ../../Includes.rst.txt - -============== -Note of thanks -============== - -A big thank you goes `Tritum GmbH `__ for the many hours I was allowed to work on Cart. - -In particular, I would like to thank Björn. He always has an open ear. He contributed his opinion to many questions -and decisions. Without him, Cart would not be what it is today. - -Another thanks goes to the testers for their feedback and understanding when I made changes to the data model again and -again. - -A big thank you also goes out to all the supporters on github. diff --git a/Documentation/Introduction/Sponsoring/Index.rst b/Documentation/Introduction/Sponsoring/Index.rst index a2fce503..78671a83 100644 --- a/Documentation/Introduction/Sponsoring/Index.rst +++ b/Documentation/Introduction/Sponsoring/Index.rst @@ -9,8 +9,5 @@ If there is a feature that has not yet been implemented in Cart, you can contact There is also the possibility to support the further development independently of new functions. * Ask for an invoice. -* `GitHub Sponsors `_ * `paypal.me `_ -Sponsors --------- diff --git a/Documentation/guides.xml b/Documentation/guides.xml index 3ce88ac9..3ed5ce12 100644 --- a/Documentation/guides.xml +++ b/Documentation/guides.xml @@ -11,9 +11,9 @@ interlink-shortcode="extcode/cart" /> diff --git a/README.md b/README.md index aee99e82..a2c4e1cc 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,11 @@ Sometimes minor versions also result in minor adjustments to own templates or co | Cart | TYPO3 | PHP | Support/Development | |--------|------------|-----------|--------------------------------------| -| 11.x.x | 13.4 | 8.2 - 8.5 | Features, Bugfixes, Security Updates | -| 10.x.x | 12.4 | 8.1 - 8.5 | Bugfixes, Security Updates | -| 9.x.x | 12.4 | 8.1 - 8.4 | Security Updates | -| 8.x.x | 10.4, 11.5 | 7.2+ | Security Updates | +| 12.x.x | 14.1 | 8.2 - 8.5 | Features, Bugfixes, Security Updates | +| 11.x.x | 13.4 | 8.2 - 8.5 | Bugfixes, Security Updates | +| 10.x.x | 12.4 | 8.1 - 8.5 | Security Updates | +| 9.x.x | 12.4 | 8.1 - 8.4 | | +| 8.x.x | 10.4, 11.5 | 7.2+ | | | 7.x.x | 10.4 | 7.2 - 7.4 | | | 6.x.x | 9.5 | 7.2 - 7.4 | | | 5.x.x | 8.7 | 7.0 - 7.4 | | @@ -79,7 +80,6 @@ News uses **semantic versioning** which basically means for you, that ## 4. Sponsoring * Ask for an invoice. -* [GitHub Sponsors](https://github.com/sponsors/extcode) * [PayPal.Me](https://paypal.me/extcart) [1]: https://docs.typo3.org/typo3cms/extensions/cart/ diff --git a/ext_emconf.php b/ext_emconf.php index 4169be74..105ca46a 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -4,7 +4,7 @@ 'title' => 'Cart', 'description' => 'Shopping Cart(s) for TYPO3', 'category' => 'plugin', - 'version' => '11.7.2', + 'version' => '12.0.0', 'state' => 'stable', 'author' => 'Daniel Gohlke', 'author_email' => 'ext@extco.de', @@ -12,9 +12,9 @@ 'constraints' => [ 'depends' => [ 'php' => '8.2.0-8.4.99', - 'typo3' => '13.4.0-13.4.99', - 'extbase' => '13.4.0-13.4.99', - 'fluid' => '13.4.0-13.4.99', + 'typo3' => '14.1.0-14.4.99', + 'extbase' => '14.1.0-14.1.99', + 'fluid' => '14.1.0-14.1.99', ], 'conflicts' => [], 'suggests' => [],