Job Summary
Clubee is a B2B SaaS platform for sports organizations, focusing on automation of various business processes including communication, marketing, finance, and CRM. The role requires a Senior PHP Symfony Developer to work on backend infrastructure, with emphasis on scalability, security, and best practices. The tech stack includes Symfony, PostgreSQL/MySQL, AWS, and GitHub Actions. The position demands both technical excellence and leadership capabilities.
How to Succeed
- Research Clubee's business model and existing solutions in sports management software
- Prepare concrete examples of scaling similar B2B SaaS applications
- Be ready to discuss technical decisions and trade-offs in your previous projects
- Prepare questions about their current architecture and technical challenges
- Review your Symfony projects and be ready to discuss architectural decisions
- Practice coding exercises focusing on SOLID principles and clean code
- Prepare examples of leading technical initiatives or mentoring other developers
Table of Contents
Symfony Core and Advanced Concepts 7 Questions
Essential for building and maintaining the core application infrastructure at Clubee, focusing on Symfony's service container, event system, and performance optimization.
The Service Container is a central feature of Symfony that manages service instantiation and dependency injection. It works by:
- Reading service definitions from configuration (YAML/XML/PHP)
- Managing service lifecycle (singleton/transient)
- Automatically injecting dependencies
- Lazy-loading services when needed
Key benefits:
- Reduces coupling between components
- Improves testability through dependency injection
- Optimizes performance via service compilation
- Enables automatic service registration and autowiring
For Clubee's B2B SaaS platform, this is crucial as it allows for:
- Easy service management across multiple modules (CRM, marketing, finance)
- Efficient dependency management in a large codebase
- Better testing isolation for critical business services
Tagged services allow you to group related services for collective processing. They're especially useful for plugin-like architectures.
Example relevant to Clubee's needs:
// services.yaml
services:
App\Notification\EmailNotifier:
tags: ['app.notifier']
App\Notification\SmsNotifier:
tags: ['app.notifier']
// NotificationManager.php
class NotificationManager
{
private array $notifiers;
public function __construct(
#[TaggedIterator('app.notifier')] iterable $notifiers
) {
$this->notifiers = iterator_to_array($notifiers);
}
}
This pattern would be valuable for Clubee's communication automation system, allowing easy extension of notification channels.
Event Listeners and Subscribers are key to Symfony's event-driven architecture.
Listener Example:
class UserActivityListener
{
#[AsEventListener(event: UserRegisteredEvent::class)]
public function onUserRegistered(UserRegisteredEvent $event): void
{
// Handle event
}
}
Subscriber Example:
class UserActivitySubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
UserRegisteredEvent::class => ['onUserRegistered', 10],
UserLoginEvent::class => ['onUserLogin', 20]
];
}
}
Key differences:
- Listeners handle single events
- Subscribers can handle multiple events
- Subscribers define their events statically
- Listeners are more flexible for dependency injection
For Clubee's sports organization automation, this would be crucial for handling various club events and automated responses.
For a B2B SaaS platform like Clubee, efficient caching is crucial. Here's a comprehensive approach:
- Multi-layer caching strategy:
class SportClubRepository
{
public function getClubData(int $clubId): array
{
return $this->cache->get("club.$clubId", function(ItemInterface $item) use ($clubId) {
$item->expiresAfter(3600);
return $this->fetchClubData($clubId);
});
}
}
- Implementation levels:
- HTTP caching (Varnish/CDN)
- Application caching (Redis/Memcached)
- Database query caching
- API response caching
- Cache invalidation strategy:
- Tags for related content
- Versioned cache keys
- Event-based cache clearing
This approach would be particularly effective for Clubee's high-traffic sports organization data.
Symfony Messenger is perfect for Clubee's automated communication and marketing needs:
#[AsMessageHandler]
class MarketingCampaignHandler
{
public function __invoke(SendCampaignMessage $message)
{
// Process campaign asynchronously
}
}
// Configure transport
// config/packages/messenger.yaml
framework:
messenger:
transports:
async: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
'App\Message\SendCampaignMessage': async
Key features:
- Multiple transport support (AMQP, Redis, Doctrine)
- Message retry and error handling
- Message routing and middleware
- Async/Sync processing flexibility
- Scalable message handling
This would be essential for Clubee's automated marketing and communication features.
For Clubee's financial and CRM operations, proper transaction handling is crucial:
class PaymentService
{
public function processPayment(Payment $payment): void
{
$this->entityManager->beginTransaction();
try {
// Process payment
// Update balance
$this->entityManager->flush();
$this->entityManager->commit();
} catch (\Exception $e) {
$this->entityManager->rollback();
throw $e;
}
}
}
Transaction Isolation Levels:
- READ UNCOMMITTED - Lowest isolation
- READ COMMITTED - Prevents dirty reads
- REPEATABLE READ - Prevents non-repeatable reads
- SERIALIZABLE - Highest isolation, prevents phantom reads
For Clubee's financial operations, REPEATABLE READ would be recommended to ensure data consistency.
For Clubee's B2B platform, security is paramount. Here's the security architecture:
- Authentication Flow:
# config/packages/security.yaml
security:
firewalls:
main:
lazy: true
provider: app_user_provider
custom_authenticator: App\Security\ApiKeyAuthenticator
json_login:
check_path: app_login
- Key Components:
- User Providers (for club manager data)
- Authenticators (API tokens, JWT)
- Access Decision Manager
- Voters for fine-grained permissions
- Role Hierarchy for organization structure
- Custom Implementation for Clubee:
#[IsGranted('ROLE_CLUB_MANAGER')]
public function manageClub(Club $club): Response
{
$this->denyAccessUnlessGranted('manage', $club);
// Club management logic
}
This setup would secure Clubee's multi-tenant architecture while maintaining flexibility for different organization types.
Database Optimization and Scaling 6 Questions
Critical for maintaining performance in a growing B2B SaaS platform, focusing on PostgreSQL/MySQL optimization and scaling strategies.
For a B2B SaaS platform like Clubee, I would implement the following optimization strategies:
- Use EXPLAIN ANALYZE to identify performance bottlenecks
- Optimize JOIN operations by ensuring proper indexing and join order
- Implement query caching using Redis for frequently accessed data
- Use materialized views for complex aggregations
- Implement database partitioning for large tables (especially useful for sports event data)
- Use LIMIT and OFFSET with keyset pagination for large result sets
- Optimize WHERE clauses to utilize indexes effectively
- Consider denormalization where appropriate for read-heavy operations
For PostgreSQL/MySQL (both mentioned in job requirements):
-
B-Tree indexes (default):
- Primary key columns
- Foreign key relationships
- Columns used in WHERE, ORDER BY, GROUP BY
-
Partial indexes:
- For filtered queries on specific conditions
- Example: indexing only active club members
-
Composite indexes:
- For queries involving multiple columns
- Order matters: most selective column first
-
GiST indexes:
- For geographical data (useful for sports club locations)
- Full-text search implementations
-
Hash indexes:
- Equality operations
- Membership checks
For a sports organization management system like Clubee:
-
Horizontal Sharding Strategies:
- By geographic region (EU focus mentioned in description)
- By organization/club ID
- By time period (season-based)
-
Implementation Approach:
- Use PostgreSQL native partitioning
- Implement consistent hashing for shard selection
- Maintain central metadata database
- Use database proxy (ProxySQL/PgPool) for routing
-
Consider:
- Cross-shard queries handling
- Data consistency across shards
- Backup and recovery procedures
Given the project uses GitHub Actions for deployment:
-
Multi-step Migration Process:
- Ensure backward compatibility
- Use temporary duplicate columns when restructuring
- Implement rolling updates
-
Migration Sequence:
- Deploy new code that works with both old/new schema
- Run additive migrations (new tables/columns)
- Gradually migrate data
- Remove deprecated schema elements
-
Safety Measures:
- Transaction wrapping
- Timeouts for long-running migrations
- Rollback plans
- Database replication lag monitoring
For Clubee's sports organization management:
-
PostgreSQL Full-Text Solutions:
- Use tsvector and tsquery data types
- Implement GiST/GIN indexes for search columns
- Configure proper text search configuration
-
Optimization Strategies:
- Create search vectors at write time
- Use materialized views for complex search patterns
- Implement trigram similarity for fuzzy matching
-
Implementation Example:
CREATE INDEX idx_fts_club_search ON clubs USING GIN (
to_tsvector('english',
coalesce(name,'') || ' ' ||
coalesce(description,'') || ' ' ||
coalesce(location,'')
)
);
For a scalable B2B SaaS platform:
-
AWS RDS Configuration:
- Set up Primary-Replica architecture
- Configure read replicas across availability zones
- Monitor replication lag
-
Symfony Implementation:
doctrine:
dbal:
connections:
default:
url: '%env(DATABASE_URL)%'
replica:
url: '%env(DATABASE_REPLICA_URL)%'
- Load Balancing:
- Write operations to primary
- Read operations to replicas
- Use ProxySQL for intelligent routing
- Implement connection pooling
SOLID and Clean Code Practices 6 Questions
Fundamental for maintaining high code quality and scalability in a growing codebase, ensuring maintainability and extensibility.
Here's an example of SRP in the context of Clubee's sports organization automation:
// Bad example - multiple responsibilities
class TeamManager
{
public function createTeam(array $data): Team
{
// Team creation logic
}
public function sendWelcomeEmail(Team $team): void
{
// Email sending logic
}
public function generateTeamStats(Team $team): array
{
// Statistics calculation
}
}
// Good example - separated responsibilities
class TeamCreationService
{
public function __construct(
private readonly EmailService $emailService,
private readonly TeamStatsCalculator $statsCalculator
) {}
public function createTeam(array $data): Team
{
$team = new Team($data);
$this->emailService->sendWelcomeEmail($team);
return $team;
}
}
class TeamStatsCalculator
{
public function generateStats(Team $team): array
{
// Statistics calculation logic
}
}
This separation allows for better testing and maintenance, especially important in a B2B SaaS platform where features might need to scale independently.
For Clubee's sports organization system, here's an implementation of ISP:
// Instead of one large interface
interface TeamRepositoryInterface
{
public function findById(int $id): ?Team;
public function save(Team $team): void;
public function delete(Team $team): void;
public function findByLeague(int $leagueId): array;
public function generateStatistics(Team $team): array;
public function exportTeamData(Team $team): string;
}
// Better approach with segregated interfaces
interface TeamReaderInterface
{
public function findById(int $id): ?Team;
public function findByLeague(int $leagueId): array;
}
interface TeamWriterInterface
{
public function save(Team $team): void;
public function delete(Team $team): void;
}
interface TeamStatsInterface
{
public function generateStatistics(Team $team): array;
}
class TeamRepository implements TeamReaderInterface, TeamWriterInterface
{
// Implementation
}
class TeamStatisticsService implements TeamStatsInterface
{
// Implementation
}
This approach allows clients to depend only on the interfaces they actually need, making the system more flexible and maintainable.
Here's a practical example relevant to the sports organization automation system:
// Low-level module
class PostgreSQLNotificationStorage implements NotificationStorageInterface
{
public function save(Notification $notification): void
{
// PostgreSQL-specific implementation
}
}
// High-level module
class NotificationService
{
public function __construct(
private readonly NotificationStorageInterface $storage,
private readonly NotificationSenderInterface $sender
) {}
public function processTeamNotification(Team $team, string $message): void
{
$notification = new Notification($team, $message);
$this->storage->save($notification);
$this->sender->send($notification);
}
}
// The interface that both depend on
interface NotificationStorageInterface
{
public function save(Notification $notification): void;
}
This implementation allows for easy switching between different storage implementations (PostgreSQL/MySQL) as mentioned in the job requirements, while maintaining loose coupling.
Here's an example tailored to Clubee's automation needs:
// Base class for team communication strategy
abstract class TeamCommunicationStrategy
{
abstract public function send(Team $team, string $message): void;
}
// Different implementations
class EmailCommunication extends TeamCommunicationStrategy
{
public function send(Team $team, string $message): void
{
// Email implementation
}
}
class SMSCommunication extends TeamCommunicationStrategy
{
public function send(Team $team, string $message): void
{
// SMS implementation
}
}
// Service using the strategy
class TeamCommunicationService
{
public function __construct(
private readonly TeamCommunicationStrategy $strategy
) {}
public function notifyTeam(Team $team, string $message): void
{
$this->strategy->send($team, $message);
}
}
This design allows adding new communication methods without modifying existing code, essential for a growing B2B SaaS platform.
Here's an example in the context of sports organization management:
abstract class Member
{
abstract public function calculateFees(): float;
abstract public function getAccessRights(): array;
}
class RegularMember extends Member
{
public function calculateFees(): float
{
return 100.0; // Base fee
}
public function getAccessRights(): array
{
return ['view_schedule', 'book_training'];
}
}
class PremiumMember extends Member
{
public function calculateFees(): float
{
return 200.0; // Premium fee
}
public function getAccessRights(): array
{
return ['view_schedule', 'book_training', 'access_premium_content'];
}
}
// Service that works with any Member type
class MembershipService
{
public function processMembership(Member $member): void
{
$fees = $member->calculateFees();
$rights = $member->getAccessRights();
// Process membership logic
}
}
This example shows how different member types can be used interchangeably while maintaining expected behavior.
For Clubee's codebase, I would implement the following code review checklist:
- Single Responsibility Check:
- Verify each class has one reason to change
- Look for methods that could be extracted into separate services
- Ensure services align with specific business capabilities
- Open/Closed Assessment:
- Check for strategy patterns where behavior varies
- Ensure extension points exist for likely future changes
- Look for abstract classes and interfaces where appropriate
- Liskov Substitution Verification:
- Verify that child classes don't violate parent contracts
- Check that overridden methods maintain expected behavior
- Ensure type hints are properly used
- Interface Segregation Review:
- Look for large interfaces that could be split
- Verify client code only depends on methods it uses
- Check for interface cohesion
- Dependency Inversion Validation:
- Ensure high-level modules don't depend on implementation details
- Verify proper use of Symfony's service container
- Check for constructor injection of dependencies
Additional points specific to the role:
- Verify AWS service integrations are properly abstracted
- Check that database operations are encapsulated in repositories
- Ensure security best practices are followed
I would use automated tools like PHP Insights and PHPStan to assist in this process.
AWS and Infrastructure Management 6 Questions
Essential for understanding and managing cloud infrastructure, deployment processes, and scaling strategies.
For a B2B SaaS platform like Clubee, I would implement:
- Multi-AZ deployment with AWS ECS/EKS for container orchestration
- Application Load Balancer for traffic distribution
- Auto-scaling groups across multiple availability zones
- RDS in Multi-AZ configuration for database redundancy
- ElastiCache for session management and caching
- S3 for static assets with CloudFront distribution
- Route53 for DNS management with health checks
- Separate environments for staging and production This ensures 99.99% uptime and seamless failover capabilities.
For a sports organization platform that might experience peak loads during events:
- Use AWS Target Tracking Scaling policies based on:
- CPU utilization (target 70%)
- Memory utilization
- Request count per target
- Implement predictive scaling for known high-traffic periods
- Set up scaling cool-down periods to prevent thrashing
- Use Application Auto Scaling for RDS and ElastiCache
- Implement proper monitoring with CloudWatch metrics
- Configure scaling boundaries (min/max instances) based on business requirements
For secure secrets management in a Symfony application:
- Use AWS Secrets Manager for storing sensitive credentials
- Implement AWS Parameter Store for configuration values
- Use IAM roles and policies for access control
- In Symfony:
- Use environment variables in .env files
- Implement AWS SDK for secrets retrieval
- Use Symfony's secrets management system
- For CI/CD (mentioned Github Actions):
- Store deployment credentials in Github Secrets
- Use AWS AssumeRole for secure deployments
- Rotate secrets automatically using AWS Secrets Manager
For a B2B SaaS platform using PostgreSQL/MySQL:
- Automated backups:
- Daily automated snapshots with 30-day retention
- Transaction logs for point-in-time recovery
- Multi-AZ deployment for high availability
- Read replicas for scaling read operations
- Backup strategies:
- Automated: Using RDS automated backups
- Manual: Pre-deployment snapshots
- Cross-region: For disaster recovery
- Monitoring:
- CloudWatch metrics for backup success/failure
- SNS notifications for backup events
- Regular backup restoration testing
For Clubee's sports platform:
- CloudFront distribution setup:
- S3 origin for static assets
- Custom origin for dynamic content
- Configure:
- Cache behaviors based on content type
- TTL settings for different resources
- HTTPS enforcement
- Implement:
- Cache invalidation through Github Actions
- Custom error pages
- Geographic restrictions if needed
- Optimize:
- Enable compression
- Configure origin shield
- Use edge locations closest to user base
Comprehensive monitoring strategy:
- CloudWatch:
- Custom metrics for business KPIs
- Alarms for critical thresholds
- Dashboard for key metrics
- AWS X-Ray:
- Trace requests across services
- Performance bottleneck identification
- Logging:
- Centralized logging with CloudWatch Logs
- Log retention policies
- Log insights for analysis
- Monitoring specific to sports platform:
- User activity peaks during events
- Database performance metrics
- API endpoint response times
- Integration with Symfony's Monolog
Testing and Quality Assurance 6 Questions
Critical for maintaining code quality and preventing regressions in a fast-paced development environment.
In a Symfony project, I implement the testing pyramid following a bottom-up approach:
- Unit Tests (Base - 70%):
- Testing individual services, particularly those handling business logic
- Using PHPUnit for testing isolated components
- Focus on SOLID principles compliance validation
- Integration Tests (Middle - 20%):
- Testing service interactions
- Database operations testing using actual PostgreSQL/MySQL
- Testing Symfony's service container and dependency injection
- End-to-End Tests (Top - 10%):
- API endpoint testing using PHPUnit's WebTestCase
- Full feature testing including AWS service integrations
- User flow testing
For Clubee's B2B SaaS platform, I would particularly focus on testing the automated communication and marketing features, ensuring they scale properly.
My strategy for complex service classes involves:
- Test Setup:
private SportManagerService $service;
private EntityManagerInterface $em;
protected function setUp(): void
{
$this->em = $this->createMock(EntityManagerInterface::class);
$this->service = new SportManagerService(
$this->em,
$this->createMock(LoggerInterface::class)
);
}
- Test Organization:
- Arrange: Set up test data and mocks
- Act: Execute the method being tested
- Assert: Verify the results
- Key Practices:
- Testing each public method independently
- Using data providers for multiple test cases
- Mocking external dependencies (especially AWS services)
- Testing edge cases and error conditions
- Focus on testing business logic that's critical for sports organization automation, as per Clubee's core business.
For API endpoint testing in Symfony, I implement:
class ClubManagementControllerTest extends WebTestCase
{
public function testCreateClub(): void
{
$client = static::createClient();
$client->request('POST', '/api/clubs', [], [],
['CONTENT_TYPE' => 'application/json'],
json_encode(['name' => 'Test Club', 'type' => 'sports'])
);
$this->assertResponseIsSuccessful();
$this->assertJsonContains(['status' => 'success']);
}
}
Key aspects:
- Using WebTestCase for full stack testing
- Testing with real database (using test database)
- Testing authentication/authorization
- Verifying response formats and status codes
- Testing rate limiting and error handling
- Implementing data fixtures for consistent test data
For complex dependencies, especially in Clubee's context with AWS services:
- Using PHPUnit's MockBuilder for complex mocks:
$awsClient = $this->getMockBuilder(AwsClient::class)
->disableOriginalConstructor()
->addMethods(['sendNotification'])
->getMock();
- Implementing test doubles:
- Stubs for predetermined responses
- Mocks for behavior verification
- Spies for analyzing interactions
- Creating custom mock traits:
trait AwsServiceMockTrait
{
protected function mockAwsService(): void
{
$this->awsService = $this->createMock(AwsServiceInterface::class);
$this->awsService->method('processRequest')
->willReturn(['status' => 'success']);
}
}
- Using Prophecy when needed for more complex behavior scenarios
To maintain both coverage and quality:
- Coverage Strategy:
- Aim for 90%+ coverage of business logic
- Use PHPUnit's coverage reports
- Configure CI/CD (GitHub Actions) to fail if coverage drops
- Quality Measures:
- Follow Arrange-Act-Assert pattern
- Implement mutation testing using Infection
- Regular test suite maintenance
- Code review with focus on test quality
- Best Practices:
public function testClubRegistration()
{
// Arrange
$clubData = new ClubDTO(...);
// Act
$result = $this->service->registerClub($clubData);
// Assert
$this->assertInstanceOf(Club::class, $result);
$this->assertTrue($result->isActive());
}
- Focus on critical paths in sports organization automation features
My BDD experience in Symfony includes:
- Tool Stack:
- Behat for feature definitions
- PHPSpec for specification testing
- Symfony's WebTestCase for web scenarios
- Implementation Example:
Feature: Club Management
Scenario: Creating a new sports club
Given I am an authenticated administrator
When I submit valid club details
Then the club should be created
And an welcome email should be sent
- Integration with Symfony:
- Custom Context classes
- Service container awareness
- Database integration
- AWS service mocking
- Benefits for Clubee:
- Clear requirements documentation
- Improved stakeholder communication
- Better alignment with business goals
- Facilitates automated testing of complex workflows
API Design and Integration 6 Questions
Crucial for building scalable and maintainable APIs that support the B2B SaaS platform's functionality.
For Clubee's B2B SaaS platform, I would implement API versioning using these approaches:
- URI Versioning:
#[Route('/api/v1/clubs', name: 'api_v1_clubs')]
#[Route('/api/v2/clubs', name: 'api_v2_clubs')]
- Custom Request Listener for header-based versioning:
class ApiVersionListener
{
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
$version = $request->headers->get('Accept-Version', '1.0');
$request->attributes->set('version', $version);
}
}
- Version-specific service classes using Symfony's service container:
services:
app.api.v1.club_service:
class: App\Service\V1\ClubService
app.api.v2.club_service:
class: App\Service\V2\ClubService
This ensures backward compatibility while allowing new feature development.
For Clubee's scalable platform, I would implement:
- Redis-based rate limiting using Symfony's Rate Limiter:
#[Route('/api/clubs')]
#[RateLimit(limit: 100, period: '15 minutes')]
public function listClubs(): JsonResponse
{
// Implementation
}
- Custom rate limiter configuration:
framework:
rate_limiter:
api_limit:
policy: 'sliding_window'
limit: 100
interval: '15 minutes'
- AWS CloudFront for additional layer of protection:
services:
app.rate_limiter.storage:
class: App\RateLimit\RedisStorage
arguments: ['@redis.client']
This ensures fair API usage while maintaining performance for all clients.
For Clubee's B2B platform, I would implement:
- OpenAPI/Swagger documentation using NelmioApiDocBundle:
#[Route('/api/clubs', methods: ['POST'])]
#[OA\Response(response: 201, description: 'Club created successfully')]
#[OA\RequestBody(content: new Model(type: CreateClubRequest::class))]
public function createClub(Request $request): JsonResponse
{
// Implementation
}
- Automated documentation generation in CI/CD pipeline:
# Github Actions
jobs:
build:
steps:
- name: Generate API docs
run: php bin/console api:doc:dump --format=json --output=public/api-docs.json
- API versioning reflection in documentation:
#[OA\Info(version: "1.0.0", title: "Clubee API Documentation")]
class ApiDocController
This ensures up-to-date documentation that evolves with the API.
For Clubee's sports organization management system:
- Implement bulk endpoints:
#[Route('/api/clubs/batch', methods: ['POST'])]
public function batchCreateClubs(Request $request): JsonResponse
{
$clubs = $this->serializer->deserialize($request->getContent(), 'Club[]', 'json');
$this->entityManager->transactional(function() use ($clubs) {
foreach ($clubs as $club) {
$this->entityManager->persist($club);
}
});
}
- Use Doctrine's batch processing:
public function batchUpdateMembers(array $members): void
{
foreach ($members as $i => $member) {
$this->entityManager->persist($member);
if (($i % 100) === 0) {
$this->entityManager->flush();
$this->entityManager->clear();
}
}
}
- Implement async processing for large batches using Symfony Messenger:
#[AsMessageHandler]
class BatchProcessingHandler
{
public function __invoke(BatchOperation $message)
{
// Process batch asynchronously
}
}
For Clubee's robust error handling:
- Custom Exception Subscriber:
class ApiExceptionSubscriber implements EventSubscriberInterface
{
public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
$response = new JsonResponse([
'error' => [
'message' => $exception->getMessage(),
'code' => $exception->getCode()
]
]);
$event->setResponse($response);
}
}
- Custom API Exceptions:
class ClubNotFoundException extends \Exception implements ApiException
{
public function __construct(int $clubId)
{
parent::__construct("Club with ID {$clubId} not found", 404);
}
}
- Validation error handling:
#[Route('/api/clubs', methods: ['POST'])]
public function createClub(ClubRequest $request): JsonResponse
{
$violations = $this->validator->validate($request);
if (count($violations) > 0) {
throw new ValidationException($violations);
}
}
This ensures consistent error handling across the application while providing meaningful feedback to API consumers.