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:
- 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
This creates a Docker PHP-FPM + Nginx Project and is the preferred way of use.
composer create-project robertwesner/simple-mvc-php-docker-templateThis creates a new project with the required folder structure.
composer create-project robertwesner/simple-mvc-php-templateIf you already have a project, require the package and migrate your files manually.
composer require robertwesner/simple-mvc-phpPROJECT_ROOT
|-- public
| '-- any publicly accessible data like JS, CSS, images, ...
|-- routes
| '-- PHP routing scripts
|-- views
| '-- twig views
|-- src
|-- vendor
'-- route.php
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,
]);
});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']);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) {
// ...
});Configurations are optional and stored in $PROJECT_ROOT$/configurations, written in PHP.
You can run this server with zero configuration if you do not need the following features.
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'));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']);Requires the use of configuration files, refer to the previous section for mor information.
Use your preferred ThrowableHandler by instantiating it as ThrowableHandlerInterface.
By default, without registering a handler, no exception information will be stored or printed.
Example: PrintThrowableHandler outputs directly to the browser
Configuration::CONTAINER
::instantiate(ThrowableHandlerInterface::class, PrintThrowableHandler::class);Example: StderrThrowableHandler outputs into the server stderr and doesn't leak to the browser
Configuration::CONTAINER
::instantiate(ThrowableHandlerInterface::class, StderrThrowableHandler::class);You can implement your own handler quite easily for additional tasks like sending automated mails.