⚠️ Security Warning:For security, you must set your web server's document root to the
public/directory only. Never expose the project root or any directory abovepublic/to the web. If misconfigured, sensitive files (like.env,storage/,config/, etc.) could be publicly accessible and compromise your application.See Server Configuration for deployment details.
# Place this in your project root (not public/)
# Redirect all requests to public/
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/public/
RewriteRule ^(.*)$ /public/$1 [L]
# Deny access to sensitive files everywhere
<FilesMatch "^(\.env|\.git|composer\.(json|lock)|config\.php)$">
Order allow,deny
Deny from all
</FilesMatch>server {
listen 80;
server_name yourdomain.com;
# Set the root to the public directory
root /path/to/your/project/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Deny access to sensitive files
location ~ /\.(env|git|htaccess) {
deny all;
}
location ~* /(composer\.json|composer\.lock|config\.php) {
deny all;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}- Dynamic Route Parameters: Define routes with parameters (e.g.,
/user/{id},/blog/{slug}) for flexible CRUD and API endpoints. - Robust CSRF Protection: Middleware-based CSRF protection for all state-changing requests (forms, AJAX, HTMX). Use
{{ csrf_field()|raw }}in forms and{{ csrf_token() }}for AJAX/HTMX headers. All tokens use the_csrf_tokensession key. - SPA-like Interactivity with HTMX: Use HTMX attributes for seamless, partial updates (e.g., form submissions, file uploads) and request indicators (spinners). Example:
<form hx-post="/validation-test" hx-target="#form-container" hx-swap="innerHTML" hx-indicator="#spinner" hx-trigger="submit delay:500ms" > ... </form>
- Private File Handling: Upload files to private storage (not web-accessible). Download private files only via internal controller methods (no direct links). Example:
// Upload Storage::put($file, '', 'private'); // Download (controller) $path = Storage::path($filename, 'private');
- Storage Improvements: Storage paths are now always resolved relative to the project root. Public/private separation, symlink support for serving public files, and compatibility with the PHP built-in server for downloads.
- UI/UX: Two-column grid for validation and file upload forms, spinner/indicator support, and SPA feel for user interactions.
- HTMX for modern, interactive UIs (already loaded in your base template)
- PicoCSS for beautiful, minimal styling (already loaded in your base template)
- Twig for templating
- CLI for scaffolding and project management
- Error pages and basic debug tools
SproutPHP supports flexible route parameters (like most modern frameworks):
| Pattern | Matches | Example URI | Notes |
|---|---|---|---|
/user/{id} |
Yes | /user/123 |
{id} = 123 |
/user/{id?} |
Yes | /user/123, /user |
{id} = 123 or null |
/file/{path:.+} |
Yes | /file/foo/bar |
{path} = foo/bar |
/file/{path?:.+} |
Yes | /file, /file/foo/bar |
{path} = foo/bar or null |
/blog/{slug} |
Yes | /blog/hello-world |
{slug} = hello-world |
/blog/{slug} |
No | /blog/hello/world |
{slug} does not match / |
{param}— required parameter (matches anything except/){param?}— optional parameter (trailing slash and parameter are optional){param:regex}— required with custom regex{param?:regex}— optional with custom regex
Optional parameters are passed as null if missing. For catch-all (wildcard) parameters, use a custom regex like {path:.+}.
// routes/web.php
Route::get('/home', 'HomeController@index');// app/Controllers/HomeController.php
class HomeController {
public function index() {
return view('home', ['title' => 'Hello, World!']);
}
}{# app/Views/home.twig #}
<h1>{{ title }}</h1>- Faster response times
- Lower memory usage
- Easier to understand and debug
- No vendor lock-in
- You are in control
- Not a Laravel, Symfony, CodeIgniter, or Slim clone
- Not a full-stack SPA framework
- Not for those who want everything done for them
- Minimal MVC structure
- Simple routing
- Twig templating (optional to swap for PHP)
- Essential CLI tools (scaffolding)
- Custom error handling
- Centralized configuration system with
config()helper - Enhanced security with configurable XSS and CSP protection
- HTMX and PicoCSS pre-installed
| Feature/Improvement | Status | Notes/Suggestions |
|---|---|---|
| [ ] Event System | In Consideration | Event/listener system |
| [ ] Localization (i18n) | In Consideration | Translation files and helpers |
| [ ] Caching (Redis, Memcached, etc.) | In Consideration | Cache abstraction, Redis/Memcached support |
| [ ] Testing Utilities | In Consideration | PHPUnit integration and helpers |
| [x] File Uploads & Storage | In Consideration | File upload and storage abstraction |
| [ ] Command Bus/CQRS | In Consideration | Command handler system |
| [ ] Form Builder | In Consideration | Dynamic form generation and validation |
| [ ] API Support (JWT, rate limiting, etc.) | In Consideration | API middleware, JWT, transformers |
| [ ] ORM/Query Builder | In Consideration | Query builder/ORM for easier DB access |
| [ ] Model Relationships | In Consideration | hasOne, hasMany, belongsTo, etc. |
| [ ] Package Installation System | In Consideration | Install and manage reusable packages/plugins |
SproutPHP is designed to be lightweight. The following features are (or will be) available as optional packages, so you can install only what you need:
- Authentication & Authorization
- Admin Panel/CRUD
- Advanced Error Handling
- API Support (JWT, rate limiting, etc.)
- File Uploads & Storage
- Queue/Job System
- Event System
- Localization (i18n)
- Caching (Redis, Memcached, etc.)
- Testing Utilities
- Asset Pipeline
- Social Login
- Notifications
- Payment Integration
- SEO Tools
- Form Builder
- User Profile/Avatar
- Two-Factor Authentication
- Command Bus/CQRS
- GraphQL Support
- No asset pipeline or
node_modules - No heavy ORM (use Models for direct DB access)
- No built-in authentication (add your own as needed)
- No complex middleware (keep it simple)
SproutPHP now includes a powerful configuration system:
// Access configuration values
$appName = config('app.name');
$dbHost = config('database.connections.mysql.host');
$xssEnabled = config('security.xss.enabled');config/app.php- Application settings and global middlewareconfig/database.php- Database connectionsconfig/security.php- Security settings (CSRF, XSS, CSP)config/view.php- View engine settingsconfig/cache.php- Cache configurationconfig/mail.php- Mail configuration
See CONFIGURATION.md for complete documentation.
SproutPHP now includes a minimal, extensible validation system for validating user input in controllers and forms.
- Validate associative arrays (e.g., $_POST, custom data)
- Supports common rules: required, email, min, max, numeric, string, in, etc.
- Collects and returns error messages
- Easily display errors in Twig views
- Extensible: add your own rules as needed
use Core\Support\Validator;
public function handleForm()
{
$data = $_POST;
$validator = new Validator($data, [
'email' => 'required|email',
'name' => 'required|min:3',
'age' => 'required|numeric|min:18',
'role' => 'in:admin,user,guest',
]);
if ($validator->fails()) {
// Pass errors and old input to the view
return view('your-form-view', [
'errors' => $validator->errors(),
'old' => $data,
]);
}
// Proceed with valid data
}required— Field must not be emptyemail— Must be a valid email addressmin:N— Minimum length Nmax:N— Maximum length Nnumeric— Must be a numberinteger— Must be an integerstring— Must be a stringboolean— Must be a boolean valuearray— Must be an arrayin:val1,val2,...— Value must be one of the listed optionsnot_in:val1,val2,...— Value must NOT be one of the listed optionssame:field— Must match another fielddifferent:field— Must be different from another fieldconfirmed— Must have a matching {field}_confirmation valueregex:pattern— Must match a regex patternurl— Must be a valid URLip— Must be a valid IP addressdate— Must be a valid datebefore:date— Must be a date before the given dateafter:date— Must be a date after the given datenullable— Field is allowed to be null (affects other rules)present— Field must be present in the input (even if empty)digits:N— Must be exactly N digitsdigits_between:min,max— Must be between min and max digitssize:N— Must be exactly N characters (for strings) or N value (for numbers/arrays)starts_with:val1,val2,...— Must start with one of the given valuesends_with:val1,val2,...— Must end with one of the given valuesuuid— Must be a valid UUID
You can add more rules by extending the Validator class.
<form method="POST">
<input type="text" name="name" value="{{ old.name|e }}">
{% if errors.name %}
<div class="error">{{ errors.name }}</div>
{% endif %}
<input type="email" name="email" value="{{ old.email|e }}">
{% if errors.email %}
<div class="error">{{ errors.email }}</div>
{% endif %}
<button type="submit">Submit</button>
</form>- Use the
validate()helper for a shortcut:validate($data, $rules) - See the Validator class in
core/Support/Validator.phpfor more details and to add custom rules.
SproutPHP supports instant dark and light mode switching using PicoCSS's built-in color schemes. The framework provides an optional sun/moon icon button in the navbar to toggle the theme.
- PicoCSS automatically applies dark or light styles based on the
data-themeattribute on<html>. - The toggle button updates
data-themetodarkorlightand saves the preference in localStorage. - No extra CSS is needed for the color scheme itself—PicoCSS handles all color changes.
Add this to your navbar:
<button
id="theme-toggle-btn"
aria-label="Toggle dark/light mode"
style="background:none;border:none;cursor:pointer;font-size:1.5rem;"
>
<span id="theme-icon">☀️</span>
</button>Add this script (in your layout or navbar):
<script>
(function () {
const themeBtn = document.getElementById("theme-toggle-btn");
const themeIcon = document.getElementById("theme-icon");
const html = document.documentElement;
if (!themeBtn || !themeIcon) return;
function setInitialTheme() {
let theme = localStorage.getItem("theme");
if (!theme) {
theme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
html.setAttribute("data-theme", theme);
themeIcon.textContent = theme === "dark" ? "🌙" : "☀️";
}
setInitialTheme();
themeBtn.addEventListener("click", function () {
const currentTheme = html.getAttribute("data-theme");
const newTheme = currentTheme === "dark" ? "light" : "dark";
html.setAttribute("data-theme", newTheme);
localStorage.setItem("theme", newTheme);
themeIcon.textContent = newTheme === "dark" ? "🌙" : "☀️";
});
})();
</script>Result:
- The icon (☀️ or 🌙) is shown in the navbar.
- Clicking the icon toggles the theme and updates the icon instantly.
- The theme preference is saved in
localStorage. - PicoCSS automatically applies the correct color scheme.
You do not need to install or include HTMX or PicoCSS yourself—they are already downloaded and loaded in your base template:
SproutPHP is compatible with PHPUnit and other popular PHP testing tools.
-
Install PHPUnit (dev only):
composer require --dev phpunit/phpunit
-
Create a
tests/directory in your project root. -
Add a sample test:
// tests/ExampleTest.php use PHPUnit\Framework\TestCase; class ExampleTest extends TestCase { public function testBasicAssertion() { $this->assertTrue(true); } }
-
Run your tests:
./vendor/bin/phpunit
You can test any part of your app: helpers, models, controllers, middleware, etc. Use mocks and stubs as needed.
Note: SproutPHP does not include test files by default. You are free to organize and write tests as you see fit for your project.