robertwesner / simple-mvc-php
A small library for creating PHP web servers.
Requires
- php: >=8.4
- guzzlehttp/psr7: ^2.7
- twig/twig: ^3.19
Requires (Dev)
- phpunit/phpunit: ^11.3.1
- robertwesner/dependency-injection: ^1.3.0
- squizlabs/php_codesniffer: ^3.10.2
Suggests
This package is auto-updated.
Last update: 2025-06-04 17:44:23 UTC
README
Simple MVC for PHP
A small library for creating PHP web servers.
Initially created for private use in place of Node-JS when creating very simple websites. Now able to run more complex applications. Feel free to use if it fits your needs.
Websites using this:
Features
- Request handling (
GET
,POST
,PUT
,PATCH
,DELETE
)- Query parameters
- JSON parameters
- URI parameters
- Intuitive Syntax
- Simple to use composer template
- Integrated Twig templating engine
- [Optional] Autowiring of controller dependencies
- [Optional] Ability to load external bundles
Installation
New Docker project
This creates a Docker PHP-FPM + Nginx Project and is the preferred way of use.
composer create-project robertwesner/simple-mvc-php-docker-template
New project
This creates a new project with the required folder structure.
composer create-project robertwesner/simple-mvc-php-template
Existing project
If you already have a project, require the package and migrate your files manually.
composer require robertwesner/simple-mvc-php
Usage
Project structure
PROJECT_ROOT
|-- public
| '-- any publicly accessible data like JS, CSS, images, ...
|-- routes
| '-- PHP routing scripts
|-- views
| '-- twig views
|-- src
|-- vendor
'-- route.php
Routing scripts
You can create any amount of routing scripts. They define a mapping between a URL and a controller function or method.
Example:
PROJECT_ROOT
'-- routes
| |-- api.php
| '-- view.php
'-- views
'-- main.twig
api.php
Route::post('/api/login', function (Request $request) { // Reads either Query or JSON-Body Parameter $password = $request->getRequestParameter('password'); if ($password === null) { return Route::response('Bad Request', 400); } // ... return Route::json([ 'success' => $success, ]); }); Route::post('/api/logout', function () { // ... }); // Also able to read URI parameters Route::get('/api/users/(?<userId>\d+)', function (Request $request) { $userId = $request->getUriParameter('userId'); // Returns numeric userId from capture group // ... }); // 404 page, FALLBACK will be called when no other route matches Route::get(Route::FALLBACK, function (Request $request) { return Route::render('404.twig'); });
view.php
Route::get('/', function () { // ... return Route::render('main.twig', [ 'loggedIn' => $loggedIn, ]); });
Using Controller Classes
More complex Logic can be handled with class controllers.
Resolving the controller requires robertwesner/dependency-injection.
See: demo class and demo routing
final class UserService { // ... } readonly class UserController { public function __construct( private UserService $userService, ) {} public function all(): ResponseInterface { // ... } public function get(Request $request): ResponseInterface { // ... } public function create(Request $request): ResponseInterface { // ... } public function delete(Request $request): ResponseInterface { // ... } }
// Note: this requires robertwesner/dependency-injection Route::get('/api/users', [UserController::class, 'all']); Route::get('/api/users/(?<userId>\d+)', [UserController::class, 'get']); Route::post('/api/users', [UserController::class, 'create']); Route::delete('/api/users/(?<userId>\d+)', [UserController::class, 'delete']);
Autowiring
Installing robertwesner/dependency-injection allows for automatic resolution of Route dependencies:
// Autowired service class (AuthenticationService) inside Route // Note: this requires robertwesner/dependency-injection Route::post('/api/admin/some-endpoint', function (Request $request, AuthenticationService $authenticationService) { // ... });
Configuration
Configurations are optional and stored in $PROJECT_ROOT$/configuration
, written in PHP.
You can run this server with zero configuration if you do not need the following features.
Container
File: container.php
Configures additional autowiring steps if you intend to use robertwesner/dependency-injection
in complex use cases.
You can manually define container instances with this configuration.
Configuration::CONTAINER // Either let the container do all the heavy lifting via class names, // MySQLEntityManager would be automatically instantiated by the container. // This is necessary for usage of interfaces, rather than implementations. ::instantiate(EntityManagerInterface::class, MySQLEntityManager::class) // Or pass your own instance when necessary, since Bar is not to be autowired. ::register(FooInterface::class, new Bar('SOME VALUE'));
Bundles
File bundles.php
Loads external bundles (implementing BundleInterface) which may configure their own Container values.
Feel free to store configurations for your bundles in a subfolder inside
$PROJECT_ROOT$/configuration
.Example:
$PROJECT_ROOT$/configurations/database/database.yml
Configuration::BUNDLES ::load(FooBundle::class) // Optionally with additional configuration of any type, depending on the bundle. ::load(BarBundle::class, ['faz' => 'baz']);