DEV Community

Cover image for I built a PHP framework with Spring Boot style - zero dependencies, dynamic ORM, attribute-based routing (works on shared host).
CDdev
CDdev

Posted on

I built a PHP framework with Spring Boot style - zero dependencies, dynamic ORM, attribute-based routing (works on shared host).

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.

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]);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
huseyin_yontar profile image
Hüseyin Yontar

Looks cool 🎉

Collapse
 
cn8001 profile image
CDdev

Thanks!

Collapse
 
max_8c3335bce1affd7396b57 profile image
Max

Marvelous engineering. Well done !

Collapse
 
tuanacosgun profile image
Tuana Coşgun

👏👏👏

Collapse
 
aral_aaac_2093b6ae3fbca2a profile image
aral aaac

Fascinating. Wish we had this sooner