Skip to content

Commit fa0757c

Browse files
Introduce Controls trait
Add a `Controls` trait that provides the factory function `createViewmodeSwitcher()` and a `handleControls()` function to handle the requests of all tracked controls.
1 parent c7516f9 commit fa0757c

2 files changed

Lines changed: 375 additions & 0 deletions

File tree

src/Common/Controls.php

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
namespace ipl\Web\Common;
4+
5+
use Icinga\Web\UrlParams;
6+
use InvalidArgumentException;
7+
use ipl\Html\Form;
8+
use ipl\Web\Compat\CompatController;
9+
use ipl\Web\Control\LimitControl;
10+
use ipl\Web\Control\PaginationControl;
11+
use ipl\Web\Control\ViewModeSwitcher;
12+
use ipl\Web\Url;
13+
use Psr\Http\Message\ServerRequestInterface;
14+
15+
/**
16+
* Provides factory methods to prepare reusable web controls and allows to handle their requests
17+
*
18+
* @phpstan-require-extends CompatController
19+
*/
20+
trait Controls
21+
{
22+
/** @var Form[] Controls registered for handling by {@see self::handleControls()} */
23+
private array $trackedControls = [];
24+
25+
/** @var ?Url Url to redirect to in {@see self::handleControls()} */
26+
private ?Url $redirectUrl = null;
27+
28+
/**
29+
* Create a {@see ViewModeSwitcher} control
30+
*
31+
* The control is registered via {@see self::trackControl()} and gets a default {@see Form::ON_SUBMIT} handler
32+
* that writes the chosen view mode to the {@see self::getRedirectUrl()}.
33+
*
34+
* @param UrlParams $params The url params; the view mode param is shifted out of them
35+
* @param ?string $viewModeParam Custom view mode param, null uses the default of the given class
36+
* @param class-string<ViewModeSwitcher> $viewModeSwitcherClass
37+
*
38+
* @return ViewModeSwitcher
39+
*
40+
* @throws InvalidArgumentException
41+
*/
42+
public function createViewModeSwitcher(
43+
UrlParams $params,
44+
?string $viewModeParam = null,
45+
string $viewModeSwitcherClass = ViewModeSwitcher::class
46+
): ViewModeSwitcher {
47+
if (! is_a($viewModeSwitcherClass, ViewModeSwitcher::class, true)) {
48+
throw new InvalidArgumentException(
49+
sprintf('%s is not a subclass of ViewModeSwitcher', $viewModeSwitcherClass)
50+
);
51+
}
52+
53+
$viewModeSwitcher = new $viewModeSwitcherClass();
54+
if ($viewModeParam !== null) {
55+
$viewModeSwitcher->setViewModeParam($viewModeParam);
56+
}
57+
58+
$viewModeSwitcher->populate([
59+
$viewModeSwitcher->getViewModeParam() => $params->shift($viewModeSwitcher->getViewModeParam())
60+
]);
61+
62+
$this->trackControl($viewModeSwitcher);
63+
64+
$viewModeSwitcher->on(ViewModeSwitcher::ON_SUBMIT, function (ViewModeSwitcher $switcher): void {
65+
$this->getRedirectUrl()->setParam($switcher->getViewModeParam(), $switcher->getViewMode());
66+
});
67+
68+
return $viewModeSwitcher;
69+
}
70+
71+
/**
72+
* Double the default item limit and page size for `minimal` view mode
73+
*
74+
* @param LimitControl $limitControl
75+
* @param ?PaginationControl $paginationControl
76+
*
77+
* @return void
78+
*/
79+
protected function applyViewModeLimit(
80+
LimitControl $limitControl,
81+
?PaginationControl $paginationControl = null
82+
): void {
83+
$viewModeSwitcher = $this->getTrackedControl(ViewModeSwitcher::class);
84+
if ($viewModeSwitcher?->getViewMode() === 'minimal') {
85+
$limitControl->setDefaultLimit($limitControl->getDefaultLimit() * 2);
86+
87+
$paginationControl
88+
?->setDefaultPageSize($paginationControl->getDefaultPageSize() * 2)
89+
->apply();
90+
}
91+
}
92+
93+
/**
94+
* Register the given control to be handled by {@see self::handleControls()}
95+
*
96+
* @param Form $control
97+
*
98+
* @return $this
99+
*/
100+
protected function trackControl(Form $control): static
101+
{
102+
$this->trackedControls[] = $control;
103+
104+
return $this;
105+
}
106+
107+
/**
108+
* Get the tracked control of the given type
109+
*
110+
* @template TForm of Form
111+
*
112+
* @param class-string<TForm> $type
113+
*
114+
* @return ?TForm
115+
*/
116+
protected function getTrackedControl(string $type): ?Form
117+
{
118+
foreach ($this->trackedControls as $control) {
119+
if ($control instanceof $type) {
120+
return $control;
121+
}
122+
}
123+
124+
return null;
125+
}
126+
127+
/**
128+
* Get the Url {@see self::handleControls()} redirects to
129+
*
130+
* @return Url
131+
*/
132+
protected function getRedirectUrl(): Url
133+
{
134+
return $this->redirectUrl ??= Url::fromRequest();
135+
}
136+
137+
/**
138+
* Call {@see Form::handleRequest()} on every control in {@see self::$trackedControls}. If {@see self::$redirectUrl}
139+
* has been set, a redirect is performed afterwards.
140+
*
141+
* @param ServerRequestInterface $request
142+
*
143+
* @return $this
144+
*/
145+
protected function handleControls(ServerRequestInterface $request): static
146+
{
147+
foreach ($this->trackedControls as $control) {
148+
$control->handleRequest($request);
149+
}
150+
151+
if ($this->redirectUrl !== null) {
152+
$this->redirectNow($this->redirectUrl);
153+
}
154+
155+
return $this;
156+
}
157+
}

tests/Common/ControlsTest.php

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
<?php
2+
3+
namespace ipl\Tests\Web\Common;
4+
5+
use Icinga\Web\UrlParams;
6+
use InvalidArgumentException;
7+
use ipl\Html\Form;
8+
use ipl\I18n\NoopTranslator;
9+
use ipl\I18n\StaticTranslator;
10+
use ipl\Web\Common\Controls;
11+
use ipl\Web\Control\LimitControl;
12+
use ipl\Web\Control\PaginationControl;
13+
use ipl\Web\Control\ViewModeSwitcher;
14+
use ipl\Tests\Web\TestCase;
15+
use Psr\Http\Message\ServerRequestInterface;
16+
17+
class ControlsTest extends TestCase
18+
{
19+
protected function setUp(): void
20+
{
21+
StaticTranslator::$instance = new NoopTranslator();
22+
}
23+
24+
public function testCreateViewModeSwitcher(): void
25+
{
26+
if (! class_exists('Icinga\Web\UrlParams')) {
27+
$this->markTestSkipped('This test only runs locally');
28+
}
29+
30+
$controller = $this->controls();
31+
32+
$params = UrlParams::fromQueryString('view=minimal&foo=bar');
33+
$switcher = $controller->createViewModeSwitcher($params);
34+
35+
$this->assertSame('minimal', $switcher->getViewMode(), 'The view mode should be populated from the param');
36+
$this->assertFalse($params->has('view'), 'The view mode param should be shifted out of the params');
37+
$this->assertSame('bar', $params->get('foo'), 'Other params should be left untouched');
38+
$this->assertContains($switcher, $controller->trackedControls(), 'The created switcher should be tracked');
39+
40+
$customParams = UrlParams::fromQueryString('layout=detailed');
41+
$customSwitcher = $controller->createViewModeSwitcher($customParams, 'layout');
42+
43+
$this->assertSame(
44+
'detailed',
45+
$customSwitcher->getViewMode(),
46+
'The view mode should be read from the custom param'
47+
);
48+
$this->assertFalse(
49+
$customParams->has('layout'),
50+
'The custom view mode param should be shifted out of the params'
51+
);
52+
}
53+
54+
public function testCreateViewModeSwitcherResolvesTheDefaultViewMode(): void
55+
{
56+
if (! class_exists('Icinga\Web\UrlParams')) {
57+
$this->markTestSkipped('This test only runs locally');
58+
}
59+
60+
$default = $this->controls()->createViewModeSwitcher(UrlParams::fromQueryString('foo=bar'));
61+
62+
$this->assertSame(
63+
ViewModeSwitcher::DEFAULT_VIEW_MODE,
64+
$default->getViewMode(),
65+
'Without a param the default view mode should apply'
66+
);
67+
68+
$default->setDefaultViewMode('detailed');
69+
70+
$this->assertSame(
71+
'detailed',
72+
$default->getViewMode(),
73+
'A default set after creation should be honored, as it is read lazily'
74+
);
75+
76+
$withParam = $this->controls()->createViewModeSwitcher(UrlParams::fromQueryString('view=minimal'));
77+
$withParam->setDefaultViewMode('detailed');
78+
79+
$this->assertSame(
80+
'minimal',
81+
$withParam->getViewMode(),
82+
'A present param should take precedence over the default'
83+
);
84+
}
85+
86+
public function testCreateViewModeSwitcherWithCustomClass(): void
87+
{
88+
if (! class_exists('Icinga\Web\UrlParams')) {
89+
$this->markTestSkipped('This test only runs locally');
90+
}
91+
92+
$customClass = get_class(new class extends ViewModeSwitcher {
93+
});
94+
95+
$switcher = $this->controls()->createViewModeSwitcher(
96+
UrlParams::fromQueryString('view=minimal'),
97+
viewModeSwitcherClass: $customClass
98+
);
99+
100+
$this->assertInstanceOf($customClass, $switcher);
101+
}
102+
103+
public function testCreateViewModeSwitcherThrowsOnClassThatIsNoViewModeSwitcher(): void
104+
{
105+
if (! class_exists('Icinga\Web\UrlParams')) {
106+
$this->markTestSkipped('This test only runs locally');
107+
}
108+
109+
$this->expectException(InvalidArgumentException::class);
110+
111+
$this->controls()->createViewModeSwitcher(
112+
UrlParams::fromQueryString(''),
113+
viewModeSwitcherClass: \stdClass::class
114+
);
115+
}
116+
117+
public function testApplyViewModeLimitDoublesLimitsInMinimalMode(): void
118+
{
119+
$switcher = $this->createMock(ViewModeSwitcher::class);
120+
$switcher->method('getViewMode')->willReturn('minimal');
121+
122+
$limitControl = $this->createMock(LimitControl::class);
123+
$limitControl->method('getDefaultLimit')->willReturn(25);
124+
$limitControl->expects($this->once())->method('setDefaultLimit')->with(50);
125+
126+
$paginationControl = $this->createMock(PaginationControl::class);
127+
$paginationControl->method('getDefaultPageSize')->willReturn(25);
128+
$paginationControl->expects($this->once())
129+
->method('setDefaultPageSize')
130+
->with(50)
131+
->willReturnSelf();
132+
$paginationControl->expects($this->once())->method('apply')->willReturnSelf();
133+
134+
$controller = $this->controls();
135+
$controller->track($switcher);
136+
$controller->applyLimit($limitControl, $paginationControl);
137+
}
138+
139+
public function testApplyViewModeLimitLeavesLimitsUntouchedInNonMinimalMode(): void
140+
{
141+
$switcher = $this->createMock(ViewModeSwitcher::class);
142+
$switcher->method('getViewMode')->willReturn('common');
143+
144+
$limitControl = $this->createMock(LimitControl::class);
145+
$limitControl->expects($this->never())->method('setDefaultLimit');
146+
147+
$paginationControl = $this->createMock(PaginationControl::class);
148+
$paginationControl->expects($this->never())->method('setDefaultPageSize');
149+
150+
$controller = $this->controls();
151+
$controller->track($switcher);
152+
$controller->applyLimit($limitControl, $paginationControl);
153+
}
154+
155+
public function testApplyViewModeLimitDoesNothingWithoutTrackedSwitcher(): void
156+
{
157+
$limitControl = $this->createMock(LimitControl::class);
158+
$limitControl->expects($this->never())->method('setDefaultLimit');
159+
160+
$this->controls()->applyLimit($limitControl);
161+
}
162+
163+
public function testHandleControls(): void
164+
{
165+
$request = $this->createMock(ServerRequestInterface::class);
166+
167+
$first = $this->createMock(Form::class);
168+
$first->expects($this->once())->method('handleRequest')->with($request);
169+
170+
$second = $this->createMock(Form::class);
171+
$second->expects($this->once())->method('handleRequest')->with($request);
172+
173+
$controller = $this->controls();
174+
$controller->track($first);
175+
$controller->track($second);
176+
177+
$this->assertSame(
178+
[$first, $second],
179+
$controller->trackedControls(),
180+
'Controls should be tracked in registration order'
181+
);
182+
183+
$controller->handle($request);
184+
}
185+
186+
/**
187+
* Get a controller using the {@see Controls} trait, with public seams onto its protected orchestration
188+
*/
189+
private function controls(): object
190+
{
191+
return new class {
192+
use Controls;
193+
194+
/** @return Form[] */
195+
public function trackedControls(): array
196+
{
197+
return $this->trackedControls;
198+
}
199+
200+
public function track(Form $control): void
201+
{
202+
$this->trackControl($control);
203+
}
204+
205+
public function handle(ServerRequestInterface $request): void
206+
{
207+
$this->handleControls($request);
208+
}
209+
210+
public function applyLimit(
211+
LimitControl $limitControl,
212+
?PaginationControl $paginationControl = null
213+
): void {
214+
$this->applyViewModeLimit($limitControl, $paginationControl);
215+
}
216+
};
217+
}
218+
}

0 commit comments

Comments
 (0)