Fusion or the Art of Writing PHP Into a Vue SFC Components

This is truly a special article.

Two weeks ago, on February 3rd and 4th, 2025, Laracon EU was held in Amsterdam. Although I wasn't able to attend in person, I followed the entire event via live stream. Conferences always provide a fantastic opportunity to gain new insights and to immerse further into the wonderful Laravel ecosystem. I hope to attend Laracon someday.

One of the talks that caught my attention was "Introducing [REDACTED]: A New Paradigm for Laravel + Javascript" by Aaron Francis. This intrigued me since we already have Inertia and Livewire, which are excellent tools for building modern applications with Laravel. So, what new advancements could be offered?

Thumbnail of the video "Introducing REDACTED: a new paradigm for Laravel + Javascript" by Aaron Francis
Thumbnail of the video "Introducing [REDACTED]: a new paradigm for Laravel + Javascript" by Aaron Francis

Aaron introduced Fusion, described as a novel approach to integrate your modern frontend with your Laravel backend. The concept allows writing PHP code directly in a frontend file, and through some processing, the PHP code is executed server-side, with results sent to the frontend. This is reminiscent of Inertia, but taken much further.

Note

Please watch the video. It's fascinating, easy to follow, and will significantly aid in understanding the rest of this article.

During an interview on the Navbar podcast, Aaron articulated Fusion's aim: to bridge the gap between the frontend and backend, simplifying development and transforming Laravel with Inertia to resemble current meta-frameworks like Next or Nuxt. With his business partner Steve, Aaron discovered that typical Laravel conventions, such as routes in web.php, were not intuitive for JavaScript developers who prefer file-based routing. Aaron's goal is to make Laravel more accessible to JavaScript developers.

With Fusion, a new block is introduced in the Vue SFC to write PHP code, making it an integral part of the Laravel application.

vue
<php>
  // Define a prop in PHP
  $name = prop(Auth::user()->name);
</php>

<template>
  <!-- Use it in Vue! -->
  Hello {{ name }}!
</template>

This article doesn't aim to determine whether Fusion should be used in production or if embedding PHP in a Vue SFC is advisable. That debate is well-covered, especially on X or Reddit.

Instead, I intend to delve into Fusion's workings for two main reasons:

  1. There's much to learn from examining code. Though I haven't read many books, I often peruse project codebases, stealing tricks and smart patterns overlooked elsewhere.
  2. Fusion embodies a technical pinnacle of what's achievable with Laravel, Vue, and Vite. Thus, my curiosity to understand its workings led me to write this analysis. I'm studying the Alien Signals library by Johnson Chu similarly. Comprehending something deeply often involves articulating it to others, hence this article.

Note

Vite's integration with Laravel is exceptional, offering a lot of benefits to Laravel developers. If you haven't explored Vite yet, you definitely should.

Now, let's dive into the code.

Note

Reading the codebase is intriguing; it resembles reading PHP written in JavaScript.

The Fusion Codebase

A week after presenting at Laracon EU, Aaron made Fusion's codebase public. Curious about its mechanics, I speculated that a Vite plugin might extract the PHP from a Vue SFC, similar to how Vite's Vue plugin handles script, template, and style tags.

Supporting HTML, CSS, and JavaScript is straightforward for Vite since they're within its scope. However, PHP presents a challenge since it's not supported inherently by Vite.

Let's pose questions we will answer later. First: how does Fusion manage PHP code?

Fusion Hype Video by Aaron Francis and Steve Tenuto, available on X.

Watching the hype video, it's evident there's more than just extracting PHP code and executing it. Fusion passes PHP code results as props to the Vue component and supports RPC on the Laravel backend from the client. Additionally, the keyword sync enables synchronization between frontend and backend states.

For instance, a function called favorite becomes readily usable in the frontend:

vue
<php>
use function \Fusion\{prop, expose};
use \App\Models\Podcast;

$podcasts = prop(fn () => Podcast::all())->readonly();

expose(favorite: fn (Podcast $podcast) => response()->json($podcast->toggleFavorite()));
</php>

<script lang="ts" setup>
import Podcast from '@Components/Podcast.vue'
</script>

<template>
  <Podcast
    v-for="podcast in podcasts"
    :key="podcast.id"
    v-bind="podcast"
    @favorite="favorite({ podcast }).then(fav => podcast.favorited = fav)"
  />
</template>

Another question arises: what does Fusion do behind the scenes to enable this functionality?

Finally, Fusion is said to push Inertia far beyond its current limits. So, another question: how does Fusion leverage Inertia?

Let's address these questions one by one.

  1. How does Fusion manage PHP code?
  2. What mechanisms does Fusion employ behind the scenes to enable these capabilities?
  1. In what ways does Fusion utilize Inertia?

Ready for an intense exploration? Let's get started.

How Does Fusion Manage PHP Code?

Everything begins with Vite. As usual.

For those new to it, Vite is a remarkably fast frontend build tool powering the next generation of web applications. It brought a new tooling philosophy known as on-demand philosophy. Essentially, Vite acts as a proxy between the browser, which requests files, and the file system holding them. When the browser requests a file, Vite intercepts the request, retrieves the file, and sends it back. However, acting as a proxy allows Vite additional capabilities, such as file transformation, code injection, remote file fetching, and generating virtual files (files not present on the file system). It's akin to a pipeline. Undoubtedly, Vite's advent has been transformative for the frontend ecosystem. By adding plugins to its pipeline, it can be tailored to suit any requirement.

With this in mind, we can grasp how a SFC file undergoes processing by Vite.

vue
<script setup>
// JavaScript code will go here
</script>

<template>
  <div />
  <!-- Your template code will go here -->
</template>

<style scoped>
  /* Your CSS code will go here */
</style>

When a .vue file request reaches the browser, a Vite plugin named vite-plugin-vue transforms the file—processing the script and template into readable JavaScript code. The CSS gets extracted and sent down the CSS pipeline before being dispatched to the browser. The cool aspect is the possibility to create a plugin capable of scrutinizing the transformation pipeline. Even better, there's no need to build it from scratch; that's precisely what the vite-plugin-inspect plugin accomplishes.

For instance, a Vue SFC transformation looks like:

Vue SFC transformation seen using the plugin vite-plugin-inspect
Vue SFC transformation seen using the plugin vite-plugin-inspect

Alright, thanks for the explanation, but what transpires in Fusion? Exactly the same thing but involving the PHP block. When a request contacts the Vite server using a .vue file, a plugin named fusion-vue extracts the PHP block. This process initiates in the transform function below:

ts
function transform(code, filename) {
  if (!filename.endsWith('.vue')) {
    return code
  }

  return new Transformer({
    config: fusionConfig,
    code,
    filename,
    isProd
  }).transform()
}

The transform function forms the crux of the Vite pipeline. It transforms files before they reach the browser. In Fusion's case, it passes the file (the entire content) to a class named Transformer, which manages the file's transformation. You might notice the condition filename.endsWith('.vue'), signifying that the transformation only applies to .vue files. This approach is a prevalent pattern among Vite plugins.

The Transformer class is responsible for:

  1. Extracting the PHP block from the Vue SFC. Interestingly, it employs a parser from @vue/compiler-sfc to achieve this task. Notably, the vite-plugin-vue plugin uses this same parser, illustrating that adding a custom block to a Vue SFC is straightforward. I've observed this practice before; the Vue I18n project supports an i18n custom block specifying translations within a Vue SFC.
js
import { parse } from '@vue/compiler-sfc'

const descriptor = parse(code, { filename }).descriptor
const php = descriptor.customBlocks.find(block => block.type === 'php')
The Vue SFC transformed within the Vite pipeline
The Vue SFC transformed within the Vite pipeline
  1. Removing the PHP block from the Vue SFC.
  2. Determining if the PHP code warrants persistence. Another critical aspect we'll delve into in the following section.
  3. Validating and saving the PHP code. This verification occurs through an external process executing a Laravel command.
ts
await runPhp(['fusion:conform', this.relativeFilename])

Any PHP code errors trigger an error message interpreted by Vite, so accurately that it displays on the browser to indicate an error in the PHP code block.

The Vite error overlay showing the error in the PHP code
The Vite error overlay showing the error in the PHP code

If functionality runs smoothly, the PHP code, stored as an extracted file, gets saved in the storage folder. This aspect is noteworthy, as we will explore further in the following section.

  1. Creating a JavaScript file based on the PHP code that bridges any gaps between the frontend's needs and the PHP block's code. This interesting step demands further exploration.

Again, a Laravel command is invoked during this step:

ts
await runPhp(['fusion:shim', this.relativeFilename])
  1. Finally, returns the transformed code to Vite, allowing the SFC to proceed on its path.

Consider this for a moment. Allow me to illustrate something:

Schema showing all the steps we've seen so far
Schema showing all the steps we've seen so far

It can operate in this manner. In Fusion, Vue SFC files function as a file-based router akin to Nuxt's mechanism. The first request typically reaches the PHP server, which responds with the initial client-side code, which then targets the Vite server for additional resource requests. However, if a Vue SFC route remains unvisited, the corresponding PHP functionality won't be ready to handle it, resulting in a 404 error from the Laravel server. Consequently, the Vite server remains untouched, impeding the transformation process. This poses a hurdle, a huge one.

To fully comprehend how this issue is tackled, we must examine all aspects of the Vite plugin.

Within the Vite configuration, we find two noteworthy features:

  1. The Vite configuration where Vite recognizes the location to find the generated JavaScript file (the shim), which receives an alias assigned in the Vite resolver. This alias obscures the file's location, encouraging best practices like avoiding hardcoded pathnames and rendering code location agnostic to path changes. This Vite-related configuration derives from a PHP config file to eliminate redundant configurations, so the Fusion Vite plugin spawns a process executing a Laravel command to resolve config necessities.
  2. A watcher added to monitor project-wide changes utilizing the Vite watcher, a Chokidar instance.
ts
function configureServer(server) {
  server.watcher.on('all', (event, filename) => {
    // Do whatever you want
  })
}

This watcher supports two functions:

  1. Cleaning up the database and storage folder upon file or folder deletion. The storage folder contains transformed Vue SFC files, serving as the file-based router. Without removal, the user can still access deleted files.
  2. Triggering file transformation. This operates identically to the transform function previously encountered, requiring manual file loading. Only Vue SFC files undergo transformation.
ts
function configureServer(server) {
  server.watcher.on('all', async (event, filename) => {
    if (filename.endsWith('.vue')) {
      const fileContent = fs.readFileSync(filename, 'utf-8')

      await new Transformer({
        config: fusionConfig,
        code: fileContent,
        filename,
        isProd
      }).transform()
    }
  })
}

But why transform files upon creation or modification? This approach appears to contradict the on-demand philosophy and previously defined transform function. Yet, a rationale exists.

Consider an error in the PHP block, such as an overlook in importing a function or class, or attempting to hit a route for the first time. When reloading the page, the PHP server returns an error message, as Laravel does by default. Yet, correcting the Vue SFC's PHP block won't rectify the PHP server error since the Vite transformation won't trigger anew. Consequently, repeatedly hitting cmd+r serves no purpose.

For this reason, the file system must be monitored for Vue SFC changes, triggering a manual transformation. While suboptimal, this method is necessary. Aaron devised a strategy prioritizing transform method transformations over watcher-triggered ones, with throttling applied to the watcher.

This is also to solve the problem we've seen earlier: when the PHP server is hit first, the generated PHP file isn't created yet.

Let's summarize our learning thus far with this schematic:

A schema showing all the steps that makes Fusion work.
A schema showing all the steps that makes Fusion work.

Though we haven't completely covered every component, this diagram aids our understanding of the article's remainder.

Note

Ensure the Vite server operates correctly for everything to function seamlessly.

This exploration deeply engaged us in Fusion's Vite plugin. Let's now move onward to grasp how Fusion organizes its PHP code handling.

Note

Have questions? Ask me anything or leave a comment below. I truly enjoy reading your feedback!

What Does Fusion Do Behind the Scenes to Make This Possible?

Let's delve into the PHP-side operations.

We've seen the PHP code extracted, validated, and saved on the disk. We now need to unravel two pivotal steps to better comprehend Fusion's operation:

  1. Determining if the PHP code should be persisted.
  2. Saving the PHP code to the file system.

For ease of understanding, let's tackle the second step first. The first step will subsequently become clear.

Writing the PHP Code on the File System

Once extracted from the Vue SFC, the PHP code requires persistence on the file system. This process is crucial since the PHP written within the Vue SFC becomes an integral part of the Laravel application, not merely an independent PHP script. This generated file is essential for the Laravel application to respond to incoming requests.

After extraction, the PHP code is placed in a temporary file. This temporary file maintains the exact code content inherent to the Vue SFC. Subsequently, Fusion creates an entry in a dedicated SQLite database. We'll revisit this database later, understanding it facilitates communication between the PHP and JavaScript processes.

Finally, the Vite plugin instigates the fusion:conform command.

The fusion:conform Command

We've delayed understanding this command; now's the moment to clarify. Within the fusion:conform command—a Laravel command written in PHP—neither Vite nor JavaScript applies, strictly PHP.

Upon invocation, this command utilizes a src argument representing the Vue SFC file's path. Acting like a unique identifier allows Fusion to retrieve the pertinent database entry.

This entry contains the destination path for the final PHP file to be used by the Laravel application. Currently, this file doesn't exist; only a temporary file holds the code. Thus, the next step involves reading the temporary file content prior to further processing.

The subsequent process enlists a conformer class, assigned to:

  1. Altering the code to confer compatibility with the Laravel application.
  2. Ensuring code validity by passing it through a PHP parser.

The function examines PHP code's conformity by running its transformation through a sequence of transformer classes.

Each transformer class extends a Transformer super-class, custom-built to expand NodeVisitorAbstract and deploys the TransformerInterface sourced from PHP Parser.

php
<?php

namespace Fusion\Conformity\Transformers;

abstract class Transformer extends NodeVisitorAbstract implements TransformerInterface
{
    public function __construct(
        protected ?string $filename = null
    ) {}
}

Each custom transformer, furthermore, implements the enterNode method to shift the code transformation.

php
<?php

namespace Fusion\Conformity\Transformers;

class PropsTransformer extends Transformer
{
    public function enterNode(Node $node)
    {
        if ($node instanceof MethodCall) {
            // Perform some operation
        }
    }
}

Note

Detailed exploration into transformers or PHP parsers falls outside this article's scope as they offer advanced functionality not immediately necessary to comprehend Fusion's operation.

In total, ten transformers are involved, each one with a specific responsibility. Conceive them as steps within a pipeline—a conventional pattern in programming designed to disassemble intricate tasks into smaller, approachable segments. Consequently, when the user code undergoes transformation and is rendered compatible with the Laravel app, the output from one transformer becomes the next's input.

txt
PHP code from the Vue SFC
    | |
[Procedural Transformer]: Turns procedural code into an anonymous class method entitled runProceduralCode.
[Props Transformer]: Changes prop() function calls into this object's method calls within the class.
[Function Import Transformer]: Strips unnecessary function imports, specifically pre-known ones.
[Prop Value Transformer]: Stipulates that $this->prop() method call chains terminate with ->value().
[Expose Transformer]: Alters expose() function calls into this object method calls, barring variable assignments.
[Mount Transformer]: Validates and modifies mount() function calls, ensuring single anonymous function arguments.
[Prop Discovery Transformer]: Uncovers and documents prop() function property names.
[Action Discovery Transformer]: Discovers and constructs methods for action handlers exposed via expose().
[Anonymous Return Transformer]: Guarantees direct expression returns for anonymous class returns.
[Anonymous Class Transformer]: Converts anonymous classes into named classes, within a generated namespace, gathering necessary requirements.
    | |
Usable PHP code

Consider a concrete example before continuing. Assume the following PHP code exists within the Vue SFC:

vue
<php>
use function \Fusion\{prop};

$name = prop('bienvenue');

$name = strtoupper($name);
</php>

After passing through the transformation, this results:

php
<?php

/**
 * File auto-generated by Fusion.
 * Alterations aren't advisable.
 */
namespace Fusion\Generated\Pages;

class IndexGenerated extends \Fusion\FusionPage
{
    #[\Fusion\Attributes\ServerOnly]
    public array $discoveredProps = ['name'];
    use \Fusion\Concerns\IsProceduralPage;
    public function runProceduralCode()
    {
        $name = $this->prop(name: 'name', default: 'bienvenue')->value();
        $name = strtoupper($name);
        $this->syncProps(get_defined_vars());
    }
}

This class IndexGenerated will be used by the Laravel application upon handling requests at the '/' route. Visualize it as a mini-controller containing user code. Behind the scenes, much of the complexity is abstracted within the FusionPage class.

The discoveredProps array records property names processed during prop() function calls, ensuring synchronization between frontend and backend properties.

The method runProceduralCode, invoked upon page loading, executes user code alongside Fusion-generated code, ensuring prop synchronization between the frontend and backend.

The syncProps method maintains backend-to-frontend property synchronization. It retrieves defined variables within the method, and discoveredProps array content, ensuring the generated props property solely holds user-requested data.

Note

Using get_defined_vars—a built-in PHP function—returns a current scope-representative variable array. In this case, it constitutes ['name' => 'bienvenue'].

Note

#[ServerOnly] classifies properties for server-side application exclusively. Analogous TypeScript decorators function similarly under reflect-metadata.

Finally, Fusion places transformed content in its database-provided file path, updating associated entries with the class's legislative name comprising namespace and class designation. The procedure remains enclosed in a try/catch block, ready to address any transformational errors arising. Upon encountering errors, a transient file gets deleted while an error message directs to Vite, as noted earlier.

At this stage, our file system now appears thus:

    A Fusion Application

  • resources/js/Pages
  • [Podcast].vue
  • Index.vue
  • Search.vue
  • storages/fusion
  • fusion.sqlite

Next, the Vite plugin resumes operation, as detailed in the next section.

The Database

Since discussing this article, we've referred to a database but withheld specifics until now. It's time to understand its purpose!

The database assists in bridging communication between the PHP and JavaScript processes, storing essential information for this purpose. With a glance at the database, we can discern stored data and its utilization.

idsrcphp_classphp_pathshim_pathphp_hash
1/resources/js/Pages/Index.vue\Fusion\Generated\Pages\IndexGenerated/storage/fusion/PHP/Pages/IndexGenerated.php/storage/fusion/JavaScript/Pages/Index.jsphp_1e182535deedd9b60e12c277bef58603
2/resources/js/Pages/Search.vue\Fusion\Generated\Pages\SearchGenerated/storage/fusion/PHP/Pages/SearchGenerated.php/storage/fusion/JavaScript/Pages/Search.jsphp_468b4badedbe0cb6ce4734fcc921ea5f
3/resources/js/Pages/[Podcast].vue\Fusion\Generated\Pages\DynamicPodcastGenerated/storage/fusion/PHP/Pages/DynamicPodcastGenerated.php/storage/fusion/JavaScript/Pages/[Podcast].jsphp_2ca978b1c880558f54c4a59ce7584e6c

During Vue SFC transformation and PHP code extraction, a hash calculation occurs, saved within the php_hash column of the database.

The hash serves to verify whether PHP code alteration occurred. A matching hash means unchanged PHP code, introducing an optimization to omit redundant transformations.

Note

A hash is a one-way function generating a fixed-size character string from input, typically comprising numeric and alphanumeric sequences. A hash uniquely identifies its input, meaning identical hashes indicate identical inputs.

This database repository embodies additional data like the PHP and JavaScript file paths used mainly to handle incoming requests efficiently.

An additional question emerges: How does the SQLite database affect the developer experience?

While utilizing a SQLite database, Fusion does not dictate the Laravel application's database choice: the two remain entirely distinct.

Fusion's service provider assign a database connection called __fusion leveraging SQLite, which does not interfere with the Laravel application database connection. This means that it's possible to have multiple database connections within a single Laravel application. Really nice to know!

php
public function boot()
{
  ConfigFacade::set('database.connections.__fusion', [
    'name' => '__fusion',
    'driver' => 'sqlite',
    'database' => Fusion::storage('fusion.sqlite'),
    'foreign_key_constraints' => true,
    'journal_mode' => 'OFF',
  ]);
}

Note

A Laravel service provider executes during application bootstrapping to register services or bind classes into the service container. It's a pivotal location to administer and configure services.

The Component model now uses this connection:

php
<?php

namespace Fusion\Models;

use Illuminate\Database\Eloquent\Model;

class Component extends Model
{
    protected $connection = '__fusion';
}

With this configuration, the Component class utilizes the __fusion database connection. Consequently, the SQLite database is only used by Fusion; it exerts no visible impact on the Laravel app, allowing the application's free choice regarding its database connection method. It's a straightforward and smartly implemented strategy.

Handling Incoming Requests

Delve into perhaps the most intriguing article segment: handling incoming requests through Fusion.

Upon starting to read, we've largely covered development, including Vite plugin functioning and PHP transformation. Our task now incorporates understanding how Fusion harmonizes these elements for request management.

Fusion employs a file-based router approach. Vue SFCs automatically register themselves as routes within the Laravel app, achieved by calling Fusion as a facade, supported by the Fusion manager, then registered to the FusionServiceProvider as part of the typical routing configuration in routes/web.php.

php
<?php

use Fusion\Fusion;

Fusion::pages();

Simply stated, this appears to be the entire explanation! However, we continue, unraveling more complexities. Within the method pages, all Vue SFC components housed within resources/js/Pages are located and registered meticulously. Employing the familiar Laravel router facilitates implementation.

php
Route::any($uri, [FusionController::class, 'handle'])
  ->defaults('__component', $component)
  ->defaults('__class', Component::where('src', $src)->first()?->php_class);

Notably, the routing process assigns two default values:

  1. __component: Path leading to the Vue SFC file.
  2. __class: Full qualified PHP class name—this class was generated within the fusion:conform command.

Consequently, when a request accesses the / route, the handle operation of the FusionController activates. Within the controller lies a multilayered matrix handling diverse potentialities while managing props, synchronizing actions, and interfacing seamlessly with the frontend.

Upon concluding request management, an appropriate response dispatches, guided by request nature as follows:

  1. HMR or action requests receive a JSON response.
  2. Standard requests delegate rendering tasks to Inertia.

On close inspection, the data-page attribute reveals the Inertia object alongside associated Fusion components:

html
<div id="app" data-page="{&quot;component&quot;:&quot;Index&quot;,&quot;props&quot;:{&quot;errors&quot;:{},&quot;auth&quot;:{&quot;user&quot;:null},&quot;fusion&quot;:{&quot;meta&quot;:[],&quot;state&quot;:{&quot;name&quot;:&quot;AARON&quot;},&quot;actions&quot;:[{&quot;handler&quot;:&quot;applyServerState&quot;,&quot;priority&quot;:40,&quot;_handler&quot;:&quot;Fusion\\Http\\Response\\Actions\\ApplyServerState&quot;}]}},&quot;url&quot;:&quot;\/&quot;,&quot;version&quot;:&quot;f7e418d032bf1c4cc3cc55d947cab622&quot;,&quot;clearHistory&quot;:false,&quot;encryptHistory&quot;:false}"></div>

The data-page attribute is reformatted for clarity:

json
{
  "component": "Index",
  "props": {
    "errors": {},
    "auth": {
      "user": null
    },
    "fusion": {
      "meta": [],
      "state": {
        "name": "AARON"
      },
      "actions": [
        {
          "handler": "applyServerState",
          "priority": 40,
          "_handler": "Fusion\\Http\\Response\\Actions\\ApplyServerState"
        }
      ]
    }
  },
  "url": "/",
  "version": "f7e418d032bf1c4cc3cc55d947cab622",
  "clearHistory": false,
  "encryptHistory": false
}

The fusion object is of interest. It's composed of data initialized in the Vue SFC's PHP block through the prop function; comprising state and actions injected onto the client accordingly. The PendingResponse class, responsible for handling, specifically classifies these operations within Fusion as follows.

Specifically, the applyServerState action employs:

js
import { ref, unref } from 'vue'

export default function applyServerState({ fusion, state }, next) {
  Object.keys(fusion.state).forEach((key) => {
    state[key] = ref(unref(fusion.state[key]))
  })

  return next({ fusion, state })
}

Developers benefit by automatically leveraging action processing by Fusion without manual calls.

I haven’t disclosed everything yet. Besides extracting from the Vue SFC PHP block, Fusion injects JavaScript logic into the Vue SFC, preceding the transformation. What began as an initial component:

vue
<php>
use function \Fusion\{prop};

$name = prop('Aaron');
$name = strtoupper($name);
</php>

<template>
  Hello {{ name }}!
</template>

Transmutes during the Fusion plugin transformation (later handled by Vite) as:

vue
<script setup>
import { useFusion } from '$fusion/Pages/Index.js'
import useHotFusion from '@fusion/vue/hmr'

// __props is a magic variable provided to script setup by Vue.
const __fusionData = useFusion(['name', 'fusion'], __props.fusion)

const { name, fusion } = __fusionData

useHotFusion(__fusionData, { id: 'hot_07f7c82af56ef80f306d905892430461', hot: import.meta?.hot })
</script>

<template>
  Hello {{ name }}!
</template>

Note the synthetically injected script setup block, undemanding manual input. It features a useFusion composable, accepting two arguments, first detailing prop keys requiring extraction (honed through recognizing PHP variables via prop calls) and a prop argument signifying props. Essentially akin to employing const props = defineProps(). Fusion triggers the __props variable, provided by Vue, enabling developers to write their script setup block uninhibited. This allows typical Vue SFC syntax unfettered by Fusion processes.

In essence, composable values name and fusion extract via script setup with seamless template injection.

Where did useFusion originate? The $fusion/Pages prefix divulges the broader picture.

Concerning $fusion/Pages, we've already seen its functionality in action via the Vite plugin-generated alias. This alias abstracts out the JavaScript file's genuine location, birthed from an earlier undertaking formulating the fusion:shim. Its content reads:

js
import ActionFactory from '@fusion/vue/actionFactory'
import Pipeline from '@fusion/vue/pipeline'

export const state = ['name']
export const actions = []
export const fusionActions = ['fusionSync']

let cachedState

export function useFusion(keys = [], props = {}, useCachedState = false) {
  const state = (useCachedState && cachedState) ? cachedState : new Pipeline(props).createState()

  if (!useCachedState) {
    cachedState = state
  }

  const all = {
    ...state,
    ...new ActionFactory([...actions, ...fusionActions], state),
  }

  const shouldExport = {}
  for (const key of keys) {
    if (key in all) {
      shouldExport[key] = all[key]
    }
  }

  return shouldExport
}

The state and fusion variables live within this file, mirroring declarations from the Vue SFC in PHP functions prop and expose. Accompanied by an ActionFactory responsible for applying server-mandated actions to the state, including applyServerState, automatic, and transparent synchronization occurs.

In total, Fusion provides several pre-established actions, such as:

  • applyServerState: Synchronizes backend and client-side state data, updating state to match server expectations in real-time.
  • syncQueryString: Synchronizes query string parameters and state.
  • log: Allows logging for debugging purposes.
  • logStack: Logs action progression for debugging.

The discussion becomes even more absorbing when seeing the expose function. This method exposes a PHP functionality server-side to the client, creating RPC-like client-server behaviors allowing direct client requests to trigger server-side action.

Note

The expose concept shares kinship with approaches like tRPC and React Server Functions.

An illustrative example:

vue
<php>
expose(favorite: function(Podcast $podcast) {
    return response()->json($podcast->toggleFavorite());
});
</php>

<template>
  <button @click="favorite({ podcast }).then(fav => podcast.favorited = fav)">
    Favorite
  </button>
</template>

Exposing a method like favorite facilitates its use within the template. useFusion composable previously outlined handles functionality, yet Fusion ensures thorough processing enabling client-side button interaction to trigger server-backed operations.

  1. The client sends a server request furnished with headers x-fusion-action-request and x-fusion-action-handler.
  2. Backend processes these requests, aided by the provided metadata, responding to the client accordingly like how Inertia handles its protocol with headers.
  3. Client-side state corresponds to server-sent feedback, after processing.

Clicking on the favorite button from the example above generates this server-targeted request:

http
POST /search HTTP/1.1
Accept: application/json, text/plain, */*
X-Fusion-Action-Handler: favorite
X-Fusion-Action-Request: true

{
  "fusion": {
    "args": {
      "podcast": {
        "id": 1,
        "title": "Art Of Product",
        "author": "Derrick Reimer & Ben Orenstein",
        "image": "/art/art-of-product.jpg",
        "favorited": false,
        "created_at": "2025-02-15T18:01:36.000000Z",
        "updated_at": "2025-02-19T20:49:13.000000Z"
      }
    },
    "state": {
      "search": "",
      "podcasts": [
        {
          "id": 1,
          "title": "Art Of Product",
          "author": "Derrick Reimer & Ben Orenstein",
          "image": "/art/art-of-product.jpg",
          "favorited": false,
          "created_at": "2025-02-15T18:01:36.000000Z",
          "updated_at": "2025-02-19T20:49:13.000000Z"
        }
      ]
    }
  }
}

Meanwhile, the server processes such a request by interpreting the headers x-fusion-action-request and x-fusion-action-handler. In its service provider, Fusion registers request macros for easy request type identification and handling.

Inside the request body, args holds the method's arguments (podcast), while state comprises the app's current state.

In response, the server sends the following:

json
true

Which is the response from the favorite method. The client then updates the state accordingly.

Fusion offers a sync method fostering frontend-backend state synchronization. For example, a search field could filter items based on backend computations, achieved via sync. Essentially, sync mimics an action categorized by the fusionSync handler!

How Does Fusion Use Inertia?

In truth, Fusion ingeniously simplifies and relies on Inertia's seamless integration to process Vue SFCs effectively without reinventing its frontend framework. The Inertia::render method exemplifies Inertia-based rendering while createInertiaApp establishes the Vue app environment efficiently, counting as the full extent of Inertia's footprint within the Fusion project.

Building on top of Inertia proves prudent, allowing Aaron to focus innovation on Fusion's purpose without distraction by Vue integration intricacies within Laravel. Inertia's utility renders any redundancy void. Additionally, transcending mid-journey evolves Fusion to encompass other frameworks like React seamlessly.

Final Thoughts

Fusion has a potential, especially for developers transitioning from Next or Nuxt to a full-stack capability, eagerly embracing Laravel with Inertia. By preserving concepts dear to developers of these frameworks, such as file-based routing and RPC-driven communication, while enriching them with Laravel's power, Fusion becomes a compelling choice. You should definitely try it out!

Within Fusion's roadmap, Aaron envisions React compatibility, achievable by standing on Inertia's shoulders. However, I'm eager to see Aaron's take on handling server code execution within JSX, perhaps using a 'use php' directive? 🤔

This project illustrates Vite and Laravel's deep relationship's strength effectively. Accessing Vite's comprehensive utility effortlessly remains a groundbreaking capability. It also reaffirms the SFC's modular capacity in Vue—Fusion surely confirms it. There are no practical limits to what you can achieve using Vite and Vue SFC components!

Finally, Fusion's inter-process SQLite database communication reinforces an elegant, smart pattern!

The article made for an exciting write while I was exploring Fusion's codebase. I hope it unearthed new insights for you, and if keen on delving into the intricacies of another library, feel free to comment or use the Ask Me Anything page.

But I've got an additional treat for you! 👇

One More Thing

While drafting this article, prevalent PHP block code examples lacked appropriate syntax highlighting. I pondered and decided to tackle this task. Since Shiki is used for syntax highlighting code in this website and it uses TextMate grammars, I know that I could add support for the PHP block in VSCode.

But how?

Diving in, I realized embedding languages in single-file contexts isn't new. Vue SFCs are notable, as are Markdown, Angular, and even HTML entailing different languages. Subsequently, I focused on Vue SFC's TextMate grammar.

Note

A TextMate grammar defines coding language syntax highlights through patterns and sets rules. Operating behind VSCode and Shiki, it's the syntax highlighter's engine.

Examining Shikijs' textmate-grammars-themes repository, catering to every language and theme supported by Shiki unearthed the Vue grammar. Although lengthy, illuminating concepts surface when reading through, such as HTML, CSS, and JavaScript embeddings within respective blocks:

json
{
  "begin": "(script)\\b",
  "beginCaptures": {
    "1": {
      "name": "entity.name.tag.$1.html.vue"
    }
  },
  "end": "(</)(\\1)\\s*(?=>)",
  "endCaptures": {
    "1": {
      "name": "punctuation.definition.tag.begin.html.vue"
    },
    "2": {
      "name": "entity.name.tag.$2.html.vue"
    }
  },
  "patterns": [
    {
      "include": "#tag-stuff"
    },
    {
      "begin": "(?<=>)",
      "end": "(?=<\\/script\\b)",
      "name": "source.js",
      "patterns": [
        {
          "include": "source.js"
        }
      ]
    }
  ]
}

This handles JavaScript highlighting within <script> blocks using the JavaScript grammar inclusion through include. The same pattern presumably applies to other languages, including PHP.

This is exactly what I want to achieve, but with <php> and PHP grammar. Of course, there's already a PHP grammar, so I just need to include it the same way. I try, and it works. Youpi! 🥳

But wait, I can't submit a PR to Vue Language Tools (Shiki fetches the grammar from this repo) to add support for PHP grammar. That makes no sense, and there's no reason for them to accept it. So I need to find another solution.

After continuing my search, I found the angular-inline-style grammar. This one is used to highlight styles (CSS, SCSS) inside an Angular component (TypeScript). To do so, the grammar uses the keyword injectTo, which acts as an extension of an existing grammar—in this case, source.ts.ng. This is perfect! What if I created a new grammar that extends the Vue grammar by injecting the PHP grammar? In retrospect, this sounded easier than it actually was.

After a couple of hours of trial and error, I finally managed to create a grammar that works in Shiki. One of the most complicated parts was figuring out what was wrong when things didn’t work. With this kind of setup, there are no error messages, no warnings—nothing. You just have to guess what’s wrong. You can find the working solution in the source code of my code-to-image generator.

Finally, I created a VSCode extension that anyone can use to add support for PHP blocks in Vue SFCs. You can find it on the marketplace along with the working grammar: source.vue.php.

If Fusion becomes popular, I might eventually submit a PR to add the language to Shiki, making it easier for everyone to highlight PHP blocks.

Happy coding! 👨‍💻

PP

Thanks for reading! My name is Estéban, and I love to write about web development.

I've been coding for several years now, and I'm still learning new things every day. I enjoy sharing my knowledge with others, as I would have appreciated having access to such clear and complete resources when I first started learning programming.

If you have any questions or want to chat, feel free to comment below or reach out to me on Bluesky, X, and LinkedIn.

I hope you enjoyed this article and learned something new. Please consider sharing it with your friends or on social media, and feel free to leave a comment or a reaction below—it would mean a lot to me! If you'd like to support my work, you can sponsor me on GitHub!

Reactions

Discussions

Add a Comment

You need to be logged in to access this feature.

Login with GitHub
Support my work
Follow me on