PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

REST API – zasoby vs akcje, kody HTTP, struktura odpowiedzi, wersjonowanie

by Henryk Tews / wtorek, 09 lutego 2021 / Opublikowano w PHP

Napisanie endpointu który zwraca JSON to prosta sprawa. Zaprojektowanie REST API które jest spójne, przewidywalne i przyjemne w użyciu – to już rzemiosło. Pokazuję zasady dobrego projektowania REST API i jak przekładają się na konkretny kod PHP, z przykładami z perspektywy developera Magento 2 który często implementuje własne API lub integruje z zewnętrznymi.

Zasoby, nie akcje – podstawa REST

Największy błąd przy projektowaniu REST API to myślenie w kategoriach akcji zamiast zasobów. URL powinien opisywać zasób, a metoda HTTP – akcję na nim:

Źle (akcje w URL) Dobrze (zasoby + metody HTTP)
POST /getProducts GET /products
POST /createOrder POST /orders
GET /deleteProduct?id=5 DELETE /products/5
POST /updateProductStatus PATCH /products/5
GET /getOrdersByCustomer?id=3 GET /customers/3/orders

Kody HTTP – używaj właściwych

<?php

declare(strict_types=1);

class ProductController
{
    public function show(int $id): JsonResponse
    {
        try {
            $product = $this->productRepository->getById($id);
        } catch (NoSuchEntityException $e) {
            // 404 - zasób nie istnieje
            return new JsonResponse(['error' => 'Product not found'], 404);
        }

        // 200 - ok, zwróć zasób
        return new JsonResponse($this->serialize($product), 200);
    }

    public function store(Request $request): JsonResponse
    {
        $data = $request->getJson();

        $violations = $this->validator->validate($data);
        if (count($violations) > 0) {
            // 422 - dane niepoprawne semantycznie (przeszły parsowanie, ale są błędne)
            return new JsonResponse([
                'error'  => 'Validation failed',
                'errors' => $this->formatViolations($violations),
            ], 422);
        }

        $product = $this->productRepository->save($data);

        // 201 - zasób utworzony, nagłówek Location wskazuje na nowy zasób
        return new JsonResponse(
            $this->serialize($product),
            201,
            ['Location' => '/products/' . $product->getId()]
        );
    }

    public function update(int $id, Request $request): JsonResponse
    {
        // PATCH - częściowa aktualizacja (tylko pola z requesta)
        // PUT   - pełna zamiana zasobu (wszystkie pola)
        try {
            $product = $this->productRepository->getById($id);
        } catch (NoSuchEntityException $e) {
            return new JsonResponse(['error' => 'Product not found'], 404);
        }

        $product = $this->productRepository->patch($product, $request->getJson());

        // 200 - ok, zwróć zaktualizowany zasób
        return new JsonResponse($this->serialize($product), 200);
    }

    public function destroy(int $id): JsonResponse
    {
        try {
            $this->productRepository->deleteById($id);
        } catch (NoSuchEntityException $e) {
            return new JsonResponse(['error' => 'Product not found'], 404);
        }

        // 204 - ok, brak treści odpowiedzi
        return new JsonResponse(null, 204);
    }
}

Spójna struktura odpowiedzi

Klient API nie powinien zgadywać jak wygląda odpowiedź. Ustal jeden format i trzymaj się go:

<?php

// Zasób pojedynczy
{
    "data": {
        "id": 42,
        "type": "product",
        "attributes": {
            "sku": "MG-001",
            "name": "Produkt testowy",
            "price": 29.99,
            "status": "active"
        }
    }
}

// Kolekcja z paginacją
{
    "data": [
        {"id": 1, "type": "product", "attributes": { ... }},
        {"id": 2, "type": "product", "attributes": { ... }}
    ],
    "meta": {
        "total":        150,
        "per_page":      20,
        "current_page":   1,
        "last_page":      8
    },
    "links": {
        "first": "/products?page=1",
        "prev":  null,
        "next":  "/products?page=2",
        "last":  "/products?page=8"
    }
}

// Błąd
{
    "error": {
        "code":    "VALIDATION_FAILED",
        "message": "The request data is invalid",
        "details": [
            {"field": "price", "message": "Must be greater than 0"},
            {"field": "sku",   "message": "This value is already used"}
        ]
    }
}

Wersjonowanie API

Wersjonowanie przez URL to najprostsze i najbardziej popularne podejście – widoczne, łatwe do routowania, nie wymaga analizy nagłówków:

<?php

// Wersjonowanie przez URL - najprostsze
// /api/v1/products
// /api/v2/products  - nowa wersja z breaking changes

// Wersjonowanie przez nagłówek Accept - elegantsze, ale trudniejsze w cache
// Accept: application/vnd.myapi.v2+json

// Routing z wersją w Magento 2 - przez routes.xml
// Magento sam wersjonuje: /rest/V1/, /rest/V2/

// Własne API z wersjonowaniem w routes.xml:
// GET /rest/V1/vendor-module/products
// GET /rest/V2/vendor-module/products  - po breaking change

Autentykacja – tokeny, nie sesje

<?php

declare(strict_types=1);

class AuthMiddleware
{
    public function process(Request $request): ?JsonResponse
    {
        $authHeader = $request->getHeader('Authorization');

        if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) {
            return new JsonResponse(['error' => 'Missing authorization token'], 401);
        }

        $token = substr($authHeader, 7);

        try {
            $payload = $this->jwtService->verify($token);
            $request->setAttribute('user_id', $payload['sub']);
            $request->setAttribute('scopes', $payload['scopes']);
        } catch (TokenExpiredException $e) {
            return new JsonResponse(['error' => 'Token expired'], 401);
        } catch (InvalidTokenException $e) {
            return new JsonResponse(['error' => 'Invalid token'], 401);
        }

        return null; // null = przepuść dalej
    }
}

// Magento 2 - token klienta przez REST
// POST /rest/V1/integration/customer/token
// Body: {"username": "jan@example.com", "password": "haslo123"}
// Response: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

// Użycie tokenu w kolejnych requestach:
// Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Filtrowanie, sortowanie i paginacja – konwencje URL

<?php

// Konwencje URL dla filtrowania i sortowania
// GET /products?status=active&price_min=10&price_max=100
// GET /products?sort=price&direction=asc
// GET /products?page=2&per_page=20
// GET /products?fields=id,sku,name,price  - partial response

// Magento 2 - SearchCriteria przez REST
// GET /rest/V1/products?
//   searchCriteria[filter_groups][0][filters][0][field]=status&
//   searchCriteria[filter_groups][0][filters][0][value]=1&
//   searchCriteria[sortOrders][0][field]=name&
//   searchCriteria[sortOrders][0][direction]=ASC&
//   searchCriteria[pageSize]=20&
//   searchCriteria[currentPage]=1

// Bardziej czytelna alternatywa przez własne API:
// GET /api/v1/products?filter[status]=1&sort[name]=asc&page[size]=20&page[number]=1

Podsumowanie

Dobre REST API to API które jest przewidywalne – developer integrujący się z nim nie potrzebuje dzwonić do Ciebie żeby rozumieć jak działa. Spójne kody HTTP, jednolita struktura odpowiedzi, zasoby zamiast akcji w URL i wersjonowanie od początku – to decyzje które procentują przez cały czas życia API, szczególnie gdy zaczynają integrować się z nim zewnętrzne systemy.

About Henryk Tews

Co możesz przeczytać następne

PHP 7.4 w praktyce – pułapki typed properties, hydratacja, zapowiedź PHP 8.0
PimCore – CMS + PIM + DAM, klasy obiektów, Data Hub GraphQL, integracja z Magento
PHP 8.3 po premierze – typed constants, json_validate(), clone with w praktyce
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}