From a88650769f7e3a9e3d03e5226cf3bce10e412ea2 Mon Sep 17 00:00:00 2001 From: David Kohr Date: Thu, 17 Sep 2026 15:42:42 +0200 Subject: [PATCH 1/2] [BUGFIX] Remove shared static state from getLastPagevisit() A static variable inside an instance method is bound to the method, not to the object. As soon as the first visitor yielded a non-null value, every subsequent visitor in the same PHP process returned that same Pagevisit. The intended per-object caching never happened. This silently corrupted scoring: ScoringService uses the getter in getNumberOfDaysSinceLastVisit(), and LuxServiceRecalculateScoringCommand iterates all visitors in a single process, so lastVisitDaysAgo was wrong for every visitor but the first. It also made the lead summary mail show one single timestamp for all leads. getPagevisitLast() already performs exactly the same lookup without the static, so the method now delegates to it. This removes the bug and the duplicated logic at once. --- Classes/Domain/Model/Visitor.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Classes/Domain/Model/Visitor.php b/Classes/Domain/Model/Visitor.php index f862e885..766f1f28 100644 --- a/Classes/Domain/Model/Visitor.php +++ b/Classes/Domain/Model/Visitor.php @@ -603,16 +603,7 @@ public function removePagevisit(Pagevisit $pagevisit): self public function getLastPagevisit(): ?Pagevisit { - static $lastPagevisit = null; - if ($lastPagevisit === null) { - $pagevisits = $this->getPagevisits(); - $lastPagevisit = null; - foreach ($pagevisits as $pagevisit) { - $lastPagevisit = $pagevisit; - break; - } - } - return $lastPagevisit; + return $this->getPagevisitLast(); } /** From b7aa88c800416220b6a956669242767d19b2b6e9 Mon Sep 17 00:00:00 2001 From: Bastien Lutz Date: Fri, 18 Sep 2026 16:59:14 +0200 Subject: [PATCH 2/2] [BUGFIX] Fix caching of last page visit in visitor model Before `static` was used inside the getter. Unfortunately `static` is bound to the method and not to the individual object. When calling `getLastPagevisit()` on object a and calling it later again on object b the cached value of object a was returned. To fix this, set a dynamic property for the model so the value is cached individually. Additionally, tests were added to check if the value is cached and if the value leaks between visitor objects. fixes: #81 --- Classes/Domain/Model/Visitor.php | 6 ++++- Tests/Unit/Domain/Model/VisitorTest.php | 36 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/Classes/Domain/Model/Visitor.php b/Classes/Domain/Model/Visitor.php index 766f1f28..1f3c0b72 100644 --- a/Classes/Domain/Model/Visitor.php +++ b/Classes/Domain/Model/Visitor.php @@ -128,6 +128,7 @@ class Visitor extends AbstractModel protected bool $blacklisted = false; protected ?FrontendUser $frontenduser = null; + protected ?Pagevisit $lastPageVisit = null; public function __construct() { @@ -603,7 +604,10 @@ public function removePagevisit(Pagevisit $pagevisit): self public function getLastPagevisit(): ?Pagevisit { - return $this->getPagevisitLast(); + if ($this->lastPageVisit === null) { + $this->lastPageVisit = array_last($this->getPagevisits()); + } + return $this->lastPageVisit; } /** diff --git a/Tests/Unit/Domain/Model/VisitorTest.php b/Tests/Unit/Domain/Model/VisitorTest.php index 1dafe889..d793e4cb 100644 --- a/Tests/Unit/Domain/Model/VisitorTest.php +++ b/Tests/Unit/Domain/Model/VisitorTest.php @@ -2,13 +2,16 @@ namespace In2code\Lux\Tests\Unit\Domain\Model; +use DateTime; use In2code\Lux\Domain\Model\Attribute; use In2code\Lux\Domain\Model\Categoryscoring; +use In2code\Lux\Domain\Model\Pagevisit; use In2code\Lux\Domain\Model\Visitor; use In2code\Lux\Tests\Helper\TestingHelper; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; use TYPO3\CMS\Extbase\Persistence\ObjectStorage; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; @@ -16,6 +19,7 @@ #[CoversMethod(Visitor::class, 'getCategoryscoringsSortedByScoring')] #[CoversMethod(Visitor::class, 'getFullName')] #[CoversMethod(Visitor::class, 'getNameCombination')] +#[CoversMethod(Visitor::class, 'getLastPagevisit')] class VisitorTest extends UnitTestCase { protected array $testFilesToDelete = []; @@ -123,4 +127,36 @@ public function testGetCategoryscoringsSortedByScoring(array $sortings, array $e } self::assertSame($expectedSortings, $newScoringArray); } + + #[Test] + public function getLastPagevisitCachesResultPerVisitor(): void + { + $visitor = new Visitor(); + $pagevisit = new Pagevisit(); + $pagevisit->setCrdate(new DateTime('2026-01-01 10:00:00')); + $visitor->addPagevisit($pagevisit); + + self::assertSame($pagevisit, $visitor->getLastPagevisit()); + + $newerPagevisit = new Pagevisit(); + $newerPagevisit->setCrdate(new DateTime('2026-01-02 10:00:00')); + $visitor->addPagevisit($newerPagevisit); + + self::assertSame($pagevisit, $visitor->getLastPagevisit()); + } + + #[Test] + public function getLastPagevisitIsNotSharedBetweenVisitors(): void + { + $firstPagevisit = new Pagevisit(); + $firstVisitor = new Visitor(); + $firstVisitor->addPagevisit($firstPagevisit); + + $secondPagevisit = new Pagevisit(); + $secondVisitor = new Visitor(); + $secondVisitor->addPagevisit($secondPagevisit); + + self::assertSame($firstPagevisit, $firstVisitor->getLastPagevisit()); + self::assertSame($secondPagevisit, $secondVisitor->getLastPagevisit()); + } }