Przejdź do głównej zawartości

Wzorce rozwoju PHP

Przyspiesz rozwój PHP z Cursor i Claude Code. Ten przewodnik obejmuje nowoczesne praktyki PHP, frameworki Laravel i Symfony, modernizację legacy kodu, testowanie z PHPUnit i budowanie skalowalnych aplikacji enterprise z pomocą AI.

  1. Inicjalizacja projektu PHP

    Okno terminala
    # Projekt Laravel
    Zapytaj: "Stwórz nowy projekt Laravel z autentyfikacją, zasobami API i kolejkami zadań"
    # Lub użyj trybu Agent
    Agent: "Skonfiguruj Laravel API z autoryzacją Sanctum, wzorcem repository i Docker"
  2. Konfiguracja środowiska developmentu

    composer.json
    {
    "require": {
    "php": "^8.2",
    "laravel/framework": "^11.0",
    "laravel/sanctum": "^4.0"
    },
    "require-dev": {
    "phpunit/phpunit": "^10.0",
    "mockery/mockery": "^1.6",
    "phpstan/phpstan": "^1.10",
    "laravel/pint": "^1.13"
    }
    }
  3. Skonfiguruj reguły AI

    Okno terminala
    # .cursorrules lub CLAUDE.md
    - Używaj funkcji PHP 8.2+ (właściwości readonly, enums, itp.)
    - Przestrzegaj standardów kodowania PSR-12
    - Używaj deklaracji typów wszędzie
    - Implementuj wzorzec repository dla dostępu do danych
    - Pisz testy dla wszystkich metod publicznych
    - Używaj wstrzykiwania zależności
    - Przestrzegaj zasad SOLID
// Prompt AI
Agent: "Stwórz kompletne REST API do zarządzania produktami z:
- Zasobami API do formatowania odpowiedzi
- Form Requests dla walidacji
- Wzorcem repository
- Warstwą service
- Wersjonowaniem API
- Ograniczaniem częstotliwości"
// Wygenerowana struktura kodu:
// app/Http/Controllers/Api/V1/ProductController.php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreProductRequest;
use App\Http\Resources\ProductResource;
use App\Services\ProductService;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class ProductController extends Controller
{
public function __construct(
private readonly ProductService $productService
) {}
public function index(): AnonymousResourceCollection
{
$products = $this->productService->paginate(
perPage: request()->integer('per_page', 15),
search: request()->string('search')
);
return ProductResource::collection($products);
}
public function store(StoreProductRequest $request): ProductResource
{
$product = $this->productService->create($request->validated());
return ProductResource::make($product)
->response()
->setStatusCode(201);
}
}
// app/Services/ProductService.php
namespace App\Services;
use App\Models\Product;
use App\Repositories\ProductRepository;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class ProductService
{
public function __construct(
private readonly ProductRepository $repository
) {}
public function create(array $data): Product
{
return DB::transaction(function () use ($data) {
$product = $this->repository->create($data);
event(new ProductCreated($product));
return $product;
});
}
public function paginate(int $perPage, ?string $search): LengthAwarePaginator
{
return $this->repository->paginate($perPage, $search);
}
}
app/Models/Order.php
// Prompt AI: "Stwórz zaawansowane modele Eloquent z relacjami i scope'ami"
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Builder;
class Order extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'user_id',
'status',
'total_amount',
'notes',
];
protected $casts = [
'total_amount' => 'decimal:2',
'completed_at' => 'datetime',
'metadata' => 'array',
];
protected static function booted(): void
{
static::creating(function (Order $order) {
$order->order_number = self::generateOrderNumber();
});
}
// Relacje
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function items(): HasMany
{
return $this->hasMany(OrderItem::class);
}
// Scope'y
public function scopePending(Builder $query): Builder
{
return $query->where('status', OrderStatus::Pending);
}
public function scopeCompleted(Builder $query): Builder
{
return $query->where('status', OrderStatus::Completed)
->whereNotNull('completed_at');
}
public function scopeForUser(Builder $query, User $user): Builder
{
return $query->where('user_id', $user->id);
}
// Akcesory i mutatory
public function getTotalFormattedAttribute(): string
{
return number_format($this->total_amount, 2);
}
// Logika biznesowa
public function complete(): void
{
$this->update([
'status' => OrderStatus::Completed,
'completed_at' => now(),
]);
event(new OrderCompleted($this));
}
private static function generateOrderNumber(): string
{
return 'ORD-' . now()->format('Ymd') . '-' .
str_pad(random_int(1, 9999), 4, '0', STR_PAD_LEFT);
}
}
// Prompt AI
Zapytaj: "Stwórz system przetwarzania zadań z:
- Logiką ponawiania
- Ograniczaniem częstotliwości
- Obsługą nieudanych zadań
- Łączeniem zadań
- Śledzeniem postępu"
// app/Jobs/ProcessOrderJob.php
namespace App\Jobs;
use App\Models\Order;
use App\Services\OrderProcessor;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\Middleware\WithoutOverlapping;
class ProcessOrderJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $maxExceptions = 2;
public int $timeout = 120;
public function __construct(
public Order $order
) {}
public function middleware(): array
{
return [
new WithoutOverlapping($this->order->id),
new RateLimited('order-processing'),
];
}
public function handle(OrderProcessor $processor): void
{
$processor->process($this->order);
// Łańcuch następnego zadania
ProcessOrderNotificationJob::dispatch($this->order)
->delay(now()->addMinutes(5));
}
public function failed(\Throwable $exception): void
{
// Logowanie niepowodzenia
\Log::error('Przetwarzanie zamówienia nie powiodło się', [
'order_id' => $this->order->id,
'error' => $exception->getMessage(),
]);
// Powiadom administratora
AdminNotification::send(
'Przetwarzanie zamówienia nie powiodło się: ' . $this->order->order_number
);
}
public function retryUntil(): \DateTime
{
return now()->addHours(2);
}
}
src/Service/PaymentService.php
// Prompt AI: "Stwórz usługę Symfony z wstrzykiwaniem zależności i konfiguracją"
namespace App\Service;
use App\Entity\Payment;
use App\Repository\PaymentRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final class PaymentService
{
public function __construct(
private readonly PaymentRepository $repository,
private readonly HttpClientInterface $httpClient,
private readonly LoggerInterface $logger,
#[Autowire('%payment.api_key%')]
private readonly string $apiKey,
#[Autowire('%payment.webhook_secret%')]
private readonly string $webhookSecret,
) {}
public function processPayment(Payment $payment): PaymentResult
{
$this->logger->info('Przetwarzanie płatności', [
'payment_id' => $payment->getId(),
'amount' => $payment->getAmount(),
]);
try {
$response = $this->httpClient->request('POST', 'https://api.payment.com/charge', [
'headers' => [
'Authorization' => 'Bearer ' . $this->apiKey,
],
'json' => [
'amount' => $payment->getAmount(),
'currency' => $payment->getCurrency(),
'source' => $payment->getSource(),
],
]);
$data = $response->toArray();
$payment->setStatus(PaymentStatus::Completed);
$payment->setTransactionId($data['transaction_id']);
$this->repository->save($payment, true);
return new PaymentResult(
success: true,
transactionId: $data['transaction_id']
);
} catch (\Exception $e) {
$this->logger->error('Płatność nie powiodła się', [
'payment_id' => $payment->getId(),
'error' => $e->getMessage(),
]);
$payment->setStatus(PaymentStatus::Failed);
$this->repository->save($payment, true);
throw new PaymentException('Przetwarzanie płatności nie powiodło się', 0, $e);
}
}
}
// config/services.yaml
services:
App\Service\PaymentService:
arguments:
$httpClient: '@payment.http_client'
tags:
- { name: monolog.logger, channel: payment }
payment.http_client:
class: Symfony\Component\HttpClient\HttpClient
factory: ['Symfony\Component\HttpClient\HttpClient', 'create']
arguments:
- timeout: 30
max_redirects: 0
// Prompt AI
Agent: "Stwórz encje Doctrine z:
- Złożonymi relacjami
- Callback'ami lifecycle
- Niestandardowymi repozytoriami
- Optymalizacją zapytań
- Event listener"
// src/Entity/Product.php
namespace App\Entity;
use App\Repository\ProductRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: ProductRepository::class)]
#[ORM\Table(name: 'products')]
#[ORM\Index(name: 'idx_product_sku', columns: ['sku'])]
#[ORM\HasLifecycleCallbacks]
class Product
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
#[Assert\Length(min: 3, max: 255)]
private string $name;
#[ORM\Column(length: 100, unique: true)]
#[Assert\NotBlank]
#[Assert\Regex('/^[A-Z0-9\-]+$/')]
private string $sku;
#[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
#[Assert\Positive]
private float $price;
#[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'products')]
#[ORM\JoinColumn(nullable: false)]
private Category $category;
#[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'products')]
#[ORM\JoinTable(name: 'product_tags')]
private Collection $tags;
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $createdAt;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
private ?\DateTimeImmutable $updatedAt = null;
public function __construct()
{
$this->tags = new ArrayCollection();
}
#[ORM\PrePersist]
public function setCreatedAtValue(): void
{
$this->createdAt = new \DateTimeImmutable();
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
// Gettery i settery...
}
// src/Repository/ProductRepository.php
namespace App\Repository;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Product>
*/
class ProductRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Product::class);
}
public function findBySearchCriteria(
?string $search = null,
?int $categoryId = null,
array $orderBy = ['name' => 'ASC']
): QueryBuilder {
$qb = $this->createQueryBuilder('p')
->leftJoin('p.category', 'c')
->leftJoin('p.tags', 't');
if ($search) {
$qb->andWhere('p.name LIKE :search OR p.sku LIKE :search')
->setParameter('search', '%' . $search . '%');
}
if ($categoryId) {
$qb->andWhere('c.id = :categoryId')
->setParameter('categoryId', $categoryId);
}
foreach ($orderBy as $field => $direction) {
$qb->addOrderBy('p.' . $field, $direction);
}
return $qb;
}
public function findProductsWithLowStock(int $threshold = 10): array
{
return $this->createQueryBuilder('p')
->andWhere('p.stockQuantity < :threshold')
->setParameter('threshold', $threshold)
->orderBy('p.stockQuantity', 'ASC')
->getQuery()
->getResult();
}
}
// Prompt AI: "Zaimplementuj nowoczesne PHP ze strict types, enum'ami i readonly properties"
declare(strict_types=1);
namespace App\Domain\Order;
// Enum'y PHP 8.1
enum OrderStatus: string
{
case Pending = 'pending';
case Processing = 'processing';
case Completed = 'completed';
case Cancelled = 'cancelled';
case Refunded = 'refunded';
public function canTransitionTo(self $newStatus): bool
{
return match ($this) {
self::Pending => in_array($newStatus, [
self::Processing,
self::Cancelled
]),
self::Processing => in_array($newStatus, [
self::Completed,
self::Cancelled
]),
self::Completed => $newStatus === self::Refunded,
default => false,
};
}
public function label(): string
{
return match ($this) {
self::Pending => 'Oczekuje na przetworzenie',
self::Processing => 'W trakcie',
self::Completed => 'Zakończone',
self::Cancelled => 'Anulowane',
self::Refunded => 'Zwrócone',
};
}
}
// Klasy readonly PHP 8.2
readonly class OrderData
{
public function __construct(
public string $id,
public string $customerId,
public Money $totalAmount,
public array $items,
public OrderStatus $status = OrderStatus::Pending,
public ?\DateTimeImmutable $completedAt = null,
) {
if (empty($items)) {
throw new \InvalidArgumentException('Zamówienie musi mieć przynajmniej jeden element');
}
}
}
// Value Object
final readonly class Money
{
public function __construct(
public int $amount,
public string $currency = 'PLN'
) {
if ($amount < 0) {
throw new \InvalidArgumentException('Kwota nie może być ujemna');
}
if (!in_array($currency, ['PLN', 'EUR', 'USD'])) {
throw new \InvalidArgumentException('Nieprawidłowa waluta');
}
}
public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException('Nie można dodawać różnych walut');
}
return new self(
$this->amount + $other->amount,
$this->currency
);
}
public function format(): string
{
return number_format($this->amount / 100, 2) . ' ' . $this->currency;
}
}
// Prompt AI
Agent: "Zmodernizuj ten legacy kod PHP:
- Konwertuj na autoload PSR-4
- Dodaj deklaracje typów
- Zamień mysql_* na PDO
- Zaimplementuj odpowiednią obsługę błędów
- Dodaj testy jednostkowe"
// Przed: legacy.php
function get_user($id) {
$result = mysql_query("SELECT * FROM users WHERE id = $id");
return mysql_fetch_assoc($result);
}
// Po: src/Repository/UserRepository.php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\User;
use PDO;
use PDOException;
final class UserRepository
{
public function __construct(
private readonly PDO $connection
) {}
public function findById(int $id): ?User
{
try {
$statement = $this->connection->prepare(
'SELECT id, email, name, created_at
FROM users
WHERE id = :id'
);
$statement->execute(['id' => $id]);
$data = $statement->fetch(PDO::FETCH_ASSOC);
if ($data === false) {
return null;
}
return User::fromArray($data);
} catch (PDOException $e) {
throw new RepositoryException(
"Nie udało się pobrać użytkownika o id {$id}",
0,
$e
);
}
}
}
// Prompt AI: "Stwórz testy PHPUnit z mockami, providerami danych i testami integracyjnymi"
declare(strict_types=1);
namespace Tests\Unit\Service;
use App\Entity\Order;
use App\Repository\OrderRepository;
use App\Service\NotificationService;
use App\Service\OrderService;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
class OrderServiceTest extends TestCase
{
use ProphecyTrait;
private ObjectProphecy $repository;
private ObjectProphecy $notificationService;
private OrderService $service;
protected function setUp(): void
{
$this->repository = $this->prophesize(OrderRepository::class);
$this->notificationService = $this->prophesize(NotificationService::class);
$this->service = new OrderService(
$this->repository->reveal(),
$this->notificationService->reveal()
);
}
/**
* @test
*/
public function it_completes_order_successfully(): void
{
// Arrange
$order = $this->createOrder(OrderStatus::Processing);
$this->repository->find(123)->willReturn($order);
$this->repository->save($order)->shouldBeCalled();
$this->notificationService->sendOrderCompleted($order)->shouldBeCalled();
// Act
$result = $this->service->completeOrder(123);
// Assert
$this->assertTrue($result);
$this->assertEquals(OrderStatus::Completed, $order->getStatus());
$this->assertNotNull($order->getCompletedAt());
}
/**
* @test
* @dataProvider invalidOrderStatusProvider
*/
public function it_cannot_complete_order_with_invalid_status(
OrderStatus $status
): void {
// Arrange
$order = $this->createOrder($status);
$this->repository->find(123)->willReturn($order);
// Act & Assert
$this->expectException(InvalidOrderStateException::class);
$this->service->completeOrder(123);
}
public function invalidOrderStatusProvider(): array
{
return [
'już zakończone' => [OrderStatus::Completed],
'anulowane' => [OrderStatus::Cancelled],
'zwrócone' => [OrderStatus::Refunded],
];
}
/**
* @test
* @group integration
*/
public function it_handles_concurrent_order_completion(): void
{
// To byłby rzeczywisty test integracyjny
// z transakcjami bazy danych
$this->markTestIncomplete('Test integracyjny do zaimplementowania');
}
private function createOrder(OrderStatus $status): Order
{
$order = new Order();
$order->setId(123);
$order->setStatus($status);
$order->setTotalAmount(99.99);
return $order;
}
}
// Prompt AI
Zapytaj: "Zaimplementuj strategię cache'owania z:
- Redis dla cache'owania obiektów
- Cache'owanie wyników zapytań
- Unieważnianie cache
- Rozgrzewanie cache
- Monitorowanie wydajności"
namespace App\Service;
use App\Entity\Product;
use App\Repository\ProductRepository;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
final class CachedProductService
{
private const CACHE_TTL = 3600; // 1 godzina
private const CACHE_PREFIX = 'product:';
public function __construct(
private readonly ProductRepository $repository,
private readonly CacheItemPoolInterface $cache,
private readonly LoggerInterface $logger
) {}
public function findById(int $id): ?Product
{
$cacheKey = self::CACHE_PREFIX . $id;
$item = $this->cache->getItem($cacheKey);
if ($item->isHit()) {
$this->logger->debug('Cache hit dla produktu', ['id' => $id]);
return $item->get();
}
$product = $this->repository->find($id);
if ($product !== null) {
$item->set($product);
$item->expiresAfter(self::CACHE_TTL);
$this->cache->save($item);
}
return $product;
}
public function findByCategory(int $categoryId): array
{
$cacheKey = self::CACHE_PREFIX . 'category:' . $categoryId;
$item = $this->cache->getItem($cacheKey);
if ($item->isHit()) {
return $item->get();
}
$products = $this->repository->findBy(['category' => $categoryId]);
$item->set($products);
$item->expiresAfter(self::CACHE_TTL);
$item->tag(['category:' . $categoryId]);
$this->cache->save($item);
return $products;
}
public function invalidateProduct(int $id): void
{
$this->cache->deleteItem(self::CACHE_PREFIX . $id);
// Unieważnij powiązane cache
$product = $this->repository->find($id);
if ($product) {
$this->cache->invalidateTags([
'category:' . $product->getCategory()->getId()
]);
}
}
public function warmCache(): void
{
$this->logger->info('Rozpoczynanie rozgrzewania cache');
// Rozgrzej popularne produkty
$popularProducts = $this->repository->findPopular(limit: 100);
foreach ($popularProducts as $product) {
$this->findById($product->getId());
}
$this->logger->info('Rozgrzewanie cache zakończone', [
'products_cached' => count($popularProducts)
]);
}
}

Wytyczne rozwoju PHP

  1. Bezpieczeństwo typów - Używaj strict types i deklaracji typów
  2. Nowoczesne PHP - Wykorzystuj funkcje PHP 8+
  3. Testowanie - Dąż do 80%+ pokrycia kodu
  4. Bezpieczeństwo - Waliduj input, escapuj output
  5. Wydajność - Profiluj przed optymalizacją
  6. Dokumentacja - Używaj PHPDoc dla wszystkich metod publicznych
// AI: "Zaimplementuj wzorzec repository ze specyfikacjami"
interface UserRepositoryInterface
{
public function find(int $id): ?User;
public function findByEmail(string $email): ?User;
public function matching(Specification $specification): array;
public function save(User $user): void;
}
class DoctrineUserRepository implements UserRepositoryInterface
{
public function __construct(
private readonly EntityManagerInterface $em
) {}
public function matching(Specification $specification): array
{
$qb = $this->em->createQueryBuilder();
$qb->select('u')
->from(User::class, 'u');
$specification->apply($qb);
return $qb->getQuery()->getResult();
}
}
// Użycie ze specyfikacjami
$activeUsers = $repository->matching(
new AndSpecification(
new ActiveUserSpecification(),
new CreatedAfterSpecification(new \DateTime('-30 days'))
)
);
# Prompt AI: "Stwórz zoptymalizowaną konfigurację Docker dla aplikacji PHP"
# Multi-stage build
FROM php:8.2-fpm-alpine AS base
# Zainstaluj zależności
RUN apk add --no-cache \
postgresql-dev \
redis \
&& docker-php-ext-install pdo pdo_pgsql opcache
# Zainstaluj composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Etap development
FROM base AS development
RUN apk add --no-cache $PHPIZE_DEPS \
&& pecl install xdebug \
&& docker-php-ext-enable xdebug
# Etap production
FROM base AS production
# Zoptymalizuj PHP
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY docker/php/conf.d/opcache.ini $PHP_INI_DIR/conf.d/
# Skopiuj aplikację
WORKDIR /var/www
COPY --chown=www-data:www-data . .
# Zainstaluj zależności
RUN composer install --no-dev --optimize-autoloader
# Rozgrzewanie cache
RUN php bin/console cache:warmup --env=prod
EXPOSE 9000
CMD ["php-fpm"]