├── tests ├── test_app │ ├── plugins │ │ └── .gitkeep │ ├── webroot │ │ ├── js │ │ │ └── app.js │ │ └── mix-manifest.json │ ├── templates │ │ ├── Error │ │ │ ├── error400.php │ │ │ └── error500.php │ │ └── layout │ │ │ └── default.php │ ├── config │ │ ├── bootstrap.php │ │ └── routes.php │ └── src │ │ ├── Controller │ │ ├── AppController.php │ │ ├── ErrorController.php │ │ └── UsersController.php │ │ └── Application.php ├── TestCase │ ├── PluginTest.php │ ├── View │ │ ├── Helper │ │ │ └── InertiaHelperTest.php │ │ ├── InertiaJsonViewTest.php │ │ └── InertiaWebViewTest.php │ └── Controller │ │ └── UsersControllerTest.php └── bootstrap.php ├── templates └── Inertia │ └── app.php ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── run-phpstan.yml │ ├── check-coding-style.yml │ └── run-tests.yml ├── CHANGELOG.md ├── phpstan.neon.dist ├── config └── routes.php ├── phpcs.xml ├── .editorconfig ├── docs ├── README.md ├── ClientSideSetup.md ├── Installation.md └── ServerSideSetup.md ├── src ├── Utility │ └── Message.php ├── View │ ├── InertiaJsonView.php │ ├── InertiaWebView.php │ ├── Helper │ │ └── InertiaHelper.php │ └── BaseViewTrait.php ├── InertiaPlugin.php ├── Middleware │ └── InertiaMiddleware.php └── Controller │ └── InertiaResponseTrait.php ├── .gitignore ├── phpunit.xml.dist ├── LICENSE ├── composer.json ├── package.json └── README.md /tests/test_app/plugins/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_app/webroot/js/app.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_app/templates/Error/error400.php: -------------------------------------------------------------------------------- 1 | Test app template error 400 2 | -------------------------------------------------------------------------------- /tests/test_app/templates/Error/error500.php: -------------------------------------------------------------------------------- 1 | Test app template error 500 2 | -------------------------------------------------------------------------------- /templates/Inertia/app.php: -------------------------------------------------------------------------------- 1 | Inertia->make($page, 'app', ''); 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: ishanvyas 4 | -------------------------------------------------------------------------------- /tests/test_app/webroot/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/js/app.js": "/js/app.js?id=123456" 3 | } 4 | -------------------------------------------------------------------------------- /tests/test_app/config/bootstrap.php: -------------------------------------------------------------------------------- 1 | plugin( 8 | 'Inertia', 9 | ['path' => '/inertia'], 10 | function (RouteBuilder $routes) { 11 | $routes->fallbacks(DashedRoute::class); 12 | } 13 | ); 14 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 0 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/test_app/templates/layout/default.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Html->charset() ?> 5 | 6 | Inertia TestApp 7 | 8 | AssetMix->script('app') ?> 9 | 10 | fetch('meta') ?> 11 | fetch('css') ?> 12 | fetch('script') ?> 13 | 14 | 15 | fetch('content') ?> 16 | 17 | 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.js] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.vue] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [*.yml] 22 | indent_style = space 23 | indent_size = 2 24 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | I assume you know that [what Inertia.js is](https://inertiajs.com/#top), [what it is for](https://inertiajs.com/who-is-it-for), and [how it works](https://inertiajs.com/how-it-works). If not, I will encourage you to read the [official Inertia.js documentation](https://inertiajs.com/) before diving into this plugin further more. 4 | 5 | - [Installation](Installation.md) 6 | - [Usage](ServerSideSetup.md) 7 | - [Server-side Setup](ServerSideSetup.md) 8 | - [Client-side Setup](ClientSideSetup.md) 9 | -------------------------------------------------------------------------------- /tests/test_app/config/routes.php: -------------------------------------------------------------------------------- 1 | scope('/', function (RouteBuilder $routes) { 10 | // Register scoped middleware for in scopes. 11 | $routes->registerMiddleware('csrf', new CsrfProtectionMiddleware([ 12 | 'httponly' => true, 13 | ])); 14 | $routes->applyMiddleware('csrf'); 15 | 16 | $routes->fallbacks(DashedRoute::class); 17 | }); 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: ishanvyas22 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: ishanvyas22 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/Utility/Message.php: -------------------------------------------------------------------------------- 1 | $this->getComponentName(), 22 | 'url' => $this->getCurrentUri(), 23 | 'props' => $this->getProps(), 24 | ]; 25 | 26 | $this->setConfig('serialize', 'page'); 27 | 28 | $this->set(compact('page')); 29 | 30 | return parent::render($view, $layout); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/test_app/src/Controller/AppController.php: -------------------------------------------------------------------------------- 1 | loadComponent('Flash'); 19 | } 20 | 21 | public function beforeFilter(EventInterface $event) 22 | { 23 | parent::beforeFilter($event); 24 | 25 | // Share data throughout the application 26 | $this->set('app', [ 27 | 'name' => 'InertiaTestApp', 28 | ]); 29 | $this->set('auth', function () { 30 | return ['user' => null]; 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Plugin specific files # 2 | ########################## 3 | /composer.lock 4 | /composer.phar 5 | /phpunit.xml 6 | /phpunit.phar 7 | /config/Migrations/schema-dump-default.lock 8 | /vendor/* 9 | /phpstan.neon 10 | 11 | # OS generated files # 12 | ###################### 13 | .DS_Store 14 | .DS_Store? 15 | ._* 16 | .Spotlight-V100 17 | .Trashes 18 | Icon? 19 | ehthumbs.db 20 | Thumbs.db 21 | .directory 22 | 23 | # Tool specific files # 24 | ####################### 25 | # PHPUnit 26 | .phpunit.result.cache 27 | # vim 28 | *~ 29 | *.swp 30 | *.swo 31 | # sublime text & textmate 32 | *.sublime-* 33 | *.stTheme.cache 34 | *.tmlanguage.cache 35 | *.tmPreferences.cache 36 | # Eclipse 37 | .settings/* 38 | # JetBrains, aka PHPStorm, IntelliJ IDEA 39 | .idea/* 40 | # NetBeans 41 | nbproject/* 42 | # Visual Studio Code 43 | .vscode 44 | # Sass preprocessor 45 | .sass-cache/ 46 | .phpunit.cache/* 47 | -------------------------------------------------------------------------------- /src/View/InertiaWebView.php: -------------------------------------------------------------------------------- 1 | loadHelper('Inertia.Inertia'); 21 | $this->loadHelper('AssetMix.AssetMix'); 22 | } 23 | 24 | /** 25 | * @inheritDoc 26 | */ 27 | public function render(?string $view = null, string|false|null $layout = null): string 28 | { 29 | $page = [ 30 | 'component' => $this->getComponentName(), 31 | 'url' => $this->getCurrentUri(), 32 | 'props' => $this->getProps(), 33 | ]; 34 | 35 | $this->set(compact('page')); 36 | 37 | return parent::render('Inertia./Inertia/app'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | tests/TestCase/ 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | src/ 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/View/Helper/InertiaHelper.php: -------------------------------------------------------------------------------- 1 | ', 31 | $id, 32 | htmlentities($encodedPageData), 33 | $class 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/InertiaPlugin.php: -------------------------------------------------------------------------------- 1 | add(new InertiaMiddleware()); 27 | 28 | return $middleware; 29 | } 30 | 31 | /** 32 | * @inheritDoc 33 | */ 34 | public function bootstrap(PluginApplicationInterface $app): void 35 | { 36 | parent::bootstrap($app); 37 | 38 | $app->addPlugin(AssetMixPlugin::class); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/test_app/src/Application.php: -------------------------------------------------------------------------------- 1 | add(new ErrorHandlerMiddleware(Configure::read('Error'))) 26 | ->add(new AssetMiddleware()) 27 | ->add(new BodyParserMiddleware()) 28 | ->add(new RoutingMiddleware($this)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2023 Ishan Vyas 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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ishanvyas22/cakephp-inertiajs", 3 | "description": "The CakePHP adapter for Inertia.js.", 4 | "type": "cakephp-plugin", 5 | "license": "MIT", 6 | "require": { 7 | "cakephp/cakephp": "^5.0", 8 | "ishanvyas22/asset-mix": "^2.0" 9 | }, 10 | "require-dev": { 11 | "phpunit/phpunit": "^10.2", 12 | "cakephp/cakephp-codesniffer": "^5.1", 13 | "phpstan/phpstan": "^1.10" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Inertia\\": "src/" 18 | } 19 | }, 20 | "autoload-dev": { 21 | "psr-4": { 22 | "Inertia\\Test\\": "tests/", 23 | "TestApp\\": "tests/test_app/src" 24 | } 25 | }, 26 | "scripts": { 27 | "check": [ 28 | "@cs-check", 29 | "@analyse", 30 | "@test" 31 | ], 32 | "cs-fix": "phpcbf --extensions=php ./src ./tests", 33 | "cs-check": "phpcs -p --extensions=php ./src ./tests", 34 | "analyse": "phpstan analyse --ansi --memory-limit=-1", 35 | "test": "phpunit --colors=always" 36 | }, 37 | "config": { 38 | "allow-plugins": { 39 | "dealerdirect/phpcodesniffer-composer-installer": true 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/test_app/src/Controller/ErrorController.php: -------------------------------------------------------------------------------- 1 | viewBuilder()->setTemplatePath('Error'); 41 | } 42 | 43 | /** 44 | * afterFilter callback. 45 | * 46 | * @param \Cake\Event\EventInterface $event Event. 47 | * @return \Cake\Http\Response|null|void 48 | */ 49 | public function afterFilter(EventInterface $event) 50 | { 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "npm run development -- --watch", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "devDependencies": { 13 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 14 | "@inertiajs/inertia": "^0.1.0", 15 | "@inertiajs/inertia-vue": "^0.1.0", 16 | "autosize": "^4.0.2", 17 | "axios": "^0.18", 18 | "cross-env": "^5.1", 19 | "eslint": "^5.14.1", 20 | "eslint-plugin-vue": "^5.2.2", 21 | "fuse.js": "^3.4.2", 22 | "laravel-mix": "^4.0.7", 23 | "lodash": "^4.17.5", 24 | "popper.js": "^1.12", 25 | "portal-vue": "^1.5.1", 26 | "postcss-import": "^12.0.1", 27 | "postcss-nesting": "^7.0.0", 28 | "resolve-url-loader": "^2.3.1", 29 | "tailwindcss": "^0.7.4", 30 | "vue": "^2.6.6", 31 | "vue-template-compiler": "^2.6.6" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/TestCase/PluginTest.php: -------------------------------------------------------------------------------- 1 | bootstrap($app); 26 | $totalPlugins = $app->getPlugins(); 27 | 28 | $this->assertCount(2, $totalPlugins); 29 | $this->assertSame('Inertia', $totalPlugins->get('Inertia')->getName()); 30 | $this->assertSame('AssetMix', $totalPlugins->get('AssetMix')->getName()); 31 | } 32 | 33 | public function testMiddleware() 34 | { 35 | $app = new Application(CONFIG); 36 | $middleware = new MiddlewareQueue(); 37 | 38 | $middleware = $app->middleware($middleware); 39 | 40 | $results = iterator_to_array($middleware); 41 | $this->assertInstanceOf(ErrorHandlerMiddleware::class, $results[0]); 42 | $this->assertInstanceOf(AssetMiddleware::class, $results[1]); 43 | $this->assertInstanceOf(BodyParserMiddleware::class, $results[2]); 44 | $this->assertInstanceOf(RoutingMiddleware::class, $results[3]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/TestCase/View/Helper/InertiaHelperTest.php: -------------------------------------------------------------------------------- 1 | Inertia = new InertiaHelper($view); 32 | } 33 | 34 | /** 35 | * Test initial setup 36 | * 37 | * @return void 38 | */ 39 | public function testItReturnsRootTemplateDiv() 40 | { 41 | $page = [ 42 | 'component' => 'Users/Index', 43 | 'url' => 'http://example.test/users', 44 | 'props' => [ 45 | 'users' => [ 46 | ['id' => 1, 'name' => 'John Doe'], 47 | ['id' => 2, 'name' => 'Jane Doe'], 48 | ], 49 | ], 50 | ]; 51 | 52 | $result = $this->Inertia->make($page, 'app', 'container'); 53 | 54 | $this->assertStringContainsString('
assertStringContainsString('page="{"component":"Users', $result); 56 | $this->assertStringContainsString('class="container">
', $result); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/run-phpstan.yml: -------------------------------------------------------------------------------- 1 | name: Run PHPStan 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'cake5' 7 | - 'cake4' 8 | - 'cake3' 9 | pull_request: 10 | branches: 11 | - '*' 12 | 13 | jobs: 14 | run-phpstan: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | php-versions: ['8.1'] 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v2 25 | 26 | - name: Validate composer.json and composer.lock 27 | run: composer validate 28 | 29 | - name: Get composer cache directory 30 | id: composer-cache 31 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 32 | 33 | - name: Get date part for cache key 34 | id: key-date 35 | run: echo "::set-output name=date::$(date +'%Y-%m')" 36 | 37 | - name: Cache composer dependencies 38 | uses: actions/cache@v2 39 | with: 40 | path: ${{ steps.composer-cache.outputs.dir }} 41 | key: ${{ runner.os }}-${{ github.ref }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }} 42 | restore-keys: | 43 | ${{ runner.os }}-${{ github.ref }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }} 44 | 45 | - name: Setup PHP 46 | uses: shivammathur/setup-php@v2 47 | with: 48 | php-version: ${{ matrix.php-versions }} 49 | extensions: mbstring, intl 50 | 51 | - name: Install dependencies 52 | if: steps.composer-cache.outputs.cache-hit != 'true' 53 | run: composer install --no-interaction 54 | 55 | - name: Run PHPStan command 56 | run: composer analyse 57 | -------------------------------------------------------------------------------- /.github/workflows/check-coding-style.yml: -------------------------------------------------------------------------------- 1 | name: Check Coding Style 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'cake5' 7 | - 'cake4' 8 | - 'cake3' 9 | pull_request: 10 | branches: 11 | - '*' 12 | 13 | jobs: 14 | check-coding-style: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | php-versions: ['8.1'] 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v2 25 | 26 | - name: Validate composer.json and composer.lock 27 | run: composer validate 28 | 29 | - name: Get composer cache directory 30 | id: composer-cache 31 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 32 | 33 | - name: Get date part for cache key 34 | id: key-date 35 | run: echo "::set-output name=date::$(date +'%Y-%m')" 36 | 37 | - name: Cache composer dependencies 38 | uses: actions/cache@v2 39 | with: 40 | path: ${{ steps.composer-cache.outputs.dir }} 41 | key: ${{ runner.os }}-${{ github.ref }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }} 42 | restore-keys: | 43 | ${{ runner.os }}-${{ github.ref }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }} 44 | 45 | - name: Setup PHP 46 | uses: shivammathur/setup-php@v2 47 | with: 48 | php-version: ${{ matrix.php-versions }} 49 | extensions: mbstring, intl 50 | 51 | - name: Install dependencies 52 | if: steps.composer-cache.outputs.cache-hit != 'true' 53 | run: composer install --no-interaction 54 | 55 | - name: Run cs check command 56 | run: composer cs-check 57 | -------------------------------------------------------------------------------- /tests/test_app/src/Controller/UsersController.php: -------------------------------------------------------------------------------- 1 | 1, 15 | 'name' => 'John Doe', 16 | ], 17 | [ 18 | 'id' => 2, 19 | 'name' => 'Alie Doe', 20 | ], 21 | ]; 22 | 23 | $posts = [ 24 | [ 25 | 'title' => 'Title 1', 26 | 'body' => 'Body of title 1', 27 | ], 28 | [ 29 | 'title' => 'Title 2', 30 | 'body' => 'Body of title 2', 31 | ], 32 | ]; 33 | 34 | $postsCount = count($posts); 35 | 36 | $this->set(compact('users', 'posts', 'postsCount')); 37 | } 38 | 39 | public function customComponent() 40 | { 41 | $component = 'Custom/Component'; 42 | $users = [ 43 | [ 44 | 'id' => 1, 45 | 'name' => 'John Doe', 46 | ], 47 | [ 48 | 'id' => 2, 49 | 'name' => 'Alie Doe', 50 | ], 51 | ]; 52 | 53 | $this->set(compact('users', 'component')); 54 | } 55 | 56 | public function store() 57 | { 58 | return $this->redirect('/users/index'); 59 | } 60 | 61 | public function internalServerError() 62 | { 63 | throw new Exception(); 64 | } 65 | 66 | public function setSuccessFlash() 67 | { 68 | $this->Flash->success('User saved successfully.'); 69 | } 70 | 71 | public function setErrorFlash() 72 | { 73 | $this->Flash->error('Something went wrong!'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'cake5' 7 | - 'cake4' 8 | - 'cake3' 9 | pull_request: 10 | branches: 11 | - '*' 12 | 13 | jobs: 14 | run-tests: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | php-versions: ['8.1', '8.2', '8.3'] 21 | composer-type: [lowest, stable, dev] 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v2 26 | 27 | - name: Validate composer.json and composer.lock 28 | run: composer validate 29 | 30 | - name: Get composer cache directory 31 | id: composer-cache 32 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 33 | 34 | - name: Get date part for cache key 35 | id: key-date 36 | run: echo "::set-output name=date::$(date +'%Y-%m')" 37 | 38 | - name: Cache composer dependencies 39 | uses: actions/cache@v2 40 | with: 41 | path: ${{ steps.composer-cache.outputs.dir }} 42 | key: ${{ runner.os }}-${{ github.ref }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.composer-type }} 43 | restore-keys: | 44 | ${{ runner.os }}-${{ github.ref }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.composer-type }} 45 | 46 | - name: Setup PHP 47 | uses: shivammathur/setup-php@v2 48 | with: 49 | php-version: ${{ matrix.php-versions }} 50 | extensions: mbstring, intl 51 | 52 | - name: Install dependencies 53 | if: steps.composer-cache.outputs.cache-hit != 'true' 54 | run: | 55 | if [[ ${{ matrix.composer-type }} == 'lowest' ]]; then 56 | composer update --prefer-dist --no-progress --no-interaction --prefer-stable --prefer-lowest 57 | elif [[ ${{ matrix.composer-type }} == 'stable' ]]; then 58 | composer update --prefer-dist --no-progress --no-interaction --prefer-stable 59 | else 60 | composer update --prefer-dist --no-progress --no-interaction 61 | fi 62 | 63 | - name: Run tests 64 | run: composer test 65 | -------------------------------------------------------------------------------- /docs/ClientSideSetup.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Client-side Setup 4 | 5 | Once you have your [server-side framework configured](ServerSideSetup.md), you then need to [setup your client-side framework](https://inertiajs.com/client-side-setup). This plugin leverages [AssetMix plugin](https://github.com/ishanvyas22/asset-mix/tree/master) so you don't have to install all the front-end dependencies one by one. Instead you can generate scaffolding(using above command) to quickly get started. 6 | 7 | **Note:** As of now AssetMix plugin only supports front-end scaffolding for Vue.js. If you want to use any other client side framework you can do so, refer [this](https://inertiajs.com/client-side-setup) link to know more. 8 | 9 | ### Adding new pages 10 | 11 | As described in official documentation, 12 | 13 | > With Inertia, each page in your application has its own controller and corresponding JavaScript component. This allows you to retrieve just the data necessary for that page, no API required. 14 | 15 | So to create new page you need to create a JavaScript component into `assets/js/` folder matching CakePHP convention. For example, the `index` method of a `ArticlesController` would use `assets/js/Pages/Articles/Index.vue` as its view component. 16 | 17 | Refer official documentation for more info: https://inertiajs.com/pages#creating-pages 18 | 19 | ### Resolve component from another path 20 | 21 | By default, all your components should go into the `Pages` directory inside your js root path(`assets/js` or `resources/js`) if you want to change this path, you have to edit `app.js` file to tell bundler to resolve another path. 22 | 23 | ```js 24 | import Vue from 'vue'; 25 | import { InertiaApp } from '@inertiajs/inertia-vue'; 26 | 27 | Vue.use(InertiaApp); 28 | 29 | const el = document.getElementById('app'); 30 | 31 | new Vue({ 32 | render: h => h(InertiaApp, { 33 | props: { 34 | initialPage: JSON.parse(el.dataset.page), 35 | resolveComponent: name => import(`@/Pages/${name}`).then(module => module.default), 36 | }, 37 | }), 38 | }).$mount(el); 39 | ``` 40 | 41 | Refer official documentation for more info: https://inertiajs.com/client-side-setup#initialize-app 42 | 43 | --- 44 | 45 | [< Server-side Setup](ServerSideSetup.md) 46 | -------------------------------------------------------------------------------- /docs/Installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | 1. Add this plugin to your application with [composer](https://getcomposer.org) 4 | 5 | ```bash 6 | composer require ishanvyas22/cakephp-inertiajs 7 | ``` 8 | 9 | 2. Load plugin into your application 10 | 11 | ```bash 12 | bin/cake plugin load Inertia 13 | ``` 14 | 15 | 3. Generate Client-side scaffolding (Inertia.js + Vue.js) 16 | 17 | ```bash 18 | # Generate vue scaffolding. You can also use (react) here. 19 | bin/cake asset_mix generate inertia-vue 20 | 21 | # Install javascript tooling and dependencies. 22 | npm install 23 | ``` 24 | 25 | **Note:** In order to use Inertia.js, you will need one of [Client-side adapters](ClientSideSetup.md). 26 | 27 | 4. Just add below line into your layout(`Template/Layout/default.ctp`) file 28 | 29 | ```php 30 | echo $this->AssetMix->script('app'); 31 | ``` 32 | 33 | Your layout file should look something like this: 34 | 35 | ```php 36 | 37 | 38 | 39 | Html->charset() ?> 40 | 41 | Html->meta('icon') ?> 42 | 43 | // Add title, load your css, etc. 44 | // ... 45 | 46 | // Load `app.js` javascript file 47 | AssetMix->script('app') ?> 48 | 49 | fetch('meta') ?> 50 | fetch('css') ?> 51 | fetch('script') ?> 52 | 53 | 54 | fetch('content') ?> 55 | 56 | 57 | ``` 58 | 59 | Now you are all set to start using your front-end components as template files. All content will be render between `` tag. 60 | 61 | **Note:** If you've created fresh CakePHP project, you must have to create `Display.vue` component into `assets/js/Pages/Pages` directory. See [this](ClientSideSetup.md) link for more info. 62 | 63 | ## Version map 64 | 65 | Plugin version | Branch | CakePHP version | PHP minimum version | 66 | --- | --- | --- | --- | 67 | 3.x | cake5 | >=5.0.0 | >=8.1 | 68 | 2.x | cake4 | >=4.0.0 | >=7.2 | 69 | 1.x | cake3 | >=3.5.0 | >=5.6 | 70 | 71 | --- 72 | 73 | [< Index](README.md) | [Usage >](ServerSideSetup.md) 74 | -------------------------------------------------------------------------------- /src/Middleware/InertiaMiddleware.php: -------------------------------------------------------------------------------- 1 | setupDetectors($request); 26 | } 27 | if (!$request->hasHeader('X-Inertia')) { 28 | return $handler->handle($request); 29 | } 30 | 31 | $response = $handler->handle($request); 32 | if ( 33 | $response->getStatusCode() === Message::STATUS_FOUND 34 | && in_array($request->getMethod(), [Message::METHOD_PUT, Message::METHOD_PATCH, Message::METHOD_DELETE]) 35 | ) { 36 | $response = $response->withStatus(Message::STATUS_SEE_OTHER); 37 | } 38 | 39 | return $response 40 | ->withHeader('Vary', 'Accept') 41 | ->withHeader('X-Inertia', 'true'); 42 | } 43 | 44 | /** 45 | * Set detectors in the request to use it throughout the application. 46 | * 47 | * @param \Cake\Http\ServerRequest $request The request. 48 | * @return void 49 | */ 50 | private function setupDetectors(ServerRequest $request): void 51 | { 52 | $request->addDetector('inertia', function ($request) { 53 | return $request->hasHeader('X-Inertia'); 54 | }); 55 | 56 | $request->addDetector('inertia-partial-component', function ($request) { 57 | return $request->hasHeader('X-Inertia-Partial-Component'); 58 | }); 59 | 60 | $request->addDetector('inertia-partial-data', function ($request) { 61 | return $request->hasHeader('X-Inertia-Partial-Data'); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CakePHP Adapter for Inertia.js 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/ishanvyas22/cakephp-inertiajs/v)](//packagist.org/packages/ishanvyas22/cakephp-inertiajs) 4 | [![Total Downloads](https://poser.pugx.org/ishanvyas22/cakephp-inertiajs/downloads)](//packagist.org/packages/ishanvyas22/cakephp-inertiajs) 5 | [![License](https://poser.pugx.org/ishanvyas22/cakephp-inertiajs/license)](//packagist.org/packages/ishanvyas22/cakephp-inertiajs) 6 | [![CakePHP](https://img.shields.io/badge/cakephp-%5E5.0.0-red?logo=cakephp)](https://book.cakephp.org/5/en/index.html) 7 | ![Tests](https://github.com/ishanvyas22/cakephp-inertiajs/workflows/Run%20tests/badge.svg?branch=master) 8 | ![PHPStan](https://github.com/ishanvyas22/cakephp-inertiajs/workflows/Run%20PHPStan/badge.svg) 9 | ![Coding Style Check](https://github.com/ishanvyas22/cakephp-inertiajs/workflows/Check%20Coding%20Style/badge.svg) 10 | 11 | CakePHP server side adapter for [Inertia.js](https://inertiajs.com/) to build single-page apps, without building an API. 12 | 13 | ## Installation and Usage 14 | 15 | See [documentation](docs/README.md) 16 | 17 | ## ❤️ Support The Development 18 | **Do you like this project? Support it by donating:** 19 | 20 | 21 | Buy Me A Coffee 22 | 23 | 24 | 25 | 26 | 27 | 28 | **or** [Paypal me](https://paypal.me/IshanVyas?locale.x=en_GB) 29 | 30 | **or** [![Contact me on Codementor](https://www.codementor.io/m-badges/isvyas/get-help.svg)](https://www.codementor.io/@isvyas?refer=badge) 31 | 32 | ### Follow me 33 | - [GitHub](https://github.com/ishanvyas22) 34 | - [LinkedIn](https://www.linkedin.com/in/ishan-vyas-314111112) 35 | - [Twitter](https://twitter.com/ishanvyas22) 36 | 37 | ## Reporting Issues 38 | 39 | If you are facing a problem with this plugin or found any bug, please open an issue on [GitHub](https://github.com/ishanvyas22/cakephp-inertiajs/issues). 40 | 41 | ## License 42 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 43 | -------------------------------------------------------------------------------- /tests/TestCase/View/InertiaJsonViewTest.php: -------------------------------------------------------------------------------- 1 | withParam('controller', 'Pages') 25 | ->withParam('action', 'display'); 26 | // Set `inertia` detector to test `InertiaJsonView` class 27 | $request->addDetector('inertia', function ($request) { 28 | return true; 29 | }); 30 | $request->addDetector('inertia-partial-component', function ($request) { 31 | return false; 32 | }); 33 | $request->addDetector('inertia-partial-data', function ($request) { 34 | return false; 35 | }); 36 | 37 | $this->View = new InertiaJsonView($request, new Response()); 38 | } 39 | 40 | public function testReturnsJsonResponseWithGivenData() 41 | { 42 | $user = ['id' => 1, 'name' => 'John Doe']; 43 | 44 | $this->View->set(compact('user')); 45 | 46 | $resultJson = json_encode(json_decode($this->View->render())); 47 | $resultArray = json_decode($resultJson, true); 48 | 49 | $this->assertEquals('application/json', $this->View->getResponse()->getType()); 50 | $this->assertArrayHasKey('component', $resultArray); 51 | $this->assertArrayHasKey('url', $resultArray); 52 | $this->assertArrayHasKey('props', $resultArray); 53 | $this->assertArrayHasKey('user', $resultArray['props']); 54 | $this->assertJsonStringEqualsJsonString(json_encode([ 55 | 'component' => 'Pages/Display', 56 | 'url' => 'http://localhost/', 57 | 'props' => ['user' => $user], 58 | ]), $resultJson); 59 | } 60 | 61 | public function testJsonViewReturnsCustomComponentName() 62 | { 63 | $component = 'Users/Index'; 64 | $user = ['id' => 1, 'name' => 'John Doe']; 65 | 66 | $this->View->set(compact('component', 'user')); 67 | 68 | $resultJson = json_encode(json_decode($this->View->render())); 69 | 70 | $this->assertJsonStringEqualsJsonString(json_encode([ 71 | 'component' => 'Users/Index', 72 | 'url' => 'http://localhost/', 73 | 'props' => ['user' => $user], 74 | ]), $resultJson); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 'TestApp', 37 | 'encoding' => 'UTF-8', 38 | 'base' => false, 39 | 'baseUrl' => false, 40 | 'dir' => 'src', 41 | 'webroot' => 'webroot', 42 | 'wwwRoot' => WWW_ROOT, 43 | 'fullBaseUrl' => 'http://localhost', 44 | 'imageBaseUrl' => 'img/', 45 | 'jsBaseUrl' => 'js/', 46 | 'cssBaseUrl' => 'css/', 47 | 'paths' => [ 48 | 'plugins' => [TEST_APP_DIR . 'plugins' . DS], 49 | 'templates' => [TEST_APP_DIR . 'templates' . DS], 50 | ], 51 | ]); 52 | Configure::write('Error', [ 53 | 'errorLevel' => E_ALL, 54 | 'skipLog' => [], 55 | 'log' => true, 56 | 'trace' => true, 57 | ]); 58 | 59 | Cache::setConfig([ 60 | '_cake_core_' => [ 61 | 'engine' => 'File', 62 | 'prefix' => 'cake_core_', 63 | 'serialize' => true, 64 | ], 65 | '_cake_model_' => [ 66 | 'engine' => 'File', 67 | 'prefix' => 'cake_model_', 68 | 'serialize' => true, 69 | ], 70 | 'default' => [ 71 | 'engine' => 'File', 72 | 'prefix' => 'default_', 73 | 'serialize' => true, 74 | ], 75 | ]); 76 | 77 | // Ensure default test connection is defined 78 | if (!getenv('db_dsn')) { 79 | putenv('db_dsn=sqlite://127.0.0.1/' . TMP . 'debug_kit_test.sqlite'); 80 | } 81 | $config = [ 82 | 'url' => getenv('db_dsn'), 83 | 'timezone' => 'UTC', 84 | ]; 85 | ConnectionManager::setConfig('test', $config); 86 | Security::setSalt('a long value no one will guess ever'); 87 | 88 | Plugin::getCollection()->add(new InertiaPlugin()); 89 | -------------------------------------------------------------------------------- /tests/TestCase/View/InertiaWebViewTest.php: -------------------------------------------------------------------------------- 1 | withParam('controller', '') 23 | ->withParam('action', ''); 24 | // Set `inertia` detector to test `InertiaJsonView` class 25 | $request->addDetector('inertia', function ($request) { 26 | return true; 27 | }); 28 | $request->addDetector('inertia-partial-component', function ($request) { 29 | return false; 30 | }); 31 | $request->addDetector('inertia-partial-data', function ($request) { 32 | return false; 33 | }); 34 | 35 | $this->View = (new ViewBuilder()) 36 | ->setClassName('Inertia\View\InertiaWebView') 37 | ->setOption('_nonInertiaProps', ['one', 'two', 'three']) 38 | ->build($request, new Response()); 39 | } 40 | 41 | public function testRendersDivWithIdAppAttribute() 42 | { 43 | $this->View->set('user', ['id' => 1, 'name' => 'John Doe']); 44 | 45 | $result = $this->View->render(); 46 | 47 | $this->assertStringContainsString('
assertStringContainsString(htmlentities('"props":{"user":{"id":1,"name":"John Doe"}}'), $result); 49 | } 50 | 51 | public function testRendersComponentName() 52 | { 53 | $this->View->set('component', 'Users/Index'); 54 | $this->View->set('user', ['id' => 1, 'name' => 'John Doe']); 55 | 56 | $result = $this->View->render(); 57 | 58 | $this->assertStringContainsString('"Users\/Index"', $result); 59 | } 60 | 61 | public function testItCanPassthruVarsToCake() 62 | { 63 | $this->View->set('component', 'Users/Index'); 64 | $this->View->set('user', ['id' => 1, 'name' => 'John Doe']); 65 | $this->View->set('one', 1); 66 | $this->View->set('two', 2); 67 | $this->View->set('three', 3); 68 | 69 | $actual = $this->View->getVars(); 70 | 71 | $expected = [ 72 | 'three', 73 | 'two', 74 | 'one', 75 | 'user', 76 | 'component', 77 | ]; 78 | 79 | $this->assertEquals($expected, $actual); 80 | 81 | $this->View->render(); 82 | $actual = $this->View->getVars(); 83 | 84 | $expected = [ 85 | 'page', 86 | 'three', 87 | 'two', 88 | 'one', 89 | ]; 90 | 91 | $this->assertEquals($expected, $actual); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Controller/InertiaResponseTrait.php: -------------------------------------------------------------------------------- 1 | isErrorStatus() || $this->isFailureStatus()) { 17 | return null; 18 | } 19 | 20 | $this->setViewBuilderClass(); 21 | 22 | $this->setFlashData(); 23 | 24 | $this->setCsrfToken(); 25 | 26 | $this->setNonInertiaProps(); 27 | } 28 | 29 | /** 30 | * Sets array of view variables for Inertia to ignore for use 31 | * by Non-Inertia view elements 32 | * 33 | * @return void 34 | */ 35 | private function setNonInertiaProps(): void 36 | { 37 | if (!property_exists($this, '_nonInertiaProps')) { 38 | return; 39 | } 40 | 41 | $nonInertiaProps = $this->_nonInertiaProps; 42 | 43 | if (is_string($nonInertiaProps)) { 44 | $nonInertiaProps = [$nonInertiaProps]; 45 | } 46 | 47 | $this->viewbuilder() 48 | ->setOption('_nonInertiaProps', $nonInertiaProps); 49 | } 50 | 51 | /** 52 | * Sets view class depending on detector. 53 | * 54 | * @return void 55 | */ 56 | private function setViewBuilderClass(): void 57 | { 58 | $viewClass = 'InertiaWeb'; 59 | if ($this->getRequest()->is('inertia')) { 60 | $viewClass = 'InertiaJson'; 61 | } 62 | 63 | $this->viewBuilder()->setClassName("Inertia.{$viewClass}"); 64 | } 65 | 66 | /** 67 | * Checks if response status code is 404. 68 | * 69 | * @return bool Returns true if response is error, false otherwise. 70 | */ 71 | private function isErrorStatus(): bool 72 | { 73 | $statusCode = $this->getResponse()->getStatusCode(); 74 | $errorCodes = [ 75 | Message::STATUS_NOT_FOUND, 76 | ]; 77 | 78 | if (in_array($statusCode, $errorCodes)) { 79 | return true; 80 | } 81 | 82 | return false; 83 | } 84 | 85 | /** 86 | * Checks if response status code is 500. 87 | * 88 | * @return bool Returns true if response is failure, false otherwise. 89 | */ 90 | private function isFailureStatus(): bool 91 | { 92 | $statusCode = $this->getResponse()->getStatusCode(); 93 | $failureCodes = [ 94 | Message::STATUS_INTERNAL_SERVER, 95 | ]; 96 | 97 | if (in_array($statusCode, $failureCodes)) { 98 | return true; 99 | } 100 | 101 | return false; 102 | } 103 | 104 | /** 105 | * Sets flash data, and deletes after setting it. 106 | * 107 | * @return void 108 | */ 109 | protected function setFlashData(): void 110 | { 111 | /** @var \Cake\Http\Session $session */ 112 | $session = $this->getRequest()->getSession(); 113 | 114 | $this->set('flash', function () use ($session) { 115 | if (!$session->check('Flash.flash.0')) { 116 | return []; 117 | } 118 | 119 | $flash = $session->read('Flash.flash.0'); 120 | 121 | $flash['element'] = strtolower(str_replace('/', '-', $flash['element'])); 122 | 123 | $session->delete('Flash'); 124 | 125 | return $flash; 126 | }); 127 | } 128 | 129 | /** 130 | * Sets `_csrfToken` field to pass into every front-end component. 131 | * 132 | * @return void 133 | */ 134 | private function setCsrfToken(): void 135 | { 136 | $this->set('_csrfToken', $this->getRequest()->getAttribute('csrfToken')); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/View/BaseViewTrait.php: -------------------------------------------------------------------------------- 1 | getRequest()->getRequestTarget(), true); 20 | } 21 | 22 | /** 23 | * Returns component name. 24 | * If passed via contoller using `component` key, will use that. 25 | * Otherwise, will return the combination of controller and action. 26 | * 27 | * That means, it will generate `Users/Index` component for `UsersController.php`'s `index` action. 28 | * 29 | * @return string 30 | */ 31 | private function getComponentName(): string 32 | { 33 | if ($this->get('component') !== null) { 34 | $component = $this->get('component'); 35 | 36 | unset($this->viewVars['component']); 37 | 38 | return $this->convertToString($component); 39 | } 40 | 41 | return sprintf( 42 | '%s/%s', 43 | $this->convertToString($this->getRequest()->getParam('controller')), 44 | ucwords($this->convertToString($this->getRequest()->getParam('action'))), 45 | ); 46 | } 47 | 48 | /** 49 | * Helper method used to ensure correct string type. 50 | * 51 | * @param mixed $value Value 52 | * @return string 53 | * @throws \UnexpectedValueException 54 | */ 55 | protected function convertToString(mixed $value): string 56 | { 57 | if (is_string($value)) { 58 | return $value; 59 | } 60 | if (is_numeric($value)) { 61 | return (string)$value; 62 | } 63 | 64 | throw new UnexpectedValueException(); 65 | } 66 | 67 | /** 68 | * Returns an array of Inertia var names and sets Non Inertia Vars for use by View 69 | * 70 | * @param array $only Partial Data 71 | * @param array $passedViewVars Associative array of all passed ViewVars and their values 72 | * @return array 73 | */ 74 | private function filterViewVars(array $only, array $passedViewVars): array 75 | { 76 | $onlyViewVars = !empty($only) ? $only : array_keys($passedViewVars); 77 | 78 | /** @var array $nonInertiaProps */ 79 | $nonInertiaProps = $this->getConfig('_nonInertiaProps') ?? []; 80 | 81 | /** 82 | * Selects the Non-Inertia Vars to be available 83 | * for use outside of Inertia Components 84 | */ 85 | $this->viewVars = array_intersect_key( 86 | $passedViewVars, 87 | array_flip($nonInertiaProps) 88 | ); 89 | 90 | /** 91 | * Returns an array of the vars names which will be 92 | * packaged into the Inertia `page` view var 93 | */ 94 | 95 | return array_diff($onlyViewVars, $nonInertiaProps); 96 | } 97 | 98 | /** 99 | * Returns `props` array excluding the default variables. 100 | * 101 | * @return array 102 | */ 103 | private function getProps(): array 104 | { 105 | $props = []; 106 | 107 | $only = $this->getPartialData(); 108 | 109 | $passedViewVars = $this->viewVars; 110 | 111 | $onlyViewVars = $this->filterViewVars($only, $passedViewVars); 112 | 113 | foreach ($onlyViewVars as $varName) { 114 | if (!isset($passedViewVars[$varName])) { 115 | continue; 116 | } 117 | 118 | $prop = $passedViewVars[$varName]; 119 | 120 | if ($prop instanceof Closure) { 121 | $props[$varName] = $prop(); 122 | } else { 123 | $props[$varName] = $prop; 124 | } 125 | } 126 | 127 | return $props; 128 | } 129 | 130 | /** 131 | * Returns view variable names from `X-Inertia-Partial-Data` header. 132 | * 133 | * @return array 134 | */ 135 | public function getPartialData(): array 136 | { 137 | if (!$this->getRequest()->is('inertia-partial-data')) { 138 | return []; 139 | } 140 | 141 | return explode( 142 | ',', 143 | $this->getRequest()->getHeader('X-Inertia-Partial-Data')[0] 144 | ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/UsersControllerTest.php: -------------------------------------------------------------------------------- 1 | get('/users/index'); 19 | 20 | $this->assertResponseOk(); 21 | $this->assertContentType('text/html'); 22 | $this->assertTemplate('app'); 23 | $this->assertResponseContains('"Users\/Index"'); 24 | $this->assertResponseContains('"http:\/\/localhost\/users\/index"'); 25 | $this->assertResponseContains('"name":"InertiaTestApp"'); 26 | $this->assertResponseContains('props'); 27 | } 28 | 29 | public function testItReturnsInertiaWebViewResponseWithCustomComponent() 30 | { 31 | $this->get('/users/custom-component'); 32 | 33 | $this->assertResponseOk(); 34 | $this->assertContentType('text/html'); 35 | $this->assertTemplate('app'); 36 | $this->assertResponseContains(htmlentities(json_encode('Custom/Component'))); 37 | } 38 | 39 | public function testItReturnsInertiaJsonViewResponseWhenRequestIsXInertia() 40 | { 41 | $this->configRequest([ 42 | 'headers' => ['X-Inertia' => 'true'], 43 | ]); 44 | 45 | $this->get('/users/index'); 46 | 47 | $this->assertResponseOk(); 48 | $this->assertContentType('application/json'); 49 | } 50 | 51 | public function testPartiaReloads() 52 | { 53 | $this->configRequest([ 54 | 'headers' => [ 55 | 'X-Inertia' => 'true', 56 | 'X-Inertia-Partial-Data' => 'posts,postsCount', 57 | 'X-Inertia-Partial-Component' => 'Users/Index', 58 | ], 59 | ]); 60 | 61 | $this->get('/users/index'); 62 | 63 | $this->assertContentType('application/json'); 64 | $this->assertResponseOk(); 65 | 66 | $expected = json_encode([ 67 | 'component' => 'Users/Index', 68 | 'url' => 'http://localhost/users/index', 69 | 'props' => [ 70 | 'posts' => [ 71 | [ 72 | 'title' => 'Title 1', 73 | 'body' => 'Body of title 1', 74 | ], 75 | [ 76 | 'title' => 'Title 2', 77 | 'body' => 'Body of title 2', 78 | ], 79 | ], 80 | 'postsCount' => 2, 81 | ], 82 | ], JSON_PRETTY_PRINT); 83 | $this->assertEquals($expected, $this->_getBodyAsString()); 84 | } 85 | 86 | public function testItRedirectsWithSeeOtherResponseCode() 87 | { 88 | $this->enableCsrfToken(); 89 | $this->configRequest([ 90 | 'headers' => ['X-Inertia' => 'true'], 91 | ]); 92 | 93 | $this->put('/users/store', ['test' => 'data']); 94 | 95 | $this->assertResponseCode(Message::STATUS_SEE_OTHER); 96 | } 97 | 98 | public function testItReturnsHtmlResultFor404() 99 | { 100 | $this->configRequest([ 101 | 'headers' => [ 102 | 'X-Inertia' => 'true', 103 | 'X-Requested-With' => 'XMLHttpRequest', 104 | ], 105 | ]); 106 | 107 | $this->get('/404'); 108 | 109 | $this->assertResponseCode(404); 110 | $this->assertContentType('text/html'); 111 | } 112 | 113 | public function testItReturnsHtmlResultFor500() 114 | { 115 | $this->configRequest([ 116 | 'headers' => [ 117 | 'X-Inertia' => 'true', 118 | 'X-Requested-With' => 'XMLHttpRequest', 119 | ], 120 | ]); 121 | 122 | $this->get('/users/internal-server-error'); 123 | 124 | $this->assertResponseCode(500); 125 | $this->assertContentType('text/html'); 126 | } 127 | 128 | public function testItSetsSuccessFlashDataIntoProps() 129 | { 130 | $this->configRequest([ 131 | 'headers' => ['X-Inertia' => 'true'], 132 | ]); 133 | 134 | $this->get('/users/set-success-flash'); 135 | $responseArray = json_decode($this->_getBodyAsString(), true); 136 | 137 | $this->assertResponseOk(); 138 | $this->assertArrayHasKey('flash', $responseArray['props']); 139 | $this->assertEquals([ 140 | 'message' => 'User saved successfully.', 141 | 'key' => 'flash', 142 | 'element' => 'flash-success', 143 | 'params' => [], 144 | ], $responseArray['props']['flash']); 145 | } 146 | 147 | public function testItSetsErrorFlashDataIntoProps() 148 | { 149 | $this->configRequest([ 150 | 'headers' => ['X-Inertia' => 'true'], 151 | ]); 152 | 153 | $this->get('/users/set-error-flash'); 154 | $responseArray = json_decode($this->_getBodyAsString(), true); 155 | 156 | $this->assertResponseOk(); 157 | $this->assertArrayHasKey('flash', $responseArray['props']); 158 | $this->assertEquals([ 159 | 'message' => 'Something went wrong!', 160 | 'key' => 'flash', 161 | 'element' => 'flash-error', 162 | 'params' => [], 163 | ], $responseArray['props']['flash']); 164 | } 165 | 166 | public function testPropsContainsCsrfToken() 167 | { 168 | $this->configRequest([ 169 | 'headers' => ['X-Inertia' => 'true'], 170 | ]); 171 | 172 | $this->get('/users/index'); 173 | $responseArray = json_decode($this->_getBodyAsString(), true); 174 | 175 | $this->assertResponseOk(); 176 | $this->assertArrayHasKey('_csrfToken', $responseArray['props']); 177 | $this->assertNotEmpty($responseArray['props']['_csrfToken']); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /docs/ServerSideSetup.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Server-side Setup 4 | 5 | To start using Inertia.js into your app, you just have to add `Inertia\Controller\InertiaResponseTrait` into your `AppController.php` controller. And it will automatically render the response according to the request. 6 | 7 | ```php 8 | ` to boot your JavaScript application in. For more info visit [this link](https://inertiajs.com/server-side-setup#root-template). 28 | 29 | By default, it will use the plugin's `Template/Inertia/app.php` file as root template. 30 | 31 | If you want to override this, read [Overriding Plugin Templates from Inside Your Application](https://book.cakephp.org/4/en/plugins.html#overriding-plugin-templates-from-inside-your-application) section from cookbook. After creating the template file, you can use [`InertiaHelper`](https://github.com/ishanvyas22/cakephp-inertiajs/blob/master/src/View/Helper/InertiaHelper.php#L20) to quickly generate a `
` element. 32 | 33 | ```php 34 | Inertia->make($page, 'app', 'container clearfix'); 37 | ``` 38 | 39 | **Output:** 40 | 41 | ```html 42 |
43 | ``` 44 | 45 | ### Render different components 46 | 47 | This plugin follows same convention as CakePHP, so for `UsersController.php`'s `index` action it will render `Index.vue` component inside `Users` directory by default. 48 | 49 | In case, you want to render any other component just set `$component` view variable and it will render that component accordingly. For example, if you want to render `Listing.vue` file inside of `Users` directory: 50 | 51 | ```php 52 | set('users', $this->Users->find()->toArray()); 63 | $this->set('component', 'Users/Listing'); 64 | } 65 | } 66 | ``` 67 | 68 | ### Customize `beforeRender` hook 69 | 70 | `InertiaResponseTrait` uses `beforeRender` hook internally to handle the view specific logic. But there might be some scenarios in which you want to use this hook to manipulate or customize some behavior. Since if you will directly call `beforeRender` method in your controller, it will override whole behavior which is not what you want. 71 | 72 | So instead you should do like this: 73 | 74 | ```php 75 | inertiaBeforeRender($event); 93 | 94 | // Do your customization here. 95 | } 96 | } 97 | ``` 98 | 99 | **Note:** You must have to call `beforeRender` method of `InertiaResponseTrait`, otherwise the inertia response won't work as expected. 100 | 101 | ### Flash messages 102 | 103 | When working with CakePHP we often use Flash messages to set one-time notification message to acknowledge user that particular action has succeeded or not. This plugin makes it easier to work with Flash messages, because you don't have to do anything extra make flash messages work. 104 | 105 | To pass flash data into the front-end component, you simply have to set flash message as you normally would: 106 | 107 | ```php 108 | $this->Flash->success('It worked!'); 109 | ``` 110 | That's it! The flash data will automatically pass to component as a `Array` for you to use. 111 | 112 | ### Integrating Inertia with CakePHP layout and template elements 113 | 114 | To include a mix of legacy CakePHP elements (such as a menu system) with Inertia components tell Inertia which view variables NOT to include in the root Inertia `div` using the `$_nonInteriaProps` class property 115 | 116 | In `AppController` (to globally set the Non-Interia vars) or a child that inherits from it (to make the change per Controller) set the `$_nonInertiaProps` class property to an array of View Vars: 117 | 118 | 119 | ```php 120 | _nonInertiaProps = Configure::read('NON_INERTIA_PROPS') 143 | // } 144 | 145 | // See `Customize beforeRender hook` regarding further customization 146 | 147 | public function index() { 148 | // menu array 149 | $menu = $this->Examples->Menus->find('threaded'); 150 | 151 | // user info to display logged in user in menu 152 | $user = $this->Authentication->getIndentity(); 153 | 154 | $examples = $this->Examples->find('all); 155 | 156 | // `menu` and `user` will be available to CakePHP view elements 157 | // `examples` will be available to Inertia Components 158 | $this->set(compact('menu', 'user', 'examples')); 159 | } 160 | } 161 | ``` 162 | 163 | --- 164 | 165 | [< Installation](Installation.md) | [Client-side Setup >](ClientSideSetup.md) 166 | --------------------------------------------------------------------------------