I was using Spring Boot and it was my default selection until one client wanted to build a simple website. I had started coding with PHP but hadn't used it for a while. Then, I remembered the simplicity and minimalism of mixing PHP, HTML, CSS, and JS into one file (or separate files with .js etc), and how easy and cheap the hosting management was. But I didn't want to leave Spring's features behind, like JPA and dependency injection.
So, I built a microframework with zero dependency that even works on shared hosting.
- Website & Docs: pointartframework.com
- GitHub Repo: Cn8001/PointArt
The site itself is using PointArt and resides on shared hosting. For more documentation please visit the website.
PointArt is a PHP micro-framework that brings Spring Boot's attribute-based style to PHP 8.1 (PHP 8.1+ since it introduced attributes, and PDO driver is required):
#[Router(name: 'user', path: '/user')]
class UserController {
#[Wired]
private UserRepository $userRepository;
#[Route('/list', HttpMethod::GET)]
public function index(): string {
return Renderer::render('user.list', ['users' => $this->userRepository->findAll()]);
}
#[Route('/show/{id}', HttpMethod::GET)]
public function show(int $id): string {
$user = User::find($id);
return $user ? Renderer::render('user.show', ['user' => $user]) : httpError(404);
}
#[Route('/create', HttpMethod::POST)]
public function create(
#[RequestParam] string $name,
#[RequestParam] string $email
): string {
$user = new User();
$user->name = $name;
$user->email = $email;
$user->save();
return Renderer::render('user.show', ['user' => $user]);
}
}
The repository pattern generates query implementations from method names — just like Spring Data JPA. If you need custom SQL it also allows it:
abstract class UserRepository extends Repository {
protected string $entityClass = User::class;
abstract public function findByName(string $name): array;
abstract public function findOneByEmail(string $email): ?User;
abstract public function existsByEmail(string $email): bool;
abstract public function countByActiveTrue(): int;
// Custom SQL when you need it
#[Query("SELECT COUNT(*) FROM users")]
abstract public function countAll(): int;
}
Features:
-
#[Router]/#[Route]— attribute-based routing with path params and query string support -
#[Wired]— property injection, no constructor boilerplate -
#[Entity]/#[Column]/#[Id]— ORM for SQLite, MySQL, PostgreSQL - Spring Data-style dynamic finders +
#[Query]for raw SQL - Route + service registry is cached — no Reflection overhead on every request
- No Composer, no build step, no CLI — literally copy files to a server and go (that is the reason why I did not use a full framework.)
It's been a fun project to build from scratch (the dynamic repository finder parser was the trickiest part). Would love any feedback and open to contributions. You can see the future ideas on the website.
Top comments (5)
Looks cool 🎉
Thanks!
Marvelous engineering. Well done !
👏👏👏
Fascinating. Wish we had this sooner