Zum Inhalt springen

Spring Boot für PHP-Entwickler: Parallelen zu Symfony und erste REST-API

Veröffentlicht am 10. Juli 2025 | ca. 2 Min. Lesezeit |

Als PHP-Entwickler mit Symfony-Erfahrung steht man Java und Spring Boot oft mit gemischten Gefühlen gegenüber. Der Ruf: kompliziert, langsam im Setup, viel Boilerplate. Die Realität mit Spring Boot ist anders. Die Konzepte sind vertraut — es ist im Wesentlichen Symfony, nur in Java.

Konzept-Mapping: Symfony ↔ Spring Boot

Symfony Spring Boot Beschreibung
services.yaml @Component, @Service Service-Registrierung
#[Route] @RequestMapping, @GetMapping Routing
AbstractController @RestController Controller-Basis
Doctrine ORM Spring Data JPA / Hibernate ORM
Repository-Interface JpaRepository<Entity, ID> Datenbankzugriff
EventSubscriber @EventListener Events
.env application.properties Konfiguration
Composer Maven / Gradle Dependency Management
bin/console ./mvnw, ./gradlew CLI

Projekt-Setup mit Spring Initializr

Das Äquivalent zu composer create-project symfony/skeleton ist start.spring.io. Dort wählt man:

  • Project: Maven oder Gradle
  • Language: Java
  • Spring Boot Version: 3.x (aktuell)
  • Dependencies: Spring Web, Spring Data JPA, H2 Database (für Entwicklung)

Das generierte Projekt hat folgende Struktur:

my-api/
├── src/main/java/com/example/myapi/
│   ├── MyApiApplication.java       # Einstiegspunkt (wie public/index.php)
│   ├── controller/
│   ├── service/
│   ├── repository/
│   └── entity/
├── src/main/resources/
│   └── application.properties      # Konfiguration (wie .env)
├── src/test/java/                   # Tests
└── pom.xml                          # Composer-äquivalent

Dependency Injection — wie Symfony

Symfony und Spring Boot nutzen beide Dependency Injection als Kernkonzept. In Symfony schreibt man:

<?php

declare(strict_types=1);

namespace App\Controller;

use App\Service\ProductService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class ProductController extends AbstractController
{
    public function __construct(
        private readonly ProductService $productService
    ) {}
}

In Spring Boot ist es fast identisch:

package com.example.myapi.controller;

import com.example.myapi.service.ProductService;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {

    private final ProductService productService;

    // Constructor Injection — analog zu Symfony
    public ProductController(ProductService productService) {
        this.productService = productService;
    }
}

Spring erkennt @Service-, @Component- und @Repository-Annotierte Klassen automatisch und injiziert sie. Genau wie Symfony Service-Autowiring.

REST-Controller

Symfony:

<?php

declare(strict_types=1);

namespace App\Controller\Api;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

#[Route('/api/products', name: 'api_products_')]
class ProductController extends AbstractController
{
    #[Route('', name: 'list', methods: ['GET'])]
    public function list(): JsonResponse
    {
        return $this->json(['products' => []]);
    }

    #[Route('/{id}', name: 'show', methods: ['GET'])]
    public function show(int $id): JsonResponse
    {
        return $this->json(['id' => $id]);
    }
}

Spring Boot:

package com.example.myapi.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public List<Product> list() {
        return productService.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> show(@PathVariable Long id) {
        return productService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<Product> create(@RequestBody @Valid ProductDto dto) {
        Product created = productService.create(dto);
        return ResponseEntity.status(201).body(created);
    }
}

Entity und Repository (analog Doctrine)

In Symfony definiert man eine Doctrine-Entity:

<?php

declare(strict_types=1);

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'products')]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private string $name;
}

In Spring Boot (JPA/Hibernate):

package com.example.myapi.entity;

import jakarta.persistence.*;

@Entity
@Table(name = "products")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 255)
    private String name;

    // Getter und Setter
    public Long getId() { return id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

Repository

Symfony mit Doctrine:

class ProductRepository extends ServiceEntityRepository
{
    public function findByCategory(string $category): array;
}

Spring Boot mit JPA:

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {
    // Automatisch generierte Methode aufgrund des Namens!
    List<Product> findByCategory(String category);
}

Spring Data JPA generiert die Query automatisch aus dem Methodennamen — ähnlich wie Doctrine QueryBuilder, aber noch magischer.

application.properties — das .env-Äquivalent

# Datenbankverbindung (analog DATABASE_URL)
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=myuser
spring.datasource.password=mypassword

# JPA
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false

# Server-Port
server.port=8080

# Logging
logging.level.com.example=DEBUG

Tests — JUnit statt PHPUnit

package com.example.myapi.controller;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(ProductController.class)
class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void listProductsReturnsEmptyArray() throws Exception {
        mockMvc.perform(get("/api/products"))
            .andExpect(status().isOk())
            .andExpect(content().contentType("application/json"))
            .andExpect(jsonPath("$").isArray());
    }
}

Fazit

Spring Boot ist für Symfony-Entwickler überraschend zugänglich. Dependency Injection, Annotationen statt XML, ein ORM mit Repository-Pattern und eine konventionsbasierte Konfiguration — das sind vertraute Konzepte. Die Hauptunterschiede liegen in der statischen Typisierung (was tatsächlich hilft), dem Build-Prozess (Maven/Gradle statt Composer) und dem Deployment (JAR-Datei statt PHP-Dateien auf einem Webserver).

Thomas Wunner

Thomas Wunner

Fachinformatiker für Anwendungsentwicklung mit Ausbildereignungsprüfung und über 14 Jahre Erfahrung im Aufbau skalierbarer Webanwendungen mit Symfony und Shopware. Abseits der Tastatur ist Thomas als Rettungsschwimmer in der Wasserwacht aktiv, legt als DJ auf und erkundet die Umgebung auf dem Motorrad.

Kommentare

Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.