Allow to use expressions with PHP 8.x

Introduce new traverse function taken from TYPO3 TypoScript conditions.

Relates: #86
This commit is contained in:
Daniel Siepmann 2022-09-16 13:50:40 +02:00
parent 5b4a379978
commit 39529be49a
13 changed files with 297 additions and 303 deletions

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace DanielSiepmann\Tracking\Domain\ExpressionLanguage;
use DanielSiepmann\Tracking\Domain\Model\Expression;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Context\Context;
interface Factory
{
public function create(
string $expression,
ServerRequestInterface $request,
Context $context
): Expression;
}

View file

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace DanielSiepmann\Tracking\Domain\ExpressionLanguage;
use DanielSiepmann\Tracking\Domain\Model\Expression;
use DanielSiepmann\Tracking\Domain\Model\SymfonyExpression;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
class SymfonyExpressionLanguage implements Factory
{
public function create(
string $expression,
ServerRequestInterface $request,
Context $context
): Expression {
$language = new ExpressionLanguage();
$this->registerTraverseFunction($language);
return new SymfonyExpression(
$expression,
[
'context' => $context,
'request' => $request,
],
$language
);
}
/**
* Taken from TYPO3 Core, see: https://docs.typo3.org/m/typo3/reference-typoscript/11.5/en-us/Conditions/Index.html#traverse
*/
private function registerTraverseFunction(ExpressionLanguage $language): void
{
$language->register(
'traverse',
static function () {
// Not implemented, we only use the evaluator
},
static function ($arguments, $array, $path) {
if (!is_array($array) || !is_string($path) || $path === '') {
return '';
}
try {
return ArrayUtility::getValueByPath($array, $path);
} catch (MissingArrayPathException $e) {
return '';
}
}
);
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace DanielSiepmann\Tracking\Domain\Model;
interface Expression
{
/**
* @return mixed
*/
public function evaluate();
}

View file

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace DanielSiepmann\Tracking\Domain\Model;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
class SymfonyExpression implements Expression
{
/**
* @var string
*/
private $expression;
/**
* @var array
*/
private $values;
/**
* @var ExpressionLanguage
*/
private $symfonyExpression;
public function __construct(
string $expression,
array $values,
ExpressionLanguage $symfonyExpression
) {
$this->expression = $expression;
$this->values = $values;
$this->symfonyExpression = $symfonyExpression;
}
public function evaluate()
{
return $this->symfonyExpression->evaluate(
$this->expression,
$this->values
);
}
}

View file

@ -23,13 +23,13 @@ declare(strict_types=1);
namespace DanielSiepmann\Tracking\Middleware;
use DanielSiepmann\Tracking\Domain\ExpressionLanguage\Factory as ExpressionFactory;
use DanielSiepmann\Tracking\Domain\Pageview\Factory;
use DanielSiepmann\Tracking\Domain\Repository\Pageview as Repository;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use TYPO3\CMS\Core\Context\Context;
class Pageview implements MiddlewareInterface
@ -44,15 +44,25 @@ class Pageview implements MiddlewareInterface
*/
private $context;
/**
* @var ExpressionFactory
*/
private $expressionFactory;
/**
* @var string
*/
private $rule = '';
public function __construct(Repository $repository, Context $context, string $rule)
{
public function __construct(
Repository $repository,
Context $context,
ExpressionFactory $expressionFactory,
string $rule
) {
$this->repository = $repository;
$this->context = $context;
$this->expressionFactory = $expressionFactory;
$this->rule = $rule;
}
@ -71,9 +81,10 @@ class Pageview implements MiddlewareInterface
ServerRequestInterface $request,
Context $context
): bool {
return (bool) (new ExpressionLanguage())->evaluate($this->rule, [
'request' => $request,
'context' => $context,
]);
return (bool) $this->expressionFactory->create(
$this->rule,
$request,
$context
)->evaluate();
}
}

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace DanielSiepmann\Tracking\Middleware;
use DanielSiepmann\Tracking\Domain\ExpressionLanguage\Factory as ExpressionFactory;
use DanielSiepmann\Tracking\Domain\Model\RecordRule;
use DanielSiepmann\Tracking\Domain\Recordview\Factory;
use DanielSiepmann\Tracking\Domain\Repository\Recordview as Repository;
@ -45,6 +46,11 @@ class Recordview implements MiddlewareInterface
*/
private $context;
/**
* @var ExpressionFactory
*/
private $expressionFactory;
/**
* @var array<RecordRule>
*/
@ -53,10 +59,12 @@ class Recordview implements MiddlewareInterface
public function __construct(
Repository $repository,
Context $context,
ExpressionFactory $expressionFactory,
array $rules
) {
$this->repository = $repository;
$this->context = $context;
$this->expressionFactory = $expressionFactory;
$this->rules = RecordRule::multipleFromArray($rules);
}
@ -79,13 +87,10 @@ class Recordview implements MiddlewareInterface
Context $context,
RecordRule $rule
): bool {
// Need silent, as expression language doens't provide a way to check for array keys
return @(bool) (new ExpressionLanguage())->evaluate(
return (bool) $this->expressionFactory->create(
$rule->getMatchesExpression(),
[
'request' => $request,
'context' => $context,
]
);
$request,
$context
)->evaluate();
}
}

View file

@ -59,7 +59,7 @@ services:
$rule: >
not (context.getAspect("backend.user").isLoggedIn())
and not (context.getAspect("frontend.preview").isPreview())
and request.getHeader("User-Agent")[0]
and traverse(request.getHeader("User-Agent"), '0')
and not (request.getHeader("User-Agent")[0] matches "/^TYPO3|TYPO3 linkvalidator/")
and not (request.getHeader("User-Agent")[0] matches "/Wget|curl|Go-http-client/")
and not (request.getHeader("User-Agent")[0] matches "/Googlebot|Bingbot|bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot|Sogou|Exabot|NextCloud-News|Feedly|XING FeedReader|CCBot|SemrushBot|SEOkicks|Twitterbot|Seekport Crawler|SemanticScholarBot|ia_archiver|PaperLiBot|TrendsmapResolver|AhrefsBot|Nuzzel/")

View file

@ -0,0 +1,54 @@
1.3.0
=====
Breaking
--------
Nothing
Features
--------
* Introduce new ``traverse()`` function for rules.
The function is taken from TYPO3, documentation can be found here: https://docs.typo3.org/m/typo3/reference-typoscript/11.5/en-us/Conditions/Index.html#traverse
Rules provided by extension are adjusted.
The new function allows for save array access in order to prevent issues in recent
PHP versions with undefined array keys.
The important change was:
.. code-block:: diff
diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml
index 1aaec24..df8bb13 100644
--- a/Configuration/Services.yaml
+++ b/Configuration/Services.yaml
@@ -59,7 +59,7 @@ services:
$rule: >
not (context.getAspect("backend.user").isLoggedIn())
and not (context.getAspect("frontend.preview").isPreview())
- and request.getHeader("User-Agent")[0]
+ and traverse(request.getHeader("User-Agent"), '0')
and not (request.getHeader("User-Agent")[0] matches "/^TYPO3|TYPO3 linkvalidator/")
and not (request.getHeader("User-Agent")[0] matches "/Wget|curl|Go-http-client/")
and not (request.getHeader("User-Agent")[0] matches "/Googlebot|Bingbot|bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot|Sogou|Exabot|NextCloud-News|Feedly|XING FeedReader|CCBot|SemrushBot|SEOkicks|Twitterbot|Seekport Crawler|SemanticScholarBot|ia_archiver|PaperLiBot|TrendsmapResolver|AhrefsBot|Nuzzel/")
Same for other array accesses, e.g recordUid of ``Recordview``::
recordUid: 'traverse(request.getQueryParams(), "tx_news_pi1", "news")'
Fixes
-----
Nothing
Tasks
-----
Nothing
Deprecation
-----------
Nothing

View file

@ -43,6 +43,7 @@ Let us examine an concrete example::
$rule: >
not (context.getAspect("backend.user").isLoggedIn())
and not (context.getAspect("frontend.preview").isPreview())
and traverse(request.getHeader("User-Agent"), '0')
and not (request.getHeader("User-Agent")[0] matches "/^TYPO3|TYPO3 linkvalidator/")
and not (request.getHeader("User-Agent")[0] matches "/^Codeception Testing/")
and not (request.getHeader("User-Agent")[0] matches "/Wget|curl|Go-http-client/")

View file

@ -56,11 +56,12 @@ Let us examine an concrete example::
request.getQueryParams()["tx_news_pi1"] && request.getQueryParams()["tx_news_pi1"]["news"] > 0
and not (context.getAspect("backend.user").isLoggedIn())
and not (context.getAspect("frontend.preview").isPreview())
and traverse(request.getHeader("User-Agent"), '0')
and not (request.getHeader("User-Agent")[0] matches "/^TYPO3|TYPO3 linkvalidator/")
and not (request.getHeader("User-Agent")[0] matches "/Wget|curl|Go-http-client/")
and not (request.getHeader("User-Agent")[0] matches "/bot|spider|Slurp|Sogou|NextCloud-News|Feedly|XING FeedReader|SEOkicks|Seekport Crawler|ia_archiver|TrendsmapResolver|Nuzzel/")
and not (request.getHeader("User-Agent")[0] matches "/mattermost|Slackbot|WhatsApp/")
recordUid: 'request.getQueryParams()["tx_news_pi1"]["news"]'
recordUid: 'traverse(request.getQueryParams(), "tx_news_pi1", "news")'
tableName: 'tx_news_domain_model_news'
The first paragraph will not be explained, check out :ref:`t3coreapi:configure-dependency-injection-in-extensions` instead.

View file

@ -1,98 +0,0 @@
<?php
namespace DanielSiepmann\Tracking\Tests\Unit\Middleware;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use DanielSiepmann\Tracking\Domain\Model\Pageview as Model;
use DanielSiepmann\Tracking\Domain\Repository\Pageview as Repository;
use DanielSiepmann\Tracking\Middleware\Pageview;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Routing\PageArguments;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase as TestCase;
/**
* @covers DanielSiepmann\Tracking\Middleware\Pageview
*/
class PageviewTest extends TestCase
{
use ProphecyTrait;
/**
* @test
*/
public function doesNotAddRequestIfRuleDoesNotApply(): void
{
$repository = $this->prophesize(Repository::class);
$context = $this->prophesize(Context::class);
$rule = 'false';
$request = $this->prophesize(ServerRequestInterface::class);
$response = $this->prophesize(ResponseInterface::class);
$handler = $this->prophesize(RequestHandlerInterface::class);
$handler->handle($request->reveal())->willReturn($response->reveal());
$repository->add()->shouldNotBeCalled();
$subject = new Pageview($repository->reveal(), $context->reveal(), $rule);
$result = $subject->process($request->reveal(), $handler->reveal());
static::assertInstanceOf(ResponseInterface::class, $result);
}
/**
* @test
*/
public function addsPageviewToRepository(): void
{
$repository = $this->prophesize(Repository::class);
$context = $this->prophesize(Context::class);
$rule = 'true';
$routing = $this->prophesize(PageArguments::class);
$routing->getPageId()->willReturn(10);
$routing->getPageType()->willReturn(0);
$language = $this->prophesize(SiteLanguage::class);
$request = $this->prophesize(ServerRequestInterface::class);
$request->getAttribute('routing')->willReturn($routing->reveal());
$request->getAttribute('language')->willReturn($language->reveal());
$request->getUri()->willReturn('');
$request->getHeader('User-Agent')->willReturn([]);
$response = $this->prophesize(ResponseInterface::class);
$handler = $this->prophesize(RequestHandlerInterface::class);
$handler->handle($request->reveal())->willReturn($response->reveal());
$repository->add(Argument::type(Model::class))->shouldBeCalledtimes(1);
$subject = new Pageview($repository->reveal(), $context->reveal(), $rule);
$result = $subject->process($request->reveal(), $handler->reveal());
static::assertInstanceOf(ResponseInterface::class, $result);
}
}

View file

@ -1,188 +0,0 @@
<?php
namespace DanielSiepmann\Tracking\Tests\Unit\Middleware;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use DanielSiepmann\Tracking\Domain\Model\Recordview as Model;
use DanielSiepmann\Tracking\Domain\Repository\Recordview as Repository;
use DanielSiepmann\Tracking\Middleware\Recordview;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Routing\PageArguments;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase as TestCase;
/**
* @covers DanielSiepmann\Tracking\Middleware\Recordview
*/
class RecordviewTest extends TestCase
{
use ProphecyTrait;
/**
* @test
*/
public function proceedsWithNextHandlerIfNoRuleIsConfigured(): void
{
$repository = $this->prophesize(Repository::class);
$repository->add()->shouldNotBeCalled();
$context = $this->prophesize(Context::class);
$request = $this->prophesize(ServerRequestInterface::class);
$response = $this->prophesize(ResponseInterface::class);
$handler = $this->prophesize(RequestHandlerInterface::class);
$handler->handle($request->reveal())->willReturn($response);
$subject = new Recordview($repository->reveal(), $context->reveal(), []);
$result = $subject->process($request->reveal(), $handler->reveal());
static::assertInstanceOf(ResponseInterface::class, $result);
}
/**
* @test
*/
public function doesntAddViewIfRuleDoesNotMatchRequest(): void
{
$repository = $this->prophesize(Repository::class);
$repository->add()->shouldNotBeCalled();
$context = $this->prophesize(Context::class);
$request = $this->prophesize(ServerRequestInterface::class);
$request->getQueryParams()->willReturn([]);
$response = $this->prophesize(ResponseInterface::class);
$handler = $this->prophesize(RequestHandlerInterface::class);
$handler->handle($request->reveal())->willReturn($response);
$subject = new Recordview($repository->reveal(), $context->reveal(), [
'topic' => [
'matches' => 'request.getQueryParams()["topic_id"] > 0',
'recordUid' => '',
'tableName' => '',
],
]);
$result = $subject->process($request->reveal(), $handler->reveal());
static::assertInstanceOf(ResponseInterface::class, $result);
}
/**
* @test
*/
public function addsSingleViewIfRuleMatches(): void
{
$repository = $this->prophesize(Repository::class);
$repository->add(Argument::that(function (Model $recordview) {
return $recordview->getPageUid() === 10
&& $recordview->getRecordUid() === 10
&& $recordview->getTableName() === 'topics'
;
}))->shouldBeCalled();
$context = $this->prophesize(Context::class);
$routing = $this->prophesize(PageArguments::class);
$routing->getPageId()->willReturn(10);
$language = $this->prophesize(SiteLanguage::class);
$request = $this->prophesize(ServerRequestInterface::class);
$request->getAttribute('routing')->willReturn($routing->reveal());
$request->getAttribute('language')->willReturn($language->reveal());
$request->getUri()->willReturn('');
$request->getHeader('User-Agent')->willReturn([]);
$request->getQueryParams()->willReturn([
'topic_id' => '10',
]);
$response = $this->prophesize(ResponseInterface::class);
$handler = $this->prophesize(RequestHandlerInterface::class);
$handler->handle($request->reveal())->willReturn($response);
$subject = new Recordview($repository->reveal(), $context->reveal(), [
'topic' => [
'matches' => 'request.getQueryParams()["topic_id"] > 0',
'recordUid' => 'request.getQueryParams()["topic_id"]',
'tableName' => 'topics',
],
]);
$result = $subject->process($request->reveal(), $handler->reveal());
static::assertInstanceOf(ResponseInterface::class, $result);
}
/**
* @test
*/
public function canAddMultipleViewsIfMultipleRulesApply(): void
{
$repository = $this->prophesize(Repository::class);
$repository->add(Argument::that(function (Model $recordview) {
return $recordview->getPageUid() === 10
&& $recordview->getRecordUid() === 10
&& $recordview->getTableName() === 'topics'
;
}))->shouldBeCalled();
$repository->add(Argument::that(function (Model $recordview) {
return $recordview->getPageUid() === 10
&& $recordview->getRecordUid() === 20
&& $recordview->getTableName() === 'news'
;
}))->shouldBeCalled();
$context = $this->prophesize(Context::class);
$routing = $this->prophesize(PageArguments::class);
$routing->getPageId()->willReturn(10);
$language = $this->prophesize(SiteLanguage::class);
$request = $this->prophesize(ServerRequestInterface::class);
$request->getAttribute('routing')->willReturn($routing->reveal());
$request->getAttribute('language')->willReturn($language->reveal());
$request->getUri()->willReturn('');
$request->getHeader('User-Agent')->willReturn([]);
$request->getQueryParams()->willReturn([
'topic_id' => '10',
'news' => '20',
]);
$response = $this->prophesize(ResponseInterface::class);
$handler = $this->prophesize(RequestHandlerInterface::class);
$handler->handle($request->reveal())->willReturn($response);
$subject = new Recordview($repository->reveal(), $context->reveal(), [
'topic' => [
'matches' => 'request.getQueryParams()["topic_id"] > 0',
'recordUid' => 'request.getQueryParams()["topic_id"]',
'tableName' => 'topics',
],
'news' => [
'matches' => 'request.getQueryParams()["news"] > 0',
'recordUid' => 'request.getQueryParams()["news"]',
'tableName' => 'news',
],
]);
$result = $subject->process($request->reveal(), $handler->reveal());
static::assertInstanceOf(ResponseInterface::class, $result);
}
}

View file

@ -11,7 +11,7 @@ $EM_CONF[$_EXTKEY] = [
'author' => 'Daniel Siepmann',
'author_email' => 'coding@daniel-siepmann.de',
'author_company' => '',
'version' => '1.2.0',
'version' => '1.3.0',
'constraints' => [
'depends' => [
'core' => '',