Przejdź do głównej treści
Grafika przedstawia ukryty obrazek

Budowa lekkiego frameworka PHP na komponentach Symfony

Zdjecie zwiazane z Budowa lekkiego frameworka PHP na komponentach Symfony

Pełne frameworki takie jak Symfony czy Laravel są bardzo potężne, ale w wielu projektach okazują się zbyt ciężkie. Jeśli potrzebujemy prostego backendu do niewielkiej aplikacji, mikroserwisu lub narzędzia wewnętrznego, dobrym rozwiązaniem jest stworzenie lekkiego frameworka opartego na komponentach Symfony.

W tym artykule pokażę jak zbudować mini-framework, który:

  • korzysta z komponentów Symfony
  • używa czystego PHP w widokach
  • posiada konfigurację w PHP
  • wspiera routing
  • obsługuje kontener DI
  • posiada middleware

Całość pozostaje bardzo lekka i łatwa do rozszerzania.

Założenia architektury

Framework będzie oparty na kilku kluczowych komponentach Symfony:

  • symfony/http-foundation – obsługa Request i Response
  • symfony/routing – system routingu
  • symfony/dependency-injection – kontener usług

Dzięki temu wykorzystujemy sprawdzone elementy ekosystemu Symfony bez konieczności instalowania całego frameworka.

Struktura projektu

Proponowana struktura katalogów:

project/
│
├─ public/
│   └─ index.php
│
├─ config/
│   ├─ routes.php
│   ├─ services.php
│   └─ middleware.php
│
├─ src/
│   ├─ Controller/
│   ├─ Middleware/
│   └─ Core/
│       └─ Middleware/
│
├─ templates/
│
└─ vendor/

Najważniejsze elementy aplikacji znajdują się w katalogu src/Core.

Punkt wejścia aplikacji

Plik public/index.php uruchamia aplikację.

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

use App\Core\App;

$app = new App(__DIR__.'/../config');

$response = $app->handle();
$response->send();

To tutaj tworzona jest instancja aplikacji oraz obsługiwany jest request.

Klasa aplikacji

Główna logika znajduje się w klasie App.

Jej zadaniem jest:

  • utworzenie kontenera DI
  • dopasowanie trasy
  • uruchomienie middleware
  • wywołanie kontrolera

Schemat działania wygląda następująco:

Request
  ↓
Router
  ↓
Middleware
  ↓
Controller
  ↓
Response

Kod src/Core/App.php:

namespace App\Core;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Core\Middleware\MiddlewareDispatcher;

class App
{
    private $container;
    private $router;

    public function __construct(private string $configPath)
    {
        $this->container = ContainerFactory::create($configPath);
        $this->router = new Router(require $configPath.'/routes.php');
    }

    public function handle(): Response
    {
        $request = Request::createFromGlobals();

        $route = $this->router->match($request);

        $controller = $this->container->get($route['controller']);

        $controllerCallable = function (Request $request) use ($controller, $route) {
            return call_user_func([$controller, $route['method']], $request);
        };

        $globalMiddleware = require $this->configPath.'/middleware.php';

        $routeMiddleware = $route['middleware'] ?? [];

        $middlewareClasses = array_merge(
            $globalMiddleware,
            $routeMiddleware
        );

        $middlewares = array_map(
            fn($m) => $this->container->get($m),
            $middlewareClasses
        );

        $dispatcher = new MiddlewareDispatcher($middlewares);

        return $dispatcher->handle($request, $controllerCallable);
    }

}

Routing

Trasy definiujemy w pliku config/routes.php.

use App\Controller\HomeController;
use App\Middleware\AuthMiddleware;

return [

    'home' => [
        'path' => '/',
        'controller' => HomeController::class,
        'method' => 'index',
        'middleware' => [
            AuthMiddleware::class
        ]
    ]

];

Każda trasa zawiera:

  • ścieżkę URL
  • klasę kontrolera
  • metodę kontrolera
  • opcjonalną listę middleware

Router wykorzystuje komponent Symfony Routing, który dopasowuje request do odpowiedniej trasy.

Kod routera src/Core/Router.php:

namespace App\Core;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;

class Router
{
    private UrlMatcher $matcher;

    public function __construct(array $routes)
    {
        $collection = new RouteCollection();

        foreach ($routes as $name => $r) {
            $collection->add($name, new Route(
                $r['path'],
                [
                    'controller' => $r['controller'],
                    'method' => $r['method'],
                    'middleware' => $r['middleware'] ?? [],
                ]
            ));
        }

        $context = new RequestContext();
        $this->matcher = new UrlMatcher($collection, $context);
    }

    public function match(Request $request): array
    {
        $this->matcher->getContext()->fromRequest($request);
        return $this->matcher->match($request->getPathInfo());
    }
}

Kontener Dependency Injection

Konfiguracja usług znajduje się w config/services.php.

use Symfony\Component\DependencyInjection\Reference;

return [

    App\Controller\HomeController::class => [
        'public' => true,
        'arguments' => [
            new Reference(App\Core\View::class)
        ]
    ],

    App\Core\View::class => [
        'arguments' => [
            __DIR__.'/../templates'
        ]
    ],

    App\Middleware\AuthMiddleware::class => [
        'public' => true
    ],

];

Kontener pozwala na łatwe wstrzykiwanie zależności pomiędzy komponentami aplikacji.

Kod Container Factory z src/Core/ContainerFactory.php:

namespace App\Core;

use Symfony\Component\DependencyInjection\ContainerBuilder;

class ContainerFactory
{
    public static function create(string $configPath)
    {
        $container = new ContainerBuilder();

        $services = require $configPath.'/services.php';

        foreach ($services as $id => $config) {

            $def = $container->register($id);

            if (!empty($config['arguments'])) {
                $def->setArguments($config['arguments']);
            }

            if (!empty($config['public'])) {
                $def->setPublic(true);
            }
        }

        $container->compile();

        return $container;
    }
}

System widoków

Widoki są napisane w czystym PHP, bez dodatkowego silnika templatingowego.

Przykładowy plik widoku:

<h1>Hello <?= htmlspecialchars($name) ?></h1>

Renderowanie odbywa się w klasie View, która ładuje plik template i przekazuje dane.

To rozwiązanie jest bardzo szybkie i nie wymaga dodatkowych bibliotek.

Kod klasy View z src/Core/View.php:

namespace App\Core;

class View
{
    public function __construct(
        private string $path
    ) {}

    public function render(string $template, array $data = []): string
    {
        extract($data);

        ob_start();

        require $this->path.'/'.$template.'.php';

        return ob_get_clean();
    }
}

Kontrolery

Kontroler obsługuje logikę konkretnej trasy.

Przykład:

namespace App\Controller;

use App\Core\View;
use Symfony\Component\HttpFoundation\Response;

class HomeController
{
    public function __construct(
        private View $view
    ) {}

    public function index()
    {
        return new Response(
            $this->view->render('home', [
                'name' => 'World'
            ])
        );
    }
}

Kontroler otrzymuje zależności z kontenera DI.

Middleware

Middleware pozwala przechwycić request przed kontrolerem.

Przykładowy middleware:

class AuthMiddleware implements MiddlewareInterface
{
    public function process(Request $request, callable $next): Response
    {
        if (!$request->headers->get('X-AUTH')) {
            return new Response('Unauthorized', 401);
        }

        return $next($request);
    }
}

Middleware może:

  • zatrzymać request
  • zmodyfikować request
  • zmodyfikować response

Kod src/Code/Middleware/MiddlewareInterface.php:

namespace App\Core\Middleware;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

interface MiddlewareInterface
{
    public function process(Request $request, callable $next): Response;
}

Kod src/Core/Middleware/MiddlewareDispatcher.php:

namespace App\Core\Middleware;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class MiddlewareDispatcher
{
    private array $middlewares;

    public function __construct(array $middlewares)
    {
        $this->middlewares = $middlewares;
    }

    public function handle(Request $request, callable $controller): Response
    {
        $next = $controller;

        foreach (array_reverse($this->middlewares) as $middleware) {

            $next = function (Request $request) use ($middleware, $next) {
                return $middleware->process($request, $next);
            };

        }

        return $next($request);
    }
}

Pipeline middleware

Request przechodzi przez wszystkie middleware w kolejności:

Request
 ↓
RequestLogger
 ↓
AuthMiddleware
 ↓
Controller
 ↓
Response

Framework obsługuje dwa poziomy middleware:

  • globalne – dla całej aplikacji
  • per-route – tylko dla wybranej trasy

Zalety takiego podejścia

Budowa własnego mikro-frameworka daje kilka korzyści:

  • bardzo szybkie uruchamianie aplikacji
  • pełna kontrola nad architekturą
  • brak zbędnych zależności
  • możliwość stopniowego rozwoju frameworka

Rozmiar całego kodu frameworka to zazwyczaj kilkaset linii.

Możliwe rozszerzenia

Jeśli projekt zacznie rosnąć, framework można łatwo rozbudować o:

  • autowiring kontenera
  • routing przez atrybuty PHP
  • generowanie URL
  • middleware groups
  • system eventów
  • CLI (symfony/console)

Dzięki temu stopniowo możemy uzyskać architekturę zbliżoną do pełnego frameworka, zachowując jednocześnie lekkość rozwiązania.

Podsumowanie

Komponenty Symfony pozwalają bardzo szybko zbudować własny framework dopasowany do potrzeb projektu.

Dzięki wykorzystaniu HttpFoundation, Routing oraz DependencyInjection otrzymujemy solidne fundamenty, a jednocześnie unikamy złożoności pełnego frameworka.

Takie podejście świetnie sprawdza się w:

  • małych aplikacjach webowych
  • mikroserwisach
  • narzędziach wewnętrznych
  • projektach edukacyjnych

Jednocześnie pozostawia dużą swobodę w dalszym rozwijaniu architektury.

Pobierz kod źródłowy mini frameworka.

6 marca 2026 1

Kategorie

programowanie

Tagi

php

Dziękujemy!
()

Powiązane wpisy


Informacja o cookies

Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.