sevval42 / spade
A lightweight PHP Framework for personal projects
Requires
- symfony/var-dumper: ^7.3
Requires (Dev)
- captainhook/captainhook: ^5.25
- friendsofphp/php-cs-fixer: ^3.75
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.2
README
A lightweight PHP framework for learning purposes and small personal projects.
This framework provides dependency injection, request and response classes and a router, a simple ORM and very simple templating. Examples can be seen in this projects src directory.
Note: Spade is intended for educational use and small projects, it therefore is not optimized or secure in any way.
Installation
Install will be available via composer
composer require sevval42/Spade
If you want to try this example repository out, clone it and run docker:
docker compose up -d --build
docker compose run --rm app composer install
docker compose run --rm app ./vendor/bin/captainhook install --force
The routes in routes/web.php can be tried out at http://localhost
Basic usage
This section will describe the base functionality provided by this framework, as well as give some short examples and restrictions.
Router.php
A route can be added to the Router by specifying the method, route and a callable, for example like this:
$router->addRoute('GET', '/users/{id:\d+}', [UserController::class, 'handle']);
The route parameter uses {variableName:regex} for specifying attributes. The handle method of the given Controller can then use the attributes like this:
public function handle(int $id): Response { ... }
You can dispatch a request like this:
public function handle(Request $request): Response { $method = $request->getMethod(); $uri = $request->getUri(); try { return $this->router->dispatch($method, $uri); } catch (RouteNotFoundException $exception) { return new NotFoundResponse(); } catch (RouteNotAllowedException $exception) { return new InternalServerErrorResponse(); } }
All this is actually handled by the App class, which can be called to initialize and dispatch routes with the router, by calling its initRoutes(array $routes) and handle(Request $request) methods.
Responses
There are a few basic Response classes, this framework provides. Simple message Responses can use the base Responseclass, while the frameworks templating uses the ViewResponse which will be further discussed in that section.
Dependency Injection
The dependency injection is handled by the Container.php class. You can set the necessary base dependencies with the set(YourClass::class, fn() => $yourClass) method, and then get(YourClass::class) objects of the given class.
This should be setup in the entry point of your program, an example can be seen in public/index.php.
Database layer
Spade has made a few abstractions, although it does not have a query builder yet.
Connection.php
This is the base class, that abstracts the PDO class given by PHP. It gives the user multiple methods to do simple queries, like:
Queries
$connection->query('SELECT * FROM user WITH id=:id', ['id' => 4]); $result = $connection->fetchOne();
Updates
$connection->update('user', ['first_name' => 'john', 'last_name' => 'cool'], 4);
Deletes
$connection->delete('user', 4);
This layer should not be used for entity handling though, as this is handled by the BaseRepository and EntityManager classes:
BaseRepository.php and BaseEntity.php
This frameworks ORM maps database tables to Classes which extend from the abstract BaseEntity class, thus Entity has the $id identifier. Furthermore, each entity class needs to implement the getTableName(): string method, which returns the database table name.
Class Attributes need to be snake_case in the database and camelCase in the Entity. For example,
create table Users ( id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, first_name VARCHAR(50), last_name VARCHAR(50), email VARCHAR(50), birth_date DATE );
maps to
<?php declare(strict_types=1); namespace App\Entities; use DateTime; use Spade\Database\BaseEntity; class User extends BaseEntity { private ?string $firstName; private ?string $lastName; private ?string $email; private ?DateTime $birthDate; ...
The abstract BaseRepository.php class gives functionality for reading data and implements find(int $id) and fetchAll() methods. Classes that extend from this BaseRepository need to implement the getEntity() method, which returns the Entity class-string, this repository watches.
A UserRepository could look like this:
class UserRepository extends BaseRepository { public function getEntity(): string { return User::class; } public function find(int $id): ?User { /** @phpstan-ignore return.type */ return parent::find($id); } }
EntityManager.php
To persist changes in the database, the EntityManager is used. It automatically tracks objects which extend from the BaseEntity class and that are fetched from the database using the Hydrator.php class.
New entities can be persisted with the persist($entity) method and removed with the remove($entity).
Any changes will be written to the database, when calling the flush method: $entityManager->flush().
Templating
Spade provides very simple templating with pages and partials. It does not have any logic yet. The TemplateService.php class needs to be initialized with the paths to the pages and partials like this (example):
$templateService = new TemplateService( PROJECT_ROOT . '/src/Templates/Pages/', PROJECT_ROOT . '/src/Templates/Partials/' );
These templates can than be returned by the ViewResponse class like this:
return new ViewResponse('user/info', $data)
where the info.php template is in src/Templates/Pages/user/info.php.
A simple page might look like this (info.php):
<html> <header><title>User {{ user.id }}</title></header> <body> <h1>User info for user {{ user.id }}</h1> <ul> <li>Name: {{{ user.firstName }}} {{ user.lastName }}</li> <li>Email: {{ user.email }}</li> <li>Birthday: {{ user.birthday }}</li> </ul> </body> </html>
where variables with three curly braces (like {{{ user.firstName }}}) are not escaped.
The given data array might look like this for the given example:
$data = [ 'user' => [ 'id' => $user->getId(), 'firstName' => $user->getFirstName(), 'lastName' => $user->getLastName(), 'email' => $user->getEmail(), 'birthday' => $user->getBirthDate()?->format('d.m.Y'), ] ];
If a variable is not filled in the $data array, the template leaves the variable empty.
License
This project is licensed under the MIT License. See the LICENSE file for details.