Skip to content

Commit bd8f4ad

Browse files
Add support for Unpoly v2
1 parent b91944b commit bd8f4ad

File tree

2 files changed

+233
-19
lines changed

2 files changed

+233
-19
lines changed

src/Unpoly.php

Lines changed: 177 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,198 @@ class Unpoly
1010
{
1111
/**
1212
* Response header to echo request's URL.
13-
*
14-
* @var string
1513
*/
1614
const LOCATION_RESPONSE_HEADER = 'X-Up-Location';
1715

1816
/**
1917
* Response header to echo request's method.
20-
*
21-
* @var string
2218
*/
2319
const METHOD_RESPONSE_HEADER = 'X-Up-Method';
2420

2521
/**
2622
* Cookie name to echo request's method.
27-
*
28-
* @var string
2923
*/
3024
const METHOD_COOKIE_NAME = '_up_method';
3125

26+
/**
27+
* @see Webstronauts\Unpoly\Unpoly::isUnpolyRequest()
28+
*/
29+
public static function isUpRequest(Request $request): bool
30+
{
31+
return static::isUnpolyRequest($request);
32+
}
33+
34+
/**
35+
* Returns whether the current request is a [page fragment update](https://unpoly.com/up.replace)
36+
* triggered by an Unpoly frontend.
37+
*
38+
* This will eventually just check for the `X-Up-Version header`.
39+
* Just in case a user still has an older version of Unpoly running on the frontend,
40+
* we also check for the X-Up-Target header.
41+
*/
42+
public static function isUnpolyRequest(Request $request): bool
43+
{
44+
return static::getVersion($request) !== null || static::getTarget($request) !== null;
45+
}
46+
47+
/**
48+
* Returns the current Unpoly version.
49+
*
50+
* The version is guaranteed to be set for all Unpoly requests.
51+
*/
52+
public static function getVersion(Request $request): ?string
53+
{
54+
return $request->headers->get('X-Up-Version');
55+
}
56+
57+
/**
58+
* Returns the mode of the targeted layer.
59+
*
60+
* Server-side code is free to render different HTML for different modes.
61+
* For example, you might prefer to not render a site navigation for overlays.
62+
*/
63+
public static function getMode(Request $request): ?string
64+
{
65+
return $request->headers->get('X-Up-Mode');
66+
}
67+
68+
/**
69+
* Returns the mode of the layer targeted for a failed fragment update.
70+
*
71+
* A fragment update is considered failed if the server responds with
72+
* a status code other than 2xx, but still renders HTML.
73+
* Server-side code is free to render different HTML for different modes.
74+
* For example, you might prefer to not render a site navigation for overlays.
75+
*/
76+
public static function getFailMode(Request $request): ?string
77+
{
78+
return $request->headers->get('X-Up-Fail-Mode');
79+
}
80+
81+
/**
82+
* Returns the CSS selector for a fragment that Unpoly will update in
83+
* case of a successful response (200 status code).
84+
*
85+
* The Unpoly frontend will expect an HTML response containing an element
86+
* that matches this selector.
87+
*
88+
* Server-side code is free to optimize its successful response by only returning HTML
89+
* that matches this selector.
90+
*/
91+
public static function getTarget(Request $request): ?string
92+
{
93+
return $request->headers->get('X-Up-Target');
94+
}
95+
96+
/**
97+
* Returns the CSS selector for a fragment that Unpoly will update in
98+
* case of an failed response. Server errors or validation failures are
99+
* all examples for a failed response (non-200 status code).
100+
*
101+
* The Unpoly frontend will expect an HTML response containing an element
102+
* that matches this selector.
103+
*
104+
* Server-side code is free to optimize its response by only returning HTML
105+
* that matches this selector.
106+
*/
107+
public static function getFailTarget(Request $request): ?string
108+
{
109+
return $request->headers->get('X-Up-Fail-Target');
110+
}
111+
112+
/**
113+
* Returns whether the given CSS selector is targeted by the current fragment
114+
* update in case of a successful response (200 status code).
115+
*
116+
* Note that the matching logic is very simplistic and does not actually know
117+
* how your page layout is structured. It will return `true` if
118+
* the tested selector and the requested CSS selector matches exactly, or if the
119+
* requested selector is `body` or `html`.
120+
*
121+
* Always returns `true` if the current request is not an Unpoly fragment update.
122+
*/
123+
public static function isTarget(Request $request, string $target): bool
124+
{
125+
return static::queryTarget(static::getTarget($request), $target);
126+
}
127+
128+
/**
129+
* Returns whether the given CSS selector is targeted by the current fragment
130+
* update in case of a failed response (non-200 status code).
131+
*
132+
* Note that the matching logic is very simplistic and does not actually know
133+
* how your page layout is structured. It will return `true` if
134+
* the tested selector and the requested CSS selector matches exactly, or if the
135+
* requested selector is `body` or `html`.
136+
*
137+
* Always returns `true` if the current request is not an Unpoly fragment update.
138+
*/
139+
public static function isFailTarget(Request $request, string $target): bool
140+
{
141+
return static::queryTarget(static::getFailTarget($request), $target);
142+
}
143+
144+
/**
145+
* Returns whether the given CSS selector is targeted by the current fragment
146+
* update for either a success or a failed response.
147+
*
148+
* Note that the matching logic is very simplistic and does not actually know
149+
* how your page layout is structured. It will return `true` if
150+
* the tested selector and the requested CSS selector matches exactly, or if the
151+
* requested selector is `body` or `html`.
152+
*
153+
* Always returns `true` if the current request is not an Unpoly fragment update.
154+
*/
155+
public static function isAnyTarget(Request $request, string $target): bool
156+
{
157+
return static::isTarget($request, $target) || static::isFailTarget($request, $target);
158+
}
159+
160+
/**
161+
* Returns whether the current form submission should be
162+
* [validated](https://unpoly.com/input-up-validate) (and not be saved to the database).
163+
*/
164+
public static function isValidationRequest(Request $request): bool
165+
{
166+
return static::getValidateNames($request) !== null;
167+
}
168+
169+
/**
170+
* If the current form submission is a [validation](https://unpoly.com/input-up-validate),
171+
* this returns the name attributes of the form field that has triggered
172+
* the validation.
173+
*
174+
* Note that multiple validating form fields may be batched into a single request.
175+
*/
176+
public static function getValidateNames(Request $request): ?string
177+
{
178+
return $request->headers->get('X-Up-Validate');
179+
}
180+
181+
protected static function queryTarget(string $actualTarget, string $testedTarget): bool
182+
{
183+
if (! static::isUnpolyRequest($request)) {
184+
return true;
185+
}
186+
187+
if ($actualTarget === $testedTarget) {
188+
return true;
189+
}
190+
191+
if ($actualTarget === 'html') {
192+
return true;
193+
}
194+
195+
if ($actualTarget === 'body' && ! in_array($testedTarget, ['head', 'title', 'meta'])) {
196+
return true;
197+
}
198+
199+
return false;
200+
}
201+
32202
/**
33203
* Modifies the HTTP headers and cookies of the response so that it can be
34204
* properly handled by the Unpoly javascript.
35-
*
36-
* @param \Symfony\Component\HttpFoundation\Request $request
37-
* @param \Symfony\Component\HttpFoundation\Response $response
38-
* @return void
39205
*/
40206
public function decorateResponse(Request $request, Response $response): void
41207
{
@@ -46,10 +212,6 @@ public function decorateResponse(Request $request, Response $response): void
46212
/**
47213
* Unpoly requires these headers to detect redirects,
48214
* which are otherwise undetectable for an AJAX client.
49-
*
50-
* @param \Symfony\Component\HttpFoundation\Request $request
51-
* @param \Symfony\Component\HttpFoundation\Response $response
52-
* @return void
53215
*/
54216
protected function echoRequestHeaders(Request $request, Response $response): void
55217
{
@@ -67,14 +229,10 @@ protected function echoRequestHeaders(Request $request, Response $response): voi
67229
*
68230
* @see https://github.com/rails/turbolinks/search?q=request_method&ref=cmdform
69231
* @see https://github.com/rails/turbolinks/blob/83d4b3d2c52a681f07900c28adb28bc8da604733/README.md#initialization
70-
*
71-
* @param \Symfony\Component\HttpFoundation\Request $request
72-
* @param \Symfony\Component\HttpFoundation\Response $response
73-
* @return void
74232
*/
75233
protected function appendMethodCookie(Request $request, Response $response): void
76234
{
77-
if (! $request->isMethod('GET') && ! $request->headers->has('X-Up-Target')) {
235+
if (! $request->isMethod('GET') && ! static::isUpRequest($request)) {
78236
$response->headers->setCookie(new Cookie(self::METHOD_COOKIE_NAME, $request->getMethod()));
79237
} else {
80238
$response->headers->removeCookie(self::METHOD_COOKIE_NAME);

tests/UnpolyTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,62 @@
1010

1111
class UnpolyTest extends TestCase
1212
{
13+
public function testChecksIfUnpolyRequestBasedOnVersion()
14+
{
15+
$request = Request::create('/foo/bar');
16+
$request->headers->set('X-Up-Version', '2.0.0');
17+
18+
$this->assertTrue(Unpoly::isUnpolyRequest($request));
19+
}
20+
21+
public function testChecksIfUnpolyRequestBasedOnTarget()
22+
{
23+
$request = Request::create('/foo/bar');
24+
$request->headers->set('X-Up-Target', '.css.selector');
25+
26+
$this->assertTrue(Unpoly::isUnpolyRequest($request));
27+
}
28+
29+
public function testVersionReturnsVersionFromHeader()
30+
{
31+
$request = Request::create('/foo/bar');
32+
$request->headers->set('X-Up-Version', '2.0.0');
33+
34+
$this->assertEquals('2.0.0', Unpoly::getVersion($request));
35+
}
36+
37+
public function testModeReturnsModeFromHeader()
38+
{
39+
$request = Request::create('/foo/bar');
40+
$request->headers->set('X-Up-Mode', 'replace');
41+
42+
$this->assertEquals('replace', Unpoly::getMode($request));
43+
}
44+
45+
public function testFailModeReturnsFailModeFromHeader()
46+
{
47+
$request = Request::create('/foo/bar');
48+
$request->headers->set('X-Up-Fail-Mode', 'replace');
49+
50+
$this->assertEquals('replace', Unpoly::getFailMode($request));
51+
}
52+
53+
public function testTargetReturnsSelectorFromHeader()
54+
{
55+
$request = Request::create('/foo/bar');
56+
$request->headers->set('X-Up-Target', '.css.selector');
57+
58+
$this->assertEquals('.css.selector', Unpoly::getTarget($request));
59+
}
60+
61+
public function testFailTargetReturnsSelectorFromHeader()
62+
{
63+
$request = Request::create('/foo/bar');
64+
$request->headers->set('X-Up-Fail-Target', '.css.selector');
65+
66+
$this->assertEquals('.css.selector', Unpoly::getFailTarget($request));
67+
}
68+
1369
public function testAppendsRequestHeadersToResponse()
1470
{
1571
$request = Request::create('/foo/bar?param=baz', 'PUT');

0 commit comments

Comments
 (0)