├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── StackUnpoly.php └── Unpoly.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `unpoly` will be documented in this file 4 | 5 | ## v2.0.0 - 2022-02-18 6 | 7 | - Drop support for older Symfony versions (965c1c79efbc4b43934fe62c3f3d977a7babb19b) 8 | - Drop support for older PHP versions (1d61619ab600f037c9b16de3f9746b227e07e4d0) 9 | 10 | ## v1.1.1 - 2020-06-17 11 | 12 | - Use getUri to maintain query parameters (#3) 13 | 14 | ## v1.1.0 - 2019-12-03 15 | 16 | - Add support for Symfony 5.0 17 | 18 | ## v1.0.0 - 2019-06-07 19 | 20 | - Initial release 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) The Webstronauts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unpoly 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/webstronauts/unpoly.svg?style=flat-square)](https://packagist.org/packages/webstronauts/unpoly) 4 | [![Build Status](https://img.shields.io/github/actions/workflow/status/webstronauts/php-unpoly/run-tests.yml?branch=main&style=flat-square)](https://github.com/webstronauts/php-unpoly/actions?query=workflow%3Arun-tests) 5 | [![StyleCI](https://github.styleci.io/repos/190603919/shield?branch=master)](https://github.styleci.io/repos/190603919) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/webstronauts/unpoly.svg?style=flat-square)](https://packagist.org/packages/webstronauts/unpoly) 7 | 8 | Stack middleware for handling [Javascript Unpoly Framework](https://unpoly.com) requests. 9 | 10 | 11 | Sponsored by The Webstronauts 12 | 13 | 14 | ## Installation 15 | 16 | You can install the package via [Composer](https://getcomposer.org). 17 | 18 | ```bash 19 | composer require webstronauts/unpoly 20 | ``` 21 | 22 | ## Usage 23 | 24 | You can manually decorate the response with the `Unpoly` object. 25 | 26 | ```php 27 | use Symfony\Component\HttpFoundation\Request; 28 | use Symfony\Component\HttpFoundation\Response; 29 | use Webstronauts\Unpoly\Unpoly; 30 | 31 | // ... 32 | 33 | $unpoly = new Unpoly(); 34 | $unpoly->decorateResponse($request, $response); 35 | ``` 36 | 37 | ### Stack Middleware 38 | 39 | You can decorate the response using the supplied [Stack](http://stackphp.com) middleware. 40 | 41 | ```php 42 | use Webstronauts\Unpoly\StackUnpoly; 43 | use Webstronauts\Unpoly\Unpoly; 44 | 45 | // ... 46 | 47 | $app = new StackUnpoly($app, new Unpoly()); 48 | ``` 49 | 50 | ### Laravel 51 | 52 | To use the package with Laravel, you'll have to wrap it around a middleware instance. 53 | 54 | ```php 55 | decorateResponse($request, $response); 76 | 77 | return $response; 78 | } 79 | } 80 | ``` 81 | 82 | Now use this middleware as described by the [Laravel documentation](https://laravel.com/docs/master/middleware). 83 | 84 | ```php 85 | \App\Http\Middleware\Unpoly::class, 92 | ]; 93 | ``` 94 | 95 | #### Validation Errors 96 | 97 | Whenever a form is submitted through Unpoly, the response is returned as JSON by default. This is because Laravel returns JSON formatted response for any request with the header `X-Requested-With` set to `XMLHttpRequest`. To make sure the application returns an HTML response for any validation errors, overwrite the `convertValidationExceptionToResponse` method in your `App\Exceptions\Handler` class. 98 | 99 | ```php 100 | response) { 107 | return $e->response; 108 | } 109 | 110 | return $request->expectsJson() && ! $request->hasHeader('X-Up-Target') 111 | ? $this->invalidJson($request, $e) 112 | : $this->invalid($request, $e); 113 | } 114 | ``` 115 | 116 | #### Other HTTP Errors 117 | 118 | If your Laravel session expires and a user attempts to navigate or perform an operating on the page using Unpoly, an abrupt JSON error response will be displayed to the user: 119 | 120 | ``` 121 | {'error': 'Unauthenticated.'} 122 | ``` 123 | 124 | To prevent this, create your own `Request` and extend Laravel's built-in `Illuminate\Http\Request`, and override the `expectsJson` method: 125 | 126 | ```php 127 | namespace App\Http; 128 | 129 | use Illuminate\Http\Request as BaseRequest; 130 | 131 | class Request extends BaseRequest 132 | { 133 | public function expectsJson() 134 | { 135 | if ($this->hasHeader('X-Up-Target')) { 136 | return false; 137 | } 138 | 139 | return parent::expectsJson(); 140 | } 141 | } 142 | ``` 143 | 144 | Then, navigate to your `public/index.php` file, and update the usage: 145 | 146 | 147 | ```php 148 | // From... 149 | $response = $kernel->handle( 150 | $request = Illuminate\Http\Request::capture() 151 | ); 152 | 153 | // To... 154 | $response = $kernel->handle( 155 | $request = App\Http\Request::capture() 156 | ); 157 | ``` 158 | 159 | Now when a user session expires, the `` of your page will be replaced with your login page, allowing users to sign back in without refreshing the page. 160 | 161 | ## Testing 162 | 163 | ``` bash 164 | composer test 165 | ``` 166 | 167 | ## Changelog 168 | 169 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 170 | 171 | ## Credits 172 | 173 | As it's just a simple port of Ruby to PHP code. All credits should go to the Unpoly team and their [unpoly](https://github.com/unpoly/unpoly) gem. 174 | 175 | - [Robin van der Vleuten](https://github.com/robinvdvleuten) 176 | - [All Contributors](../../contributors) 177 | 178 | ## License 179 | 180 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 181 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webstronauts/unpoly", 3 | "description": "Stack middleware for handling Javascript Unpoly Framework requests", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Robin van der Vleuten", 9 | "email": "robin@webstronauts.com", 10 | "homepage": "https://webstronauts.com" 11 | } 12 | ], 13 | "homepage" : "https://github.com/webstronauts/php-unpoly", 14 | "autoload": { 15 | "psr-4": { 16 | "Webstronauts\\Unpoly\\": "src" 17 | } 18 | }, 19 | "autoload-dev": { 20 | "psr-4": { 21 | "Webstronauts\\Unpoly\\Tests\\": "tests" 22 | } 23 | }, 24 | "require": { 25 | "php": "^8.0", 26 | "symfony/http-foundation": "^5.4|^6.0|^7.0", 27 | "symfony/http-kernel": "^5.4|^6.0|^7.0" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": "^9.5" 31 | }, 32 | "scripts": { 33 | "test": "vendor/bin/phpunit", 34 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 35 | 36 | }, 37 | "config": { 38 | "sort-packages": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/StackUnpoly.php: -------------------------------------------------------------------------------- 1 | app = $app; 30 | $this->unpoly = $unpoly; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = true): Response 37 | { 38 | $response = $this->app->handle($request, $type, $catch); 39 | 40 | if (self::MAIN_REQUEST === $type) { 41 | $this->unpoly->decorateResponse($request, $response); 42 | } 43 | 44 | return $response; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Unpoly.php: -------------------------------------------------------------------------------- 1 | echoRequestHeaders($request, $response); 43 | $this->appendMethodCookie($request, $response); 44 | } 45 | 46 | /** 47 | * Unpoly requires these headers to detect redirects, 48 | * 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 53 | */ 54 | protected function echoRequestHeaders(Request $request, Response $response): void 55 | { 56 | $response->headers->add([ 57 | self::LOCATION_RESPONSE_HEADER => $request->getUri(), 58 | self::METHOD_RESPONSE_HEADER => $request->getMethod(), 59 | ]); 60 | } 61 | 62 | /** 63 | * Unpoly requires this cookie to detect whether the initial page 64 | * load was requested using a non-GET method. In this case the Unpoly 65 | * framework will prevent itself from booting until it was loaded 66 | * from a GET request. 67 | * 68 | * @see https://github.com/rails/turbolinks/search?q=request_method&ref=cmdform 69 | * @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 74 | */ 75 | protected function appendMethodCookie(Request $request, Response $response): void 76 | { 77 | if (! $request->isMethod('GET') && ! $request->headers->has('X-Up-Target')) { 78 | $response->headers->setCookie(new Cookie(self::METHOD_COOKIE_NAME, $request->getMethod())); 79 | } else { 80 | $response->headers->removeCookie(self::METHOD_COOKIE_NAME); 81 | } 82 | } 83 | } 84 | --------------------------------------------------------------------------------