Why Viper?
Here’s everything I love about Viper and why I think you should give it a shot.
No new technologies
Section titled “No new technologies”Viper itself has a tiny api surface area, acting only as the orchestrator.
Your application is a standard laravel app that serves both the backend and frontend.
Instead of reinventing the wheel Viper automatically generates code that glues existing technologies you already know and love to provide a powerful developer experience.
- Backend -
laravel
- Frontend -
vue
- Client side routing -
vue-router
- Queries -
@tanstack/vue-query
- Mutations -
@tanstack/vue-query
- TypeScript generators -
spatie/typescript-transformer
File Based Routing
Section titled “File Based Routing”File based routes are registered to be served by both Laravel and Vue Router
resources/js/pages/users/[user]/edit.vue
GET users/{user}/edit
(laravel)GET users/:user/edit
(vue router)
Supports route grouping via directories without affecting the url
(app)/home.vue
=>GET /home
Provide layouts to all subroutes, nest infinitely
- _layout.vue
- home.vue
Directoryusers
- _layout.vue
- index.vue
Directory[user]
- _layout.vue
- edit.vue
- index.vue
_layout.vue
files always render a standard <router-view></router-view>
.
Choose to either prefetch your routes so the data is ready when the page mounts, or fetch after navigating for whichever experience you prefer.
tRPC but for PHP
Section titled “tRPC but for PHP”Provide fully typed props and actions to your vue components by writing php inside them
<template> <!-- fully typed as user.name: string --> <p>{{ user.name }}</p></template>
<script setup lang="ts">import { usePage } from '@ozmos/viper-vue';
// type from the php file automatically generated and available in the fileconst page = usePage<ViperGen.HomePage>();
// @tanstack/vue-query to access the prop (strongly typed!)const { data: user } = page.useQuery('user');</script>
<php>use \App\Dto\UserDto;
return new class { #[\Ozmos\Viper\Attrs\Prop] public function user(): UserDto { return UserDto::from(request()->user()); }};</php>
If you just violently recoiled at the mere thought of writing php inside your vue files you can place an adjacent php file next to the vue file instead and it will work exactly the same
<template> <!-- fully typed as user.name: string --> <p>{{ user.name }}</p></template>
<script setup lang="ts">import { usePage } from '@ozmos/viper-vue';
const page = usePage<ViperGen.HomePage>();
const { data: user } = page.useQuery('user');</script>
<?php
use \App\Dto\UserDto;
return new class { #[\Ozmos\Viper\Attrs\Prop] public function user(): UserDto { return UserDto::from(request()->user()); }};
You can also have Viper turn your SFC code into adjacent code and vice versa if you decide to switch later on
php artisan viper:invert
Use middleware, route names, and the service container
<template> <p>{{ user.name }}</p></template>
<script setup lang="ts">import { usePage } from '@ozmos/viper-vue';
const page = usePage<ViperGen.UsersUserParam>();
const { data: user } = page.useQuery('user');</script>
<php>use \App\Dto\UserDto;use \App\Models\User;use \Ozmos\Viper\Attrs;
return new#[Attrs\Name("users.show")]#[Attrs\Middleware(["auth", "can:view,user"])]class { #[Attrs\Prop] public function user(User $user): UserDto { return UserDto::from($user); }};</php>
Actions and Form Helpers
Section titled “Actions and Form Helpers”Automatically pass api endpoints to your pages ready to be consumed
<template> <!-- You can also pass fully-typed (from the DTO) overrides: form.mutate({ name: "something else" }) --> <form method="POST" @submit.prevent="form.mutate()"> <input v-model="form.state.value.name" /> <p v-if="form.errors.value.name">{{ form.errors.value.name }}</p> <button type="submit">Submit</button> </form>
<button @click="perform.mutate()">Do something else</button></template>
<script setup lang="ts">import { usePage } from '@ozmos/viper-vue';
// type from the php file automatically generated and available in the fileconst page = usePage<ViperGen.HomePage>();
// @tanstack/vue-query to access the action with a nice form helperconst form = page.useForm('createUser', { state: { name: "", }, onSuccess(data) { alert(data.name); },});
// or just the useMutation without the form state/form error parsingconst perform = page.useMutation('performSomeAction', { onSuccess() { // ... }});</script>
<php>use \App\Dto\UserDto;use \App\Dto\CreateUserDto;
return new class { #[\Ozmos\Viper\Attrs\Action] public function createUser(CreateUserDto $data): UserDto { return UserDto::from(User::create($data)); }
#[\Ozmos\Viper\Attrs\Action] public function performSomeAction() { // do something else... }};</php>