Sprunjes
Work in progress
This page is a work in progress. Help complete it by contributing on GitHub!
- What is a Sprunje?
- Why Use Sprunjes?
- Basic Sprunje
- Using a Sprunje in a Controller
- Frontend Integration
- Custom Filters
- Sorting
- Eager Loading Relationships
- Limiting Accessible Columns
- Response Format
- Advanced: Transforming Results
- Authorization
- Best Practices
- Next Steps
- Resources
Sprunjes are UserFrosting's solution for building powerful, server-side data tables. They handle the complex logic of filtering, sorting, and paginating data on the backend, while providing a clean API for your frontend components.
What is a Sprunje?
A Sprunje is a PHP class that:
- Queries your database using Eloquent models
- Filters data based on user input
- Sorts results by any column
- Paginates large datasets
- Returns JSON formatted for frontend consumption
Think of it as a "smart endpoint" specifically designed for data tables. Instead of writing custom logic for every table in your application, you create a Sprunje that handles all the complexity.
Why Use Sprunjes?
Without Sprunjes
You'd need to manually:
- Parse query parameters for filters, sorts, and pagination
- Build complex Eloquent queries with WHERE clauses
- Handle sorting logic
- Calculate pagination metadata
- Format the response as JSON
With Sprunjes
You define what data to load and how to filter it, and Sprunjes handle the rest automatically.
Basic Sprunje
Here's a simple Sprunje for displaying users:
app/src/Sprunje/UserSprunje.php:
<?php
namespace App\Sprunje;
use UserFrosting\Sprinkle\Account\Database\Models\User;
use UserFrosting\Sprinkle\Core\Sprunje\Sprunje;
class UserSprunje extends Sprunje
{
protected string $name = 'users';
/**
* Set the initial Eloquent query.
*/
protected function baseQuery()
{
return User::query();
}
}
That's it! This Sprunje can now:
- List all users
- Filter by any column
- Sort by any column
- Paginate results
Using a Sprunje in a Controller
app/src/Controller/UserController.php:
<?php
namespace App\Controller;
use App\Sprunje\UserSprunje;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class UserController
{
public function list(Request $request, Response $response, UserSprunje $sprunje): Response
{
// Get query parameters and process with Sprunje
$result = $sprunje->getResults($request->getQueryParams());
// Return JSON response
$payload = json_encode($result);
$response->getBody()->write($payload);
return $response->withHeader('Content-Type', 'application/json');
}
}
Route definition:
$app->get('/api/users', [UserController::class, 'list']);
Frontend Integration
Use the Sprunje endpoint in your Vue component:
<template>
<table class="uk-table uk-table-striped">
<thead>
<tr>
<th @click="sort('user_name')">Username</th>
<th @click="sort('email')">Email</th>
<th @click="sort('created_at')">Created</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.user_name }}</td>
<td>{{ user.email }}</td>
<td>{{ formatDate(user.created_at) }}</td>
</tr>
</tbody>
</table>
<div class="uk-margin">
<button @click="prevPage" :disabled="!hasPrevPage">Previous</button>
<button @click="nextPage" :disabled="!hasNextPage">Next</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
interface User {
id: number
user_name: string
email: string
created_at: string
}
const users = ref<User[]>([])
const currentPage = ref(1)
const totalPages = ref(1)
const sortColumn = ref('user_name')
const sortDirection = ref<'asc' | 'desc'>('asc')
async function loadUsers() {
const response = await axios.get('/api/users', {
params: {
page: currentPage.value,
sort: `${sortDirection.value === 'desc' ? '-' : ''}${sortColumn.value}`
}
})
users.value = response.data.rows
totalPages.value = response.data.count_page
}
function sort(column: string) {
if (sortColumn.value === column) {
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc'
} else {
sortColumn.value = column
sortDirection.value = 'asc'
}
loadUsers()
}
function prevPage() {
if (currentPage.value > 1) {
currentPage.value--
loadUsers()
}
}
function nextPage() {
if (currentPage.value < totalPages.value) {
currentPage.value++
loadUsers()
}
}
const hasPrevPage = computed(() => currentPage.value > 1)
const hasNextPage = computed(() => currentPage.value < totalPages.value)
onMounted(() => {
loadUsers()
})
</script>
Custom Filters
Add custom filtering logic to your Sprunje:
<?php
namespace App\Sprunje;
use UserFrosting\Sprinkle\Account\Database\Models\User;
use UserFrosting\Sprinkle\Core\Sprunje\Sprunje;
use Illuminate\Database\Eloquent\Builder;
class UserSprunje extends Sprunje
{
protected string $name = 'users';
protected function baseQuery()
{
return User::query();
}
/**
* Filter by active status.
*/
protected function filterActive(Builder $query, mixed $value): Builder
{
return $query->where('flag_enabled', $value);
}
/**
* Filter by role.
*/
protected function filterRole(Builder $query, mixed $value): Builder
{
return $query->whereHas('roles', function ($query) use ($value) {
$query->where('slug', $value);
});
}
/**
* Filter by email domain.
*/
protected function filterEmailDomain(Builder $query, mixed $value): Builder
{
return $query->where('email', 'LIKE', "%@{$value}");
}
}
Using custom filters:
// Get only active users
axios.get('/api/users?filters[active]=1')
// Get users with admin role
axios.get('/api/users?filters[role]=admin')
// Get users from specific domain
axios.get('/api/users?filters[email_domain]=example.com')
// Combine filters
axios.get('/api/users?filters[active]=1&filters[role]=admin')
Sorting
Sorting is automatic for all columns, but you can customize it:
/**
* Custom sort for full name (concatenated first_name + last_name).
*/
protected function sortFullName(Builder $query, string $direction): Builder
{
return $query->orderByRaw("CONCAT(first_name, ' ', last_name) {$direction}");
}
Frontend usage:
// Sort ascending
axios.get('/api/users?sort=user_name')
// Sort descending (prefix with -)
axios.get('/api/users?sort=-created_at')
// Custom sort
axios.get('/api/users?sort=full_name')
Eager Loading Relationships
Improve performance by eager loading relationships:
protected function baseQuery()
{
return User::with(['group', 'roles']);
}
Limiting Accessible Columns
For security, limit which columns can be filtered and sorted:
protected array $sortable = [
'user_name',
'email',
'created_at'
];
protected array $filterable = [
'user_name',
'email',
'active'
];
Response Format
Sprunjes return JSON in this format:
{
"count": 50,
"count_filtered": 10,
"count_page": 2,
"rows": [
{
"id": 1,
"user_name": "admin",
"email": "[email protected]",
"created_at": "2024-01-15T10:30:00"
}
]
}
- count: Total records in database
- count_filtered: Records after applying filters
- count_page: Total pages available
- rows: Current page data
Advanced: Transforming Results
Modify data before sending to frontend:
protected function transformRow($row): array
{
return [
'id' => $row->id,
'username' => $row->user_name,
'email' => $row->email,
'fullName' => $row->first_name . ' ' . $row->last_name,
'isActive' => (bool) $row->flag_enabled,
'roleNames' => $row->roles->pluck('name')->join(', ')
];
}
Authorization
Check permissions before returning data:
protected function baseQuery()
{
// Check if user can view users
if (!$this->currentUser->can('view_users')) {
throw new ForbiddenException();
}
return User::query();
}
Best Practices
- Create one Sprunje per data table: Don't try to make one Sprunje do everything
- Use eager loading: Load relationships upfront with
with()to avoid N+1 queries - Limit exposed columns: Use
$sortableand$filterableto control what can be queried - Transform sensitive data: Use
transformRow()to remove or modify sensitive fields - Check permissions: Always verify the user can access the data
- Index database columns: Ensure columns used for sorting and filtering are indexed
Next Steps
- Tables: See how to build table UIs that use Sprunjes
- Advanced Tables: Implement search, filters, and exports