DEV Community

Cover image for Winter Boot: Spring Boot-style microservices in PHP 8.5
suvera
suvera

Posted on

Winter Boot: Spring Boot-style microservices in PHP 8.5

If you have used Spring Boot, you already know most of Winter Boot.

In an earlier job I was writing Spring Boot and PHP side by side. Spring Boot hooked me. You could start implementing right away, dependency injection just worked, and convention over configuration meant the code I wrote was my app logic and little else. Microservices came together fast. So I kept asking myself: why can't we do this in PHP? I made the first commit in February 2021 to find out.

Winter Boot is an open-source framework for writing microservices in PHP 8.5. It takes the Spring Boot programming model (beans, dependency injection, declarative transactions, caching, scheduling) and maps it onto PHP attributes. There is no XML and no code generation step.

This post is a short tour, not a manual. The full documentation covers everything else, and the code is on GitHub.

A small service

Here is a service that reads a product from Postgres and caches it:

<?php
namespace com\example\shop;

use dev\winterframework\cache\stereotype\CacheEvict;
use dev\winterframework\cache\stereotype\Cacheable;
use dev\winterframework\pdbc\PdbcTemplate;
use dev\winterframework\stereotype\Autowired;
use dev\winterframework\stereotype\Service;
use dev\winterframework\txn\stereotype\Transactional;

#[Service]
class ProductService {
    #[Autowired]
    private PdbcTemplate $db;

    #[Cacheable(cacheNames: 'products')]
    public function find(int $id): array {
        return $this->db->queryForMap(
            'SELECT * FROM products WHERE id = ?', 
            [$id]
        );
    }

    #[Transactional]
    #[CacheEvict(cacheNames: 'products', allEntries: true)]
    public function rename(int $id, string $name): void {
        $this->db->update(
            'UPDATE products SET name = ? WHERE id = ?',
            [$name, $id]
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

And a controller that exposes it (imports trimmed from here on):

#[RestController]
class ProductController {
    #[Autowired]
    private ProductService $products;

    #[GetMapping(path: '/products/{id}')]
    public function get(#[PathVariable] int $id): ResponseEntity {
        return ResponseEntity::ok()->withJson($this->products->find($id));
    }
}
Enter fullscreen mode Exit fullscreen mode

The entry point is one class:

require_once __DIR__ . '/vendor/autoload.php';

#[EnableCaching]
#[EnableTransactionManagement]
#[WinterBootApplication(
    configDirectory: [__DIR__ . '/config'],
    scanNamespaces: [['com\\example\\shop', __DIR__ . '/src']]
)]
class ShopApplication {
    public static function main(): void {
        (new WinterWebSwooleApplication())->run(self::class);
    }
}

ShopApplication::main();
Enter fullscreen mode Exit fullscreen mode

Plus a few lines of application.yml:

server:
    port: 8080

datasource:
    -   name: shopdb
        isPrimary: true
        url: "pgsql:host=localhost;dbname=shop"
        username: shop
        password: "*****"
Enter fullscreen mode Exit fullscreen mode

Run php ShopApplication.php, then curl localhost:8080/products/1. That's the whole app.

How it runs

How a request flows through a Winter Boot service

At boot the framework scans your namespaces, builds the bean graph and maps routes. This happens once per worker process, not per request. The server underneath is Swoole, so the process stays alive and the context stays in memory between requests. You pay the reflection and wiring cost up front, and request handling is just method calls.

#[Transactional] and #[Cacheable] are aspects. They wrap the method call without a generated proxy class. A small native PHP extension (winter_boot) handles the interception. That extension is required, which is one more thing to install. I think it's worth it. There are no proxy files to regenerate after a deploy, and final classes still work. The official Docker image ships with it already built, so in practice most people never compile it.

The rest of the box

The parts I end up using in almost every service:

  • PdbcTemplate for plain SQL
  • PPA (PHP Persistence API), a lightweight ORM: mark a class with #[Table], add PpaEntityTrait, and the framework writes the INSERT, UPDATE and DELETE for you. The Doctrine module is there if you need a full ORM.
  • #[Transactional] with propagation, rollback rules and multiple datasources
  • #[Scheduled] and #[Async] for background work, no separate cron or queue runner
  • A built-in migrator for plain .sql files, easy to run as a pre-deploy step
  • Actuator endpoints for health and info, plus Prometheus metrics and OpenTelemetry
  • Interceptors, CORS, coroutine-safe sessions

Optional modules cover Redis, Kafka, SQS, S3, OpenSearch and service discovery. You add a module through Composer and enable it in application.yml. Most of them live in winter-modules, and Doctrine ORM and DBAL support is in winter-doctrine.

One newer piece I like: put #[McpTool] on a controller method or a service method and it is served as a Model Context Protocol tool on /mcp. The input schema comes from the PHP signature and PHPDoc. So the same endpoint your frontend calls can be called by Claude or any other MCP client, through the same interceptors and auth.

Does it hold up?

The most demanding thing running on it right now is Snowprint, a self-hosted, cookie-less web analytics tool. In its benchmark it stores about 9,900 events per second on 2 CPUs and about 19,800 on 4 CPUs, with every accepted event persisted. It uses the parts shown above: REST controllers, PdbcTemplate, scheduled rollups, migrations and the actuator. It also tracks traffic on the Winter Boot docs site.

Rough edges

Winter Boot needs PHP 8.5, Swoole and the winter_boot extension. That is a narrower setup than a typical PHP-FPM host, and shared hosting is out. It is built for containers. If you deploy with Docker, use suvera/winter-boot as the base image and the extension problem goes away.

It is also a smaller project than Laravel or Symfony. The ecosystem is the framework plus its own modules, not thousands of third-party packages. Plain Composer libraries work fine, though, since it's still PHP.

Try it

composer require suvera/winter-boot
Enter fullscreen mode Exit fullscreen mode

The easiest way to run it is the official Docker image, suvera/winter-boot. It's built on php:8.5-cli-alpine and comes with every extension the framework needs, plus Redis, Kafka and PostgreSQL drivers, already installed. There is also suvera/winter-boot-mysql if you're on MySQL. The installation guide shows a ready-to-use Dockerfile.

Issues and pull requests are welcome. If you build something with it, I'd like to hear about it.

Top comments (0)