[TASK] Kickstart extension with basic features (#1)

This commit is contained in:
Daniel Siepmann 2022-09-27 10:35:40 +02:00 committed by GitHub
parent 15120714de
commit 916b2723d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1546 additions and 3 deletions

150
.github/workflows/ci.yaml vendored Normal file
View file

@ -0,0 +1,150 @@
name: CI
on:
- pull_request
jobs:
check-composer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Composer
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
coverage: none
tools: composer:v2
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Validate composer.json
run: composer validate
php-linting:
runs-on: ubuntu-latest
strategy:
matrix:
php-version:
- 7.4
- 8.0
- 8.1
steps:
- name: Checkout
uses: actions/checkout@v2
- name: PHP lint
run: "find *.php Classes Configuration Tests -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l"
xml-linting:
runs-on: ubuntu-latest
needs: [check-composer]
steps:
- uses: actions/checkout@v2
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.1"
coverage: none
tools: composer:v2
- name: Install xmllint
run: sudo apt-get install libxml2-utils
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: PHPUnit configuration file
run: xmllint --schema vendor/phpunit/phpunit/phpunit.xsd --noout phpunit.xml.dist
- name: Fetch schema for xliff
run: wget https://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd --output-document=xliff-core-1.2-strict.xsd
- name: TYPO3 language files
run: xmllint --schema xliff-core-1.2-strict.xsd --noout $(find Resources -name '*.xlf')
coding-guideline:
runs-on: ubuntu-latest
needs:
- php-linting
- xml-linting
steps:
- uses: actions/checkout@v2
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.1"
coverage: none
tools: composer:v2
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Coding Guideline
run: ./vendor/bin/php-cs-fixer fix --dry-run --diff
code-quality:
runs-on: ubuntu-latest
needs:
- php-linting
strategy:
matrix:
include:
- php-version: '7.4'
- php-version: '8.0'
- php-version: '8.1'
steps:
- uses: actions/checkout@v2
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php-version }}"
coverage: none
tools: composer:v2
- name: Install dependencies with expected TYPO3 version
run: composer require --no-interaction --prefer-dist --no-progress
- name: Code Quality (by PHPStan)
run: ./vendor/bin/phpstan analyse
tests-mysql:
runs-on: ubuntu-latest
needs:
- php-linting
- xml-linting
strategy:
matrix:
include:
- php-version: '7.4'
- php-version: '8.0'
- php-version: '8.1'
steps:
- uses: actions/checkout@v2
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php-version }}"
coverage: none
tools: composer:v2
- name: Setup MySQL
uses: mirromutth/mysql-action@v1.1
with:
mysql version: '8'
mysql database: 'typo3'
mysql root password: 'root'
- name: Install dependencies
run: composer require --no-interaction --prefer-dist --no-progress
- name: PHPUnit Tests
run: |-
export typo3DatabaseDriver="pdo_mysql"
export typo3DatabaseName="typo3"
export typo3DatabaseHost="127.0.0.1"
export typo3DatabaseUsername="root"
export typo3DatabasePassword="root"
./vendor/bin/phpunit --testdox

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
/composer.lock
/.php-cs-fixer.cache
/vendor/
/public/
/var/

62
.php-cs-fixer.dist.php Normal file
View file

@ -0,0 +1,62 @@
<?php
$finder = (new PhpCsFixer\Finder())
->ignoreVCSIgnored(true)
->in(realpath(__DIR__));
return (new \PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@DoctrineAnnotation' => true,
'@PSR2' => true,
'array_syntax' => ['syntax' => 'short'],
'blank_line_after_opening_tag' => true,
'braces' => ['allow_single_line_closure' => true],
'cast_spaces' => ['space' => 'none'],
'compact_nullable_typehint' => true,
'concat_space' => ['spacing' => 'one'],
'declare_equal_normalize' => ['space' => 'none'],
'dir_constant' => true,
'function_to_constant' => ['functions' => ['get_called_class', 'get_class', 'get_class_this', 'php_sapi_name', 'phpversion', 'pi']],
'function_typehint_space' => true,
'lowercase_cast' => true,
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
'modernize_strpos' => true,
'modernize_types_casting' => true,
'native_function_casing' => true,
'new_with_braces' => true,
'no_alias_functions' => true,
'no_blank_lines_after_phpdoc' => true,
'no_empty_phpdoc' => true,
'no_empty_statement' => true,
'no_extra_blank_lines' => true,
'no_leading_import_slash' => true,
'no_leading_namespace_whitespace' => true,
'no_null_property_initialization' => true,
'no_short_bool_cast' => true,
'no_singleline_whitespace_before_semicolons' => true,
'no_superfluous_elseif' => true,
'no_trailing_comma_in_singleline_array' => true,
'no_unneeded_control_parentheses' => true,
'no_unused_imports' => true,
'no_useless_else' => true,
'no_whitespace_in_blank_line' => true,
'ordered_imports' => true,
'php_unit_construct' => ['assertions' => ['assertEquals', 'assertSame', 'assertNotEquals', 'assertNotSame']],
'php_unit_mock_short_will_return' => true,
'php_unit_test_case_static_method_calls' => ['call_type' => 'self'],
'phpdoc_no_access' => true,
'phpdoc_no_empty_return' => true,
'phpdoc_no_package' => true,
'phpdoc_scalar' => true,
'phpdoc_trim' => true,
'phpdoc_types' => true,
'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'],
'return_type_declaration' => ['space_before' => 'none'],
'single_quote' => true,
'single_line_comment_style' => ['comment_types' => ['hash']],
'single_trait_insert_per_statement' => true,
'trailing_comma_in_multiline' => ['elements' => ['arrays']],
'whitespace_after_comma_in_array' => true,
'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false],
])
->setFinder($finder);

View file

@ -0,0 +1,84 @@
<?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 WerkraumMedia\Watchlist\Controller;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Http\PropagateResponseException;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use WerkraumMedia\Watchlist\Domain\Model\Item;
use WerkraumMedia\Watchlist\Domain\Model\Watchlist;
use WerkraumMedia\Watchlist\Domain\Repository\WatchlistRepository;
class WatchlistController extends ActionController
{
private WatchlistRepository $repository;
public function __construct(
WatchlistRepository $repository
) {
$this->repository = $repository;
}
protected function initializeAction(): void
{
if ($this->request->hasArgument('watchlist') === false) {
$this->request->setArgument('watchlist', 'default');
}
}
public function indexAction(
Watchlist $watchlist
): ResponseInterface {
$this->view->assignMultiple([
'watchlist' => $watchlist,
]);
return $this->htmlResponse();
}
public function addAction(
Watchlist $watchlist,
Item $item,
string $redirectUri = ''
): void {
$watchlist->addItem($item);
$this->repository->update($watchlist);
throw new PropagateResponseException(new RedirectResponse($redirectUri, 303), 1664189968);
}
public function removeAction(
Watchlist $watchlist,
Item $item,
string $redirectUri
): void {
$watchlist->removeItem($item);
$this->repository->update($watchlist);
throw new PropagateResponseException(new RedirectResponse($redirectUri, 303), 1664189969);
}
}

View file

@ -0,0 +1,45 @@
<?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 WerkraumMedia\Watchlist\Domain;
use WerkraumMedia\Watchlist\Domain\Model\Item;
/**
* Defines interface of concrete handler implementations per item type.
*
* Each item has a type, and each type has an handler.
* The handler needs to take care of creating the item instances.
*/
interface ItemHandlerInterface
{
/**
* Returns an item instance based on its identifier.
*/
public function return(string $identifier): ?Item;
/**
* Returns the type the handler can handle, e.g. "pages" or "tt_address".
*/
public function handlesType(): string;
}

View file

@ -0,0 +1,55 @@
<?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 WerkraumMedia\Watchlist\Domain;
class ItemHandlerRegistry
{
/**
* @var ItemHandlerInterface[]
*/
private array $itemHandler = [];
public function exists(string $type): bool
{
return isset($this->itemHandler[$type]);
}
public function get(string $type): ItemHandlerInterface
{
return $this->itemHandler[$type];
}
public function add(ItemHandlerInterface $itemHandler): void
{
$this->itemHandler[$itemHandler->handlesType()] = $itemHandler;
}
/**
* @return string[]
*/
public function getSupportedTypes(): array
{
return array_keys($this->itemHandler);
}
}

View file

@ -0,0 +1,65 @@
<?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 WerkraumMedia\Watchlist\Domain\Items\Page;
use TYPO3\CMS\Core\Database\ConnectionPool;
use WerkraumMedia\Watchlist\Domain\ItemHandlerInterface;
use WerkraumMedia\Watchlist\Domain\Model\Item;
class ItemHandler implements ItemHandlerInterface
{
private ConnectionPool $connectionPool;
public function __construct(
ConnectionPool $connectionPool
) {
$this->connectionPool = $connectionPool;
}
public function return(string $identifier): ?Item
{
$pageUid = (int)$identifier;
$pageRecord = $this->getPageRecord($pageUid);
return new Page(
$pageUid,
(string)$pageRecord['title']
);
}
public function handlesType(): string
{
return 'page';
}
private function getPageRecord(int $uid): array
{
$qb = $this->connectionPool->getQueryBuilderForTable('pages');
$qb->select('title');
$qb->from('pages');
$qb->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid)));
$qb->setMaxResults(1);
return $qb->execute()->fetchAssociative() ?: [];
}
}

View file

@ -0,0 +1,51 @@
<?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 WerkraumMedia\Watchlist\Domain\Items\Page;
use WerkraumMedia\Watchlist\Domain\Model\Item;
class Page implements Item
{
private int $pageUid;
private string $title;
public function __construct(
int $pageUid,
string $title
) {
$this->pageUid = $pageUid;
$this->title = $title;
}
public function getUniqueIdentifier(): string
{
return 'page-' . $this->pageUid;
}
public function getTitle(): string
{
return $this->title;
}
}

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 WerkraumMedia\Watchlist\Domain\Model;
interface Item
{
/**
* Needs to uniquely identify the thing throughout the system.
*/
public function getUniqueIdentifier(): string;
}

View file

@ -0,0 +1,60 @@
<?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 WerkraumMedia\Watchlist\Domain\Model;
class Watchlist
{
/**
* @var Item[]
*/
private array $items = [];
public function getIdentifier(): string
{
return 'default';
}
/**
* @return Item[]
*/
public function getItems(): array
{
return array_values($this->items);
}
public function addItem(Item $item): void
{
$this->items[$item->getUniqueIdentifier()] = $item;
}
public function removeItem(Item $item): void
{
unset($this->items[$item->getUniqueIdentifier()]);
}
public function isEmpty(): bool
{
return $this->items === [];
}
}

View file

@ -0,0 +1,56 @@
<?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 WerkraumMedia\Watchlist\Domain\Repository;
use WerkraumMedia\Watchlist\Domain\ItemHandlerRegistry;
use WerkraumMedia\Watchlist\Domain\Model\Item;
class ItemRepository
{
private ItemHandlerRegistry $registry;
public function __construct(
ItemHandlerRegistry $registry
) {
$this->registry = $registry;
}
public function getByUniqueIdentifier(string $uniqueIdentifier): ?Item
{
[$type, $identifier] = explode('-', $uniqueIdentifier, 2);
if ($this->registry->exists($type)) {
return $this->registry->get($type)->return($identifier);
}
throw new \Exception(
sprintf(
'No item handler for type "%s", only supported: %s.',
$type,
implode(', ', $this->registry->getSupportedTypes()),
),
1664186139
);
}
}

View file

@ -0,0 +1,57 @@
<?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 WerkraumMedia\Watchlist\Domain\Repository;
use WerkraumMedia\Watchlist\Domain\Model\Watchlist;
use WerkraumMedia\Watchlist\Session\SessionServiceInterface;
class WatchlistRepository
{
private SessionServiceInterface $sessionService;
public function __construct(
SessionServiceInterface $sessionService
) {
$this->sessionService = $sessionService;
}
public function getByIdentifier(string $identifier): ?Watchlist
{
$watchlist = $this->sessionService->getWatchlist($identifier);
if ($watchlist instanceof Watchlist) {
return $watchlist;
}
if ($identifier === 'default') {
return new Watchlist();
}
return null;
}
public function update(Watchlist $watchlist): void
{
$this->sessionService->update($watchlist);
}
}

View file

@ -0,0 +1,59 @@
<?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 WerkraumMedia\Watchlist\Extbase\TypeConverter;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
use TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter;
use WerkraumMedia\Watchlist\Domain\Model\Item;
use WerkraumMedia\Watchlist\Domain\Repository\ItemRepository;
class ItemTypeConverter extends AbstractTypeConverter
{
protected $sourceTypes = ['string'];
protected $targetType = Item::class;
protected $priority = 10;
private ItemRepository $itemRepository;
public function __construct(
ItemRepository $itemRepository
) {
$this->itemRepository = $itemRepository;
}
public function convertFrom(
$source,
string $targetType,
array $convertedChildProperties = [],
PropertyMappingConfigurationInterface $configuration = null
): ?Item {
if (is_string($source) === false) {
throw new \InvalidArgumentException('Source was not of expected type string.', 1664197305);
}
return $this->itemRepository->getByUniqueIdentifier($source);
}
}

View file

@ -0,0 +1,58 @@
<?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 WerkraumMedia\Watchlist\Extbase\TypeConverter;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
use TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter;
use WerkraumMedia\Watchlist\Domain\Model\Watchlist;
use WerkraumMedia\Watchlist\Domain\Repository\WatchlistRepository;
class WatchlistTypeConverter extends AbstractTypeConverter
{
protected $sourceTypes = ['string'];
protected $targetType = Watchlist::class;
protected $priority = 10;
private WatchlistRepository $repository;
public function __construct(
WatchlistRepository $repository
) {
$this->repository = $repository;
}
public function convertFrom(
$source,
string $targetType,
array $convertedChildProperties = [],
PropertyMappingConfigurationInterface $configuration = null
): ?Watchlist {
if (is_string($source) === false) {
throw new \InvalidArgumentException('Source was not of expected type string.', 1664197358);
}
return $this->repository->getByIdentifier($source);
}
}

View file

@ -0,0 +1,33 @@
<?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 WerkraumMedia\Watchlist\Session;
use WerkraumMedia\Watchlist\Domain\Model\Watchlist;
interface SessionServiceInterface
{
public function getWatchlist(string $identifier): ?Watchlist;
public function update(Watchlist $watchlist): void;
}

View file

@ -0,0 +1,73 @@
<?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 WerkraumMedia\Watchlist\Session;
use TYPO3\CMS\Extbase\Property\PropertyMapper;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
use WerkraumMedia\Watchlist\Domain\Model\Item;
use WerkraumMedia\Watchlist\Domain\Model\Watchlist;
class Typo3FrontendSessionService implements SessionServiceInterface
{
private PropertyMapper $propertyMapper;
public function __construct(
PropertyMapper $propertyMapper
) {
$this->propertyMapper = $propertyMapper;
}
public function getWatchlist(string $identifier): ?Watchlist
{
$items = $this->getTsfe()->fe_user->getSessionData('watchlist-' . $identifier) ?: [];
if ($items === [] || is_array($items) === false) {
return null;
}
$watchlist = new Watchlist();
array_map(function (string $item) use ($watchlist) {
$mappedItem = $this->propertyMapper->convert($item, Item::class);
if (!$mappedItem instanceof Item) {
return;
}
$watchlist->addItem($mappedItem);
}, $items);
return $watchlist;
}
public function update(Watchlist $watchlist): void
{
$this->getTsfe()->fe_user->setAndSaveSessionData(
'watchlist-' . $watchlist->getIdentifier(),
array_map(fn (Item $item) => $item->getUniqueIdentifier(), $watchlist->getItems())
);
}
private function getTsfe(): TypoScriptFrontendController
{
return $GLOBALS['TSFE'];
}
}

View file

@ -0,0 +1,49 @@
<?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 WerkraumMedia\Watchlist;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use WerkraumMedia\Watchlist\Domain\ItemHandlerRegistry;
return function (ContainerConfigurator $container, ContainerBuilder $containerBuilder) {
$containerBuilder->addCompilerPass(new class() implements CompilerPassInterface {
public function process(ContainerBuilder $container): void
{
$registry = $container->getDefinition(ItemHandlerRegistry::class)
->setShared(true);
foreach (array_keys($container->findTaggedServiceIds('watchlist.itemHandler')) as $id) {
$definition = $container->getDefinition($id);
if (!$definition->isAutoconfigured() || $definition->isAbstract()) {
continue;
}
$registry->addMethodCall('add', [$definition]);
}
}
});
};

View file

@ -0,0 +1,13 @@
services:
_defaults:
autowire: true
autoconfigure: true
public: false
WerkraumMedia\Watchlist\:
resource: '../Classes/*'
exclude:
- '../Classes/Domain/Model/*'
WerkraumMedia\Watchlist\Domain\Items\Page\ItemHandler:
tags: ['watchlist.itemHandler']

View file

@ -0,0 +1,14 @@
<?php
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
defined('TYPO3') || die();
ExtensionUtility::registerPlugin(
'Watchlist',
'Watchlist',
'LLL:EXT:watchlist/Resources/Private/Language/locallang.xlf:plugin.watchlist',
'EXT:watchlist/Resources/Public/Icons/Extension.svg'
);
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist']['watchlist_watchlist'] = 'recursive,select_key,pages';

View file

@ -1,3 +0,0 @@
# watchlist
TYPO3 watchlist extension

53
README.rst Normal file
View file

@ -0,0 +1,53 @@
=========================
TYPO3 Extension Watchlist
=========================
Adds the feature of a watchlist to the frontend of TYPO3 CMS.
Users are able to add "items" to the watchlist.
They are also able to remove things from watchlist and show the current watchlist.
Items can be anything. The extension uses interfaces and developers are able to
connect whatever they want to be a thing that can be added to the watchlist.
Feature set
===========
The extension provides an controller with actions to add and remove items from watchlist.
There is also an action to show the current watchlist.
The extension ships with support for items of type ``pages``,
but this is more a demonstration an used for testing.
Projects are way too different and should provide their own items, see "Custom Items".
Custom Items
============
A developer needs to implement an ``ItemHandler`` and an class representing the ``Item``.
``ItemHandler``
---------------
The class needs to implement the ``WerkraumMedia\Watchlist\Domain\ItemHandlerInterface``.
The purpose is to convert an identifier of that item to an actual instance of that item.
The Handler needs to be registered via Symfony Tags, e.g. via ``Services.yaml``:
.. code:: yam;
services:
_defaults:
autowire: true
autoconfigure: true
public: false
WerkraumMedia\Watchlist\:
resource: '../Classes/*'
WerkraumMedia\Watchlist\Domain\Items\Page\ItemHandler:
tags: ['watchlist.itemHandler']
``Item``
--------
The class needs to implement the ``WerkraumMedia\Watchlist\Domain\Model\Item``.

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="messages" date="2022-09-26T10:43:30Z" product-name="Watchlist">
<header/>
<body>
<trans-unit id="headline.index" xml:space="preserve">
<source>Watchlist</source>
</trans-unit>
<trans-unit id="text.watchlist.isEmpty" xml:space="preserve">
<source>Watchlist is empty</source>
</trans-unit>
</body>
</file>
</xliff>

View file

@ -0,0 +1,18 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<h2>{f:translate(id: 'headline.index', extensionName: 'Watchlist')}</h2>
<f:if condition="{watchlist.empty}">
<f:then>
<p>{f:translate(id: 'text.watchlist.isEmpty', extensionName: 'Watchlist')}
</f:then>
<f:else>
<ol>
<f:for each="{watchlist.items}" as="item">
<li>{item.title}</li>
</f:for>
</ol>
</f:else>
</f:if>
</html>

View file

@ -0,0 +1,144 @@
<?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 WerkraumMedia\Watchlist\Tests\Functional\Frontend;
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalResponse;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
/**
* @covers \WerkraumMedia\Watchlist\Frontend\Basics
*/
class BasicsTest extends FunctionalTestCase
{
protected $coreExtensionsToLoad = [
'fluid_styled_content',
];
protected $testExtensionsToLoad = [
'typo3conf/ext/watchlist',
];
protected $pathsToProvideInTestInstance = [
'typo3conf/ext/watchlist/Tests/Functional/Frontend/Fixtures/Sites/' => 'typo3conf/sites/',
];
protected function setUp(): void
{
parent::setUp();
$this->setUpBackendUserFromFixture(1);
$this->importCSVDataSet(__DIR__ . '/Fixtures/BasicDatabase.csv');
$this->setUpFrontendRootPage(1, [
'EXT:watchlist/Tests/Functional/Frontend/Fixtures/FrontendRendering.typoscript',
'EXT:fluid_styled_content/Configuration/TypoScript/setup.typoscript',
]);
}
/**
* @test
*/
public function watchlistIsRenderedAsEmptyByDefault(): void
{
$request = new InternalRequest();
$request = $request->withPageId(1);
$result = $this->executeFrontendRequest($request);
self::assertSame(200, $result->getStatusCode());
self::assertStringContainsString('Watchlist is empty', $result->getBody()->__toString());
}
/**
* @test
*/
public function canStorePagesOnWatchlistAccrossPageCalls(): void
{
$request = new InternalRequest();
$request = $request->withPageId(1);
$request = $request->withQueryParameter('tx_watchlist_watchlist[redirectUri]', $request->getUri()->__toString());
$request = $request->withQueryParameter('tx_watchlist_watchlist[action]', 'add');
$request = $request->withQueryParameter('tx_watchlist_watchlist[item]', 'page-1');
$result = $this->executeFrontendRequest($request);
self::assertIsRedirect('http://localhost/?id=1', $result);
self::assertHasCookie('fe_typo_user', $result);
$request = new InternalRequest();
$request = $request->withPageId(1);
$request = $request->withHeader('Cookie', self::getCookies($result));
$result = $this->executeFrontendRequest($request);
self::assertStringContainsString('Page Title', $result->getBody()->__toString());
}
/**
* @test
*/
public function canRemoveStoredEntryFromWatchlist(): void
{
$request = new InternalRequest();
$request = $request->withPageId(1);
$request = $request->withQueryParameter('tx_watchlist_watchlist[redirectUri]', $request->getUri()->__toString());
$request = $request->withQueryParameter('tx_watchlist_watchlist[action]', 'add');
$request = $request->withQueryParameter('tx_watchlist_watchlist[item]', 'page-1');
$result = $this->executeFrontendRequest($request);
$cookies = self::getCookies($result);
$request = new InternalRequest();
$request = $request->withHeader('Cookie', $cookies);
$request = $request->withPageId(1);
$request = $request->withQueryParameter('tx_watchlist_watchlist[redirectUri]', $request->getUri()->__toString());
$request = $request->withQueryParameter('tx_watchlist_watchlist[action]', 'remove');
$request = $request->withQueryParameter('tx_watchlist_watchlist[item]', 'page-1');
$result = $this->executeFrontendRequest($request);
self::assertIsRedirect('http://localhost/?id=1', $result);
$request = new InternalRequest();
$request = $request->withHeader('Cookie', $cookies);
$request = $request->withPageId(1);
$result = $this->executeFrontendRequest($request);
self::assertSame(200, $result->getStatusCode());
self::assertStringContainsString('Watchlist is empty', $result->getBody()->__toString());
}
private static function assertIsRedirect(string $redirectLocation, InternalResponse $result): void
{
self::assertSame(303, $result->getStatusCode());
self::assertSame($redirectLocation, $result->getHeader('location')[0] ?? '');
}
private static function assertHasCookie(string $cookieName, InternalResponse $result): void
{
self::assertStringContainsString($cookieName . '=', self::getCookies($result));
}
private static function getCookies(InternalResponse $result): string
{
return explode(' ', $result->getHeader('Set-Cookie')[0] ?? '', 2)[0] ?? '';
}
}

View file

@ -0,0 +1,3 @@
pages
,uid,pid,slug,title
,1,0,/,Page Title
1 pages
2 ,uid,pid,slug,title
3 ,1,0,/,Page Title

View file

@ -0,0 +1,2 @@
page = PAGE
page.10 =< tt_content.watchlist_watchlist.20

View file

@ -0,0 +1,32 @@
base: /
languages:
-
title: Deutsch
enabled: true
base: /
typo3Language: de
locale: de_DE.UTF-8
iso-639-1: de
navigationTitle: Deutsch
hreflang: de-DE
direction: ''
flag: de
websiteTitle: ''
languageId: 0
-
title: English
enabled: true
base: /en
typo3Language: default
locale: en_GB.UTF-8
iso-639-1: en
websiteTitle: ''
navigationTitle: English
hreflang: en-GB
direction: ''
flag: gb
languageId: 1
fallbackType: strict
fallbacks: '0'
rootPageId: 1
websiteTitle: 'Example Website'

View file

@ -0,0 +1,87 @@
<?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 WerkraumMedia\Watchlist\Tests\Unit\Domain\Model;
use PHPUnit\Framework\TestCase;
use WerkraumMedia\Watchlist\Domain\Items\Page\Page;
use WerkraumMedia\Watchlist\Domain\Model\Watchlist;
/**
* @covers \WerkraumMedia\Watchlist\Domain\Model\Watchlist
* @testdox The Watchlist
*/
class WatchlistTest extends TestCase
{
/**
* @test
*/
public function canBeCreated(): void
{
$subject = new Watchlist();
self::assertInstanceOf(
Watchlist::class,
$subject
);
}
/**
* @test
*/
public function doesNotHaveItemsByDefault(): void
{
$subject = new Watchlist();
self::assertCount(0, $subject->getItems());
}
/**
* @test
*/
public function canAddAItem(): void
{
$subject = new Watchlist();
$thing = new Page(1, 'test');
$subject->addItem($thing);
self::assertCount(1, $subject->getItems());
self::assertSame($thing, $subject->getItems()[0]);
}
/**
* @test
*/
public function canRemoveAItem(): void
{
$subject = new Watchlist();
$thing = new Page(1, 'test');
$subject->addItem($thing);
$subject->removeItem($thing);
self::assertCount(0, $subject->getItems());
}
}

80
composer.json Normal file
View file

@ -0,0 +1,80 @@
{
"name": "werkraummedia/watchlist",
"description": "Add a watchlist to frontend of TYPO3",
"type": "typo3-cms-extension",
"license": "GPL-2.0-or-later",
"homepage": "https://github.com/werkraum-media/watchlist",
"support": {
"docs": "https://docs.typo3.org/p/werkraummedia/watchlist/master/en-us/",
"email": "coding@daniel-siepmann.de",
"issues": "https://github.com/werkraum-media/watchlist/issues",
"source": "https://github.com/werkraum-media/watchlist"
},
"authors": [
{
"name": "Daniel Siepmann",
"email": "coding@daniel-siepmann.de",
"homepage": "https://daniel-siepmann.de/",
"role": "Developer"
}
],
"autoload": {
"psr-4": {
"WerkraumMedia\\Watchlist\\": "Classes/"
}
},
"autoload-dev": {
"psr-4": {
"WerkraumMedia\\Watchlist\\Tests\\": "Tests/"
}
},
"require": {
"php": "~7.4.0 || ~8.0.0 || ~8.1.0",
"typo3/cms-backend": "^11.5",
"typo3/cms-core": "^11.5",
"typo3/cms-extbase": "^11.5",
"typo3/cms-frontend": "^11.5"
},
"require-dev": {
"cweagans/composer-patches": "^1.7",
"friendsofphp/php-cs-fixer": "^3.11",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "1.1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^9.5",
"saschaegerer/phpstan-typo3": "^1.1",
"typo3/cms-fluid-styled-content": "^11.5",
"typo3/cms-tstemplate": "^11.5",
"typo3/testing-framework": "^6.6"
},
"config": {
"sort-packages": true,
"allow-plugins": {
"typo3/cms-composer-installers": true,
"typo3/class-alias-loader": true,
"ocramius/package-versions": true,
"phpstan/extension-installer": true,
"cweagans/composer-patches": true
}
},
"extra": {
"typo3/cms": {
"cms-package-dir": "{$vendor-dir}/typo3/cms",
"extension-key": "watchlist",
"web-dir": "public"
}
},
"scripts": {
"post-autoload-dump": [
"@php -r 'is_dir($extFolder=__DIR__.\"/public/typo3conf/ext/\") || mkdir($extFolder, 0777, true);'",
"@php -r 'file_exists($extFolder=__DIR__.\"/public/typo3conf/ext/watchlist\") || symlink(__DIR__,$extFolder);'"
]
},
"extra": {
"patches": {
"typo3/testing-framework": {
"Support cookies in request": "patches/testing-framework-cookies.patch"
}
}
}
}

25
ext_localconf.php Normal file
View file

@ -0,0 +1,25 @@
<?php
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
use WerkraumMedia\Watchlist\Controller\WatchlistController;
use WerkraumMedia\Watchlist\Extbase\TypeConverter\ItemTypeConverter;
use WerkraumMedia\Watchlist\Extbase\TypeConverter\WatchlistTypeConverter;
defined('TYPO3') || die('Access denied.');
(static function (): void {
ExtensionUtility::configurePlugin(
'Watchlist',
'Watchlist',
[
WatchlistController::class => 'index, add, remove',
],
[
WatchlistController::class => 'index',
],
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
);
ExtensionUtility::registerTypeConverter(WatchlistTypeConverter::class);
ExtensionUtility::registerTypeConverter(ItemTypeConverter::class);
})();

View file

@ -0,0 +1,17 @@
diff --git a/Classes/Core/Functional/Framework/Frontend/RequestBootstrap.php b/Classes/Core/Functional/Framework/Frontend/RequestBootstrap.php
index f8b2a40..4f4020d 100644
--- a/Classes/Core/Functional/Framework/Frontend/RequestBootstrap.php
+++ b/Classes/Core/Functional/Framework/Frontend/RequestBootstrap.php
@@ -106,6 +106,12 @@ class RequestBootstrap
}
// Populating $_COOKIE
$_COOKIE = [];
+ if ($this->request->hasHeader('Cookie')) {
+ foreach ($this->request->getHeader('Cookie') as $cookie) {
+ [$cookieName, $cookieValue] = explode('=', $cookie, 2);
+ $_COOKIE[$cookieName] = rtrim($cookieValue, ';');
+ }
+ }
// Setting up the server environment
$_SERVER = [];

12
phpstan.neon Normal file
View file

@ -0,0 +1,12 @@
parameters:
level: max
paths:
- Classes
- Configuration
- Tests
checkMissingIterableValueType: false
reportUnmatchedIgnoredErrors: true
checkGenericClassInNonGenericObjectType: false
ignoreErrors:
- "#^Cannot call method fetchAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#"
- "#^Right side of \\|\\| is always false\\.$#"

38
phpunit.xml.dist Normal file
View file

@ -0,0 +1,38 @@
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
backupGlobals="false"
backupStaticAttributes="false"
beStrictAboutCoversAnnotation="true"
bootstrap="vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTestsBootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertWarningsToExceptions="true"
forceCoversAnnotation="true"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
verbose="false"
>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">Classes</directory>
</include>
</coverage>
<testsuites>
<testsuite name="unit">
<directory>Tests/Unit/</directory>
</testsuite>
<testsuite name="functional">
<directory>Tests/Functional/</directory>
</testsuite>
</testsuites>
<php>
<env name="typo3DatabaseDriver" value="pdo_sqlite"/>
</php>
</phpunit>