Przejdź do głównej zawartości

PHP Development Patterns

Ta treść nie jest jeszcze dostępna w Twoim języku.

Accelerate PHP development with Cursor IDE and Claude Code. This guide covers modern PHP practices, Laravel and Symfony frameworks, legacy code modernization, testing with PHPUnit, and building scalable enterprise applications with AI assistance.

  1. Initialize PHP Project

    Terminal window
    # Laravel project
    Ask: "Create a new Laravel project with authentication, API resources, and job queues"
    # Or use Agent mode
    Agent: "Set up Laravel API with Sanctum auth, repository pattern, and Docker"
  2. Configure Development Environment

    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. Set Up AI Rules

    Terminal window
    # .cursorrules or CLAUDE.md
    - Use PHP 8.2+ features (readonly properties, enums, etc.)
    - Follow PSR-12 coding standards
    - Use type declarations everywhere
    - Implement repository pattern for data access
    - Write tests for all public methods
    - Use dependency injection
    - Follow SOLID principles
// AI Prompt
Agent: "Create a complete REST API for Product management with:
- API Resources for response formatting
- Form Requests for validation
- Repository pattern
- Service layer
- API versioning
- Rate limiting"
// Generated code structure:
// 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
// AI Prompt: "Create advanced Eloquent models with relationships and scopes"
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();
});
}
// Relationships
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function items(): HasMany
{
return $this->hasMany(OrderItem::class);
}
// Scopes
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);
}
// Accessors & Mutators
public function getTotalFormattedAttribute(): string
{
return number_format($this->total_amount, 2);
}
// Business Logic
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);
}
}
// AI Prompt
Ask: "Create a job processing system with:
- Retry logic
- Rate limiting
- Failed job handling
- Job chaining
- Progress tracking"
// 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);
// Chain next job
ProcessOrderNotificationJob::dispatch($this->order)
->delay(now()->addMinutes(5));
}
public function failed(\Throwable $exception): void
{
// Log failure
\Log::error('Order processing failed', [
'order_id' => $this->order->id,
'error' => $exception->getMessage(),
]);
// Notify admin
AdminNotification::send(
'Order processing failed: ' . $this->order->order_number
);
}
public function retryUntil(): \DateTime
{
return now()->addHours(2);
}
}
src/Service/PaymentService.php
// AI Prompt: "Create Symfony service with dependency injection and configuration"
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('Processing payment', [
'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('Payment failed', [
'payment_id' => $payment->getId(),
'error' => $e->getMessage(),
]);
$payment->setStatus(PaymentStatus::Failed);
$this->repository->save($payment, true);
throw new PaymentException('Payment processing failed', 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
// AI Prompt
Agent: "Create Doctrine entities with:
- Complex relationships
- Lifecycle callbacks
- Custom repositories
- Query optimization
- Event listeners"
// 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();
}
// Getters and setters...
}
// 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();
}
}
// AI Prompt: "Implement modern PHP with strict types, enums, and readonly properties"
declare(strict_types=1);
namespace App\Domain\Order;
// PHP 8.1 Enums
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 => 'Awaiting Processing',
self::Processing => 'In Progress',
self::Completed => 'Completed',
self::Cancelled => 'Cancelled',
self::Refunded => 'Refunded',
};
}
}
// PHP 8.2 Readonly classes
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('Order must have at least one item');
}
}
}
// Value Object
final readonly class Money
{
public function __construct(
public int $amount,
public string $currency = 'USD'
) {
if ($amount < 0) {
throw new \InvalidArgumentException('Amount cannot be negative');
}
if (!in_array($currency, ['USD', 'EUR', 'GBP'])) {
throw new \InvalidArgumentException('Invalid currency');
}
}
public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException('Cannot add different currencies');
}
return new self(
$this->amount + $other->amount,
$this->currency
);
}
public function format(): string
{
return number_format($this->amount / 100, 2) . ' ' . $this->currency;
}
}
// AI Prompt
Agent: "Modernize this legacy PHP code:
- Convert to PSR-4 autoloading
- Add type declarations
- Replace mysql_* with PDO
- Implement proper error handling
- Add unit tests"
// Before: legacy.php
function get_user($id) {
$result = mysql_query("SELECT * FROM users WHERE id = $id");
return mysql_fetch_assoc($result);
}
// After: 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(
"Failed to fetch user with id {$id}",
0,
$e
);
}
}
}
// AI Prompt: "Create PHPUnit tests with mocks, data providers, and integration tests"
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 [
'already completed' => [OrderStatus::Completed],
'cancelled' => [OrderStatus::Cancelled],
'refunded' => [OrderStatus::Refunded],
];
}
/**
* @test
* @group integration
*/
public function it_handles_concurrent_order_completion(): void
{
// This would be an actual integration test
// with database transactions
$this->markTestIncomplete('Integration test to be implemented');
}
private function createOrder(OrderStatus $status): Order
{
$order = new Order();
$order->setId(123);
$order->setStatus($status);
$order->setTotalAmount(99.99);
return $order;
}
}
// AI Prompt
Ask: "Implement caching strategy with:
- Redis for object caching
- Query result caching
- Cache invalidation
- Cache warming
- Performance monitoring"
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 hour
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 for product', ['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);
// Invalidate related caches
$product = $this->repository->find($id);
if ($product) {
$this->cache->invalidateTags([
'category:' . $product->getCategory()->getId()
]);
}
}
public function warmCache(): void
{
$this->logger->info('Starting cache warming');
// Warm popular products
$popularProducts = $this->repository->findPopular(limit: 100);
foreach ($popularProducts as $product) {
$this->findById($product->getId());
}
$this->logger->info('Cache warming completed', [
'products_cached' => count($popularProducts)
]);
}
}

PHP Development Guidelines

  1. Type Safety - Use strict types and type declarations
  2. Modern PHP - Leverage PHP 8+ features
  3. Testing - Aim for 80%+ code coverage
  4. Security - Validate input, escape output
  5. Performance - Profile before optimizing
  6. Documentation - Use PHPDoc for all public methods
// AI: "Implement repository pattern with specifications"
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();
}
}
// Usage with specifications
$activeUsers = $repository->matching(
new AndSpecification(
new ActiveUserSpecification(),
new CreatedAfterSpecification(new \DateTime('-30 days'))
)
);
# AI Prompt: "Create optimized Docker setup for PHP application"
# Multi-stage build
FROM php:8.2-fpm-alpine AS base
# Install dependencies
RUN apk add --no-cache \
postgresql-dev \
redis \
&& docker-php-ext-install pdo pdo_pgsql opcache
# Install composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Development stage
FROM base AS development
RUN apk add --no-cache $PHPIZE_DEPS \
&& pecl install xdebug \
&& docker-php-ext-enable xdebug
# Production stage
FROM base AS production
# Optimize 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/
# Copy application
WORKDIR /var/www
COPY --chown=www-data:www-data . .
# Install dependencies
RUN composer install --no-dev --optimize-autoloader
# Cache warmup
RUN php bin/console cache:warmup --env=prod
EXPOSE 9000
CMD ["php-fpm"]