Skip to main content
CakeDC Blog

Integrate Vite for front side in CakePHP

This article is part of the CakeDC Advent Calendar 2024 (December 6th 2024)

Vite can be easily integrated to manage assets such as JavaScript, CSS, images, and fonts. This integration is particularly useful in modern applications where the frontend and backend work together.

Advantages of Using Vite with CakePHP

  • Development Optimization: HMR allows developers to see changes instantly in the browser, improving the development experience. There's no need to refresh the page constantly to view updates.

  • Efficient Bundling: Production assets are minimized and optimized, reducing loading times.

  • Modern Technology Adoption: It enables the integration of modern frontend tools like Vue.js, React, or TypeScript into traditional CakePHP projects.

Use Cases

  • Applications with Dynamic Frontends: Ideal for CakePHP projects where the frontend requires interactive components built with React or Vue.js.

  • Hybrid Applications: Integration of a SPA (Single Page Application) with a robust backend like CakePHP.

  • Enterprise Applications: Management of dashboards with interactive charts and reports, leveraging modern frontend tools while using CakePHP for data handling and business logic.

1.- Create a new View:

src/View/ViteView.php

declare(strict_types=1);

namespace App\View;

use Cake\View\View;

class ViteView extends View
{
    public function initialize(): void
    {
        $this->loadHelper('Vite');
    }
}

2.- Create a new Trait

src/Traits/ViteResponseTrait.php

namespace App\Traits;

use App\View\ViteView;
use Cake\Event\EventInterface;

trait ViteResponseTrait
{
    public function beforeRender(EventInterface $event)
    {
        $this->viewBuilder()->setClassName(ViteView::class);
    }
}

3.- Create a new Helper

src/View/Helper/ViteHelper.php

namespace App\View\Helper;

use Cake\Routing\Router;
use Cake\View\Helper;

class ViteHelper extends Helper
{
    public array $helpers = ['Html'];

    public function loadAssets(): string
    {
       if (!file_exists(WWW_ROOT . 'hot')) {
           $manifest = json_decode(
               file_get_contents(WWW_ROOT . 'js' . DS . 'manifest.json'),
               true
           );
           $path = Router::fullBaseUrl() . DS . 'js' . DS;
           $firstBlock = [];
           $secondBlock = [];
           foreach($manifest as $key => $data){
               $part = explode('.', $key);
               $part = $part[count($part) - 1];
               if ($part == 'css') {
                   $firstBlock[] = $this->Html->tag(
                       'link',
                       '',
                       ['as' => 'style', 'rel' => 'preload', 'href' => $path . $data['file']]
                   );
                   $secondBlock[] = $this->Html->tag(
                       'link',
                       '',
                       ['as' => 'style', 'rel' => 'stylesheet', 'href' => $path . $data['file']]
                   );
               }
               if ($part == 'js') {
                   $firstBlock[] = $this->Html->tag(
                       'link',
                       '',
                       ['as' => 'style', 'rel' => 'preload', 'href' => $path . $data['css'][0]]
                   );
                   $secondBlock[] = $this->Html->tag(
                       'link',
                       '',
                       ['as' => 'style', 'rel' => 'stylesheet', 'href' => $path . $data['css'][0]]
                   );
                   $firstBlock[] = $this->Html->tag(
                       'link',
                       '',
                       ['rel' => 'modulepreload', 'href' => $path . $data['file']]
                   );
                   $secondBlock[] = $this->Html->tag(
                       'script',
                       '',
                       ['type' => 'module', 'src' => $path . $data['file']]
                   );
               }
           }

           return implode('', $firstBlock) . implode('', $secondBlock);
       } else {
           $domain = file_get_contents(WWW_ROOT . 'hot');
           $head = $this->Html->script(
               $domain . '/@vite/client',
               ['rel' => 'preload', 'type' => 'module']
           );
           $head .= $this->Html->css($domain . '/resources/css/app.css');
           $head .= $this->Html->script(
               $domain . '/resources/js/app.js',
               ['rel' => 'preload', 'type' => 'module']
           );

           return $head;
       }
    }
}

4.- Create function vite and add the trait to the controller

src/Controller/PagesController.php

use App\Traits\ViteResponseTrait;

class PagesController extends AppController
{
    use ViteResponseTrait;

    public function vite()
    {
        $this->viewBuilder()->setLayout('vite');
    }
}

5.- Add a new layout

templates/layout/vite.php

<!DOCTYPE html>
<head lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Cake with Vite and Vue</title>
        <?php echo $this->Vite->loadAssets();?>
    </head>
<body class="antialiased">
<div id="app">
    <example-component></example-component>
</div>
</body>
</html>

6 .- Install and configure Vite (using DDEV)

on .ddev/config.yaml add this new configuration and run ddev restart

web_extra_exposed_ports:
    - name: vite
      container_port: 5173
      http_port: 5172
      https_port: 5173

create package.json

{
    "private": true,
    "type": "module",
    "scripts": {
        "dev": "vite",
        "build": "vite build"
    },
    "devDependencies": {
        "autoprefixer": "^10.4.20",
        "laravel-vite-plugin": "^1.0.0",
        "vite": "^5.0.0"
    },
    "dependencies": {
        "@vitejs/plugin-vue": "^5.1.4",
        "vue": "^3.5.8",
        "vuex": "^4.0.2"
    }
}

create vite.config.js

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';

const port = 5173;
const origin = `${process.env.DDEV_PRIMARY_URL}:${port}`;

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            publicDirectory: 'webroot',
            refresh: true,
        }),
        vue(),
    ],
    build: {
        outDir: 'webroot/js'
    },
    server: {
        host: '0.0.0.0',
        port: port,
        strictPort: true,
        origin: origin
    },
});

create .env

APP_NAME=cakePHP
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL="https://advent2024.ddev.site"

to install and configure all run in console: ddev npm install

7. Create an Example App

resources/js/components/ExampleComponent.vue

<template>
  <div class="text-center p-4 bg-blue-100 rounded-lg">
    <h1 class="text-2xl font-bold">Hello from Vue 3!</h1>
    <p class="mt-2">This is a Vue component integrated with CakePHP and Vite.</p>
    <h2 class="mt-4">Counter: {{ count }}</h2>
    <p class="mt-2">
      <button @click="increment">Increment</button>
      <button @click="decrement">Decrement</button>
    </p>
  </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex';

export default {
  name: 'ExampleComponent',
  computed: {
    ...mapGetters(['getCount']),
    count() {
      return this.getCount;
    },
  },
  methods: {
    ...mapActions(['increment', 'decrement']),
  },
};
</script>

<style scoped>
button {
  margin: 5px;
}
</style>

 

resources/js/app.js

import { createApp } from 'vue';
import ExampleComponent from './components/ExampleComponent.vue';
import store from './store';

createApp(ExampleComponent).use(store).mount('#app');

resources/js/store.js

import {createStore} from 'vuex';

const store = createStore({
    state: {
        count: Number(localStorage.getItem('count')) || 0,
    },
    mutations: {
        increment(state) {
            state.count++;
            localStorage.setItem('count', state.count);
        },
        decrement(state) {
            state.count--;
            localStorage.setItem('count', state.count);
        },
    },
    actions: {
        increment({ commit }) {
            commit('increment');
        },
        decrement({ commit }) {
            commit('decrement');
        },
    },
    getters: {
        getCount(state) {
            return state.count;
        },
    },
});

8.- Launch

For development run Vite

ddev npm run dev

For production run Vite build

ddev npm run build

This generates the assets directory inside webroot dir, the helper automatically load the files parsing the manifest.json

You can see in front the app

 

You can see a complete example in https://github.com/ACampanario/advent2024

 

 

 

 

Back to all articles
We Bake with CakePHP