solophp / inertia-svelte-skeleton
Starter application for SoloPHP with Inertia.js 3 and Svelte 5: server-side routing and controllers, a single-page front end, no API to write.
Package info
github.com/SoloPHP/Inertia-Svelte-Skeleton
Type:project
pkg:composer/solophp/inertia-svelte-skeleton
Requires
- php: ^8.4
- doctrine/dbal: ^4.5
- nyholm/psr7: ^1.8
- nyholm/psr7-server: ^1.1
- solophp/application: ^4.0.1
- solophp/container: ^4.0
- solophp/contracts: ^1.8
- solophp/dotenv: ^1.0
- solophp/http-emitter: ^1.0
- solophp/inertia: ^2.0
- solophp/input: ^1.1
- solophp/logger: ^1.0
- solophp/repository: ^1.1
- solophp/router: ^4.1
- solophp/validator: ^4.2
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.0
- squizlabs/php_codesniffer: ^3.13
Suggests
- ext-pdo_mysql: To run on MySQL or MariaDB
- ext-pdo_pgsql: To run on PostgreSQL
- ext-pdo_sqlite: For the default SQLite database
Provides
None
Conflicts
None
Replaces
None
README
A starting point for a single-page application built from SoloPHP packages, Inertia.js 3 and Svelte 5.
Routing, controllers, validation and the database stay on the server, as in a classic application; the pages are Svelte components, and Inertia moves between them without full page loads. There is no API to design. The skeleton has one module, a task list with forms that are validated by the server's rules while the user types and show their errors after a redirect back, a redirect with a message, filters that reload part of the page, and props that load after the page. It runs on SQLite, so there is no database server to set up first.
Requirements
- PHP 8.4 or later, with
pdo_sqlitefor the default database, orpdo_mysql/pdo_pgsql - Composer
- Node.js 20.19+, 22.12+ or 24+, for Vite
Quick start
composer create-project solophp/inertia-svelte-skeleton my-app
cd my-app
npm install
create-project copies .env.example to .env, fills in APP_KEY and runs the migrations. Then start PHP and Vite, each in its own terminal:
composer serve # http://127.0.0.1:8000 npm run dev # the Vite dev server, with hot module replacement
Open http://127.0.0.1:8000. While npm run dev runs, the pages load their scripts from Vite; without it, from the last npm run build.
What is inside
| Package | Role here | Where |
|---|---|---|
| solophp/inertia | Pages, redirects with errors and messages, deferred and once props, the Inertia protocol | InertiaServiceProvider, controllers, config/middleware.php |
| @inertiajs/svelte | The client: pages, links, forms, partial reloads | resources/js |
| Svelte 5 and Vite | Components with runes, the dev server and the build | resources/js, vite.config.js |
| solophp/application | PSR-15 kernel: providers, middleware pipeline, routing, route cache | bootstrap/app.php |
| solophp/router | Routes, groups, {id:\d+} patterns; UrlGenerator makes every link and redirect from a route name |
config/routes.php, src/Task/ |
| solophp/container | PSR-11 container with autowiring | src/Providers/ |
| solophp/input and solophp/validator | Typed request DTOs and the rules behind #[Validate] |
src/Task/Inputs/ |
| solophp/repository | Typed table repositories over Doctrine DBAL | src/Task/TaskRepository.php |
| solophp/logger, solophp/dotenv, solophp/http-emitter | Logging, .env, sending the response |
providers |
Layout
bin/key fills APP_KEY in .env
bin/migrate applies database/migrations
bootstrap/app.php builds the application; shared by the web, the CLI and the tests
config/
providers.php service providers, in order
middleware.php global middleware, outermost first
routes.php every route, one line per module
database/migrations/ forward-only migrations, DBAL schema API
public/index.php front controller, and the router of `composer serve`
resources/
views/app.php the HTML document of a first visit
js/app.js the client's entry point
js/Layouts/AppLayout.svelte the layout every page gets: navigation and messages
js/Pages/ one Svelte component per page: Home, Error, Tasks/Index, Tasks/Edit
css/app.css
src/
Http/
Vite.php the script and style tags of the build or the dev server
ValidationMessages.php failed rules -> the messages a form shows
InputValidator.php validates a form while the user types, with its route's input class
Middleware/ Error, Csrf, SharedProps, JsonBody
Exceptions/ HttpException, NotFoundException
Providers/ Http, Logging, Database, Input, Inertia
Task/ one module: model, repository, inputs, controller, resource, routes
storage/ SQLite database, logs, route cache
tests/Feature/ requests through the whole application, as the browser and the client send them
How a request runs
- The first visit is a plain browser request. The controller renders a page, such as
Tasks/Indexwith its props, and the answer isresources/views/app.phpwith the page object in a script element. - The client mounts
resources/js/Pages/Tasks/Index.sveltewith those props. From then on, a link or a form is an XHR withX-Inertia: true, and the answer is the next page object as JSON. - A form posts JSON. The controller reads it into an input DTO: invalid input throws, and
ErrorMiddlewareredirects back to the form with the messages in itserrorsprop. Valid input is saved, and the controller redirects with a message, which the layout shows frompage.flash. - While the user types, the form asks the server to validate a field with a Precognition request.
PrecognitionMiddleware, on the task routes, checks it with the input class the route names, such as->attribute('input', TaskStoreInput::class), and never runs the handler: the answer is a204or a422with the field's message. InertiaMiddlewarekeeps the protocol for every response: a client built from older assets gets a409and reloads the page, and a redirect after aPATCHorDELETEbecomes a303, so the browser follows it with a GET.
Errors and messages cross the redirect in a cookie signed with APP_KEY, so there is no session to configure.
Adding a page
- Add a route with a name to a module's
XxxRoutes, and a controller method that returns$this->inertia->render($request, $response, 'Xxx/Show', [...]). - Add
resources/js/Pages/Xxx/Show.svelte, which takes the props withlet { ... } = $props(). - Pass the URLs the page needs as props, made with
UrlGeneratorfrom route names. - Add a feature test.
For a form, add an input class with #[Validate] rules and redirect from the handler; the errors find their way back by themselves. To validate it while the user types, name the input class on the route with ->attribute('input', ...), put the route under PrecognitionMiddleware, and make the form with useForm(method, url, data). See src/Task and resources/js/Pages/Tasks for all of it.
Configuration
| Variable | Default | |
|---|---|---|
APP_NAME |
Solo |
shared with every page as app.name |
APP_ENV |
production |
production caches the routes in storage/cache/routes.php |
APP_DEBUG |
false |
exception details instead of the error page |
APP_KEY |
signs the cookie of errors and messages; composer key makes one |
|
APP_TIMEZONE |
UTC |
|
LOG_FILE, LOG_LEVEL |
storage/logs/app.log, warning |
|
DB_DRIVER, DB_PATH |
pdo_sqlite, storage/database/app.sqlite |
pdo_mysql and pdo_pgsql take DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD |
.env.local overrides .env. Variables set by the server or the shell override both.
Tests and checks
composer test # PHPUnit: every test boots the app on a fresh in-memory database composer stan # PHPStan, level max composer cs # PHP_CodeSniffer, PSR-12 composer check # all three npm run build # the production build, into public/build
The tests do not need a build: they use a manifest of their own in tests/Fixtures.
Production
- Run
npm run buildand deploypublic/buildwith the code; setAPP_ENV=productionandAPP_DEBUG=false. - Point the web server's document root to
public/and send every request for a file that does not exist topublic/index.php. - Delete
storage/cache/routes.phpon each deploy. The next request builds it again. - Make
storage/writable by PHP.
License
MIT. See LICENSE.