PHPackages                             team-mate-pro/use-case-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. team-mate-pro/use-case-bundle

ActiveSymfony-bundle[Framework](/categories/framework)

team-mate-pro/use-case-bundle
=============================

A building block for layered architecture with Symfony framework.

3.0.0(1mo ago)04.2k↓62.6%MITPHPPHP &gt;=8.3

Since Nov 7Pushed 1mo agoCompare

[ Source](https://github.com/team-mate-pro/use-case-bundle)[ Packagist](https://packagist.org/packages/team-mate-pro/use-case-bundle)[ RSS](/packages/team-mate-pro-use-case-bundle/feed)WikiDiscussions master Synced 2d ago

READMEChangelogDependencies (69)Versions (17)Used By (0)

Super Simple Architecture (SSA) Core Bundle
===========================================

[](#super-simple-architecture-ssa-core-bundle)

A Symfony PHP bundle providing architectural building blocks for clean REST API development. SSA enforces a use-case driven architecture with validated requests, standardized result objects, and consistent HTTP responses.

**Documentation**:

Overview
--------

[](#overview)

The Super Simple Architecture approach focuses on:

- **Use-case driven design**: Business logic encapsulated in dedicated use case classes with `__invoke()` method
- **Interface-based DTOs**: Use cases accept interfaces, not concrete request classes, for loose coupling
- **Validated requests**: Automatic request validation with Symfony constraints and authorization via `securityCheck()`
- **Standardized results**: Type-safe Result objects with consistent error handling and HTTP status mapping
- **REST API patterns**: Controllers that transform use case results into proper HTTP responses using `$this->response()`
- **Partial updates**: PATCH request support with `Undefined` sentinel values and conditional validation

Installation
------------

[](#installation)

```
composer require team-mate-pro/use-case-bundle
```

Quick Start
-----------

[](#quick-start)

### 1. Define a DTO Interface

[](#1-define-a-dto-interface)

Use cases should depend on interfaces, not concrete request classes. This enables loose coupling and testability.

```
interface CreateUserDtoInterface
{
    public function getEmail(): string;
    public function getName(): string;
}
```

### 2. Create a Validated Request

[](#2-create-a-validated-request)

```
use TeamMatePro\UseCaseBundle\Http\AbstractValidatedRequest;
use Symfony\Component\Validator\Constraints as Assert;

final class CreateUserRequest extends AbstractValidatedRequest implements CreateUserDtoInterface
{
    #[Assert\NotBlank]
    #[Assert\Email]
    public string $email;

    #[Assert\NotBlank]
    #[Assert\Length(min: 3)]
    public string $name;

    public function getEmail(): string
    {
        return $this->getValue('email');
    }

    public function getName(): string
    {
        return $this->getValue('name');
    }

    protected function securityCheck(): bool
    {
        return $this->isGranted('ROLE_ADMIN');
    }
}
```

Request objects automatically:

- Populate from JSON body, query params, route attributes, and multipart form data
- Validate using Symfony validator constraints
- Inject authenticated user ID if `userId` property exists
- Handle file uploads via 'file' or 'files' keys
- Throw `AccessDeniedException` if `securityCheck()` returns false

### 3. Create a Use Case

[](#3-create-a-use-case)

Use cases contain pure business logic. They accept DTO interfaces (not concrete requests) and return Result objects.

```
use TeamMatePro\Contracts\Collection\Result;
use TeamMatePro\Contracts\Collection\ResultType;

final readonly class CreateUserUseCase
{
    public function __construct(
        private UserRepository $repository,
        private UserFactory $factory
    ) {}

    public function __invoke(CreateUserDtoInterface $dto): Result
    {
        if ($this->repository->existsByEmail($dto->getEmail())) {
            return Result::create(ResultType::DUPLICATED, 'User already exists')
                ->withErrorCode('USER_EXISTS');
        }

        $user = $this->factory->create(
            email: $dto->getEmail(),
            name: $dto->getName()
        );

        $this->repository->save($user);

        return Result::create(ResultType::SUCCESS_CREATED)
            ->with($user);
    }
}
```

**Important**: Use cases must NOT contain authorization logic. Authorization belongs in the Request's `securityCheck()` method.

### 4. Create a REST Controller

[](#4-create-a-rest-controller)

Controllers use the `Action` suffix convention and delegate to use cases via `$this->response()`.

```
use TeamMatePro\UseCaseBundle\Http\AbstractRestApiController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

final class UserController extends AbstractRestApiController
{
    #[Route('/api/users', methods: ['POST'])]
    public function createUserAction(
        CreateUserRequest $request,
        CreateUserUseCase $useCase
    ): JsonResponse {
        return $this->response($useCase($request), ['user:read']);
    }

    #[Route('/api/users/{userId}', methods: ['GET'])]
    public function getUserAction(
        GetUserRequest $request,
        GetUserUseCase $useCase
    ): JsonResponse {
        return $this->response($useCase($request), ['user:read', 'user:details']);
    }

    #[Route('/api/users/{userId}', methods: ['PATCH'])]
    public function updateUserAction(
        UpdateUserRequest $request,
        UpdateUserUseCase $useCase
    ): JsonResponse {
        return $this->response($useCase($request), ['user:read']);
    }

    #[Route('/api/users/{userId}', methods: ['DELETE'])]
    public function deleteUserAction(
        DeleteUserRequest $request,
        DeleteUserUseCase $useCase
    ): JsonResponse {
        return $this->response($useCase($request));
    }
}
```

Controllers automatically:

- Map ResultType to appropriate HTTP status codes
- Serialize response data with specified serialization groups
- Support caching headers with `responseWithCache()`

Core Components
---------------

[](#core-components)

### Result Object

[](#result-object)

The `Result` object is the heart of SSA, providing a standardized container for use case outputs:

```
use TeamMatePro\Contracts\Collection\Result;
use TeamMatePro\Contracts\Collection\ResultType;

// Success with data
Result::create(ResultType::SUCCESS)->with($user);

// Created resource
Result::create(ResultType::SUCCESS_CREATED)->with($team);

// Failure with error code
Result::create(ResultType::DUPLICATED, 'Email already exists')
    ->withErrorCode('EMAIL_TAKEN');

// Not found
Result::create(ResultType::NOT_FOUND, 'User not found');

// No content (for DELETE operations)
Result::create(ResultType::SUCCESS_NO_CONTENT);

// Accepted (async operation)
Result::create(ResultType::ACCEPTED);

// Collection with metadata
Result::create()->with($users)
    ->withMeta('total', 100)
    ->withMeta('page', 1);
```

**ResultType enum** maps to HTTP status codes:

ResultTypeHTTP StatusUsage`SUCCESS`200 OKSuccessful GET, PATCH operations`SUCCESS_CREATED`201 CreatedSuccessful POST creating a resource`ACCEPTED`202 AcceptedAsync operations, background jobs`SUCCESS_NO_CONTENT`204 No ContentSuccessful DELETE operations`FAILURE`400 Bad RequestBusiness rule violations`UNAUTHORIZED`401 UnauthorizedAuthentication required`FORBIDDEN`403 ForbiddenAuthenticated but not authorized`NOT_FOUND`404 Not FoundResource doesn't exist`DUPLICATED`409 ConflictResource already exists`GONE`410 GoneResource was deleted`EXPIRED`410 GoneResource has expired`PRECONDITION_FAILED`412 Precondition FailedETag mismatch, version conflict`UNPROCESSABLE`422 Unprocessable EntitySemantic validation errors`LOCKED`423 LockedResource locked (e.g., foreign key constraint)`TOO_MANY_REQUESTS`429 Too Many RequestsRate limiting`SERVICE_UNAVAILABLE`503 Service UnavailableTemporary unavailability### Validated Requests

[](#validated-requests)

#### Auto-Population

[](#auto-population)

Request data is merged from multiple sources in this order:

1. Route attributes (from URL path)
2. JSON body
3. Query parameters
4. POST form data (multipart/form-data)
5. File uploads (via 'file' or 'files' keys)

#### getValue() Helper with Type Casting

[](#getvalue-helper-with-type-casting)

The `getValue()` method provides validation and automatic type casting:

```
class UpdatePlayerRequest extends AbstractValidatedRequest
{
    public string|int|null $age;
    public string|bool|null $active;

    // Automatically casts int to string
    public function getAge(): string
    {
        return $this->getValue('age'); // "25" even if $age = 25
    }

    // Automatically casts string to bool
    public function isActive(): bool
    {
        return $this->getValue('active'); // true if $active = "true"
    }
}
```

**Casting Rules:**

- **To string**: int, float, bool (true→"1", false→"0")
- **To int**: numeric string, float (truncates), bool (true→1, false→0)
- **To float**: numeric string, int
- **To bool**: string ("1","true","yes","on"→true), int (0→false, other→true)

#### Populate Strategies

[](#populate-strategies)

```
// Default: Direct property assignment
class MyRequest extends AbstractValidatedRequest
{
    protected function getPopulateStrategy(): string
    {
        return self::PROPERTY_SET_STRATEGY; // default
    }
}

// Serializer: For complex denormalization
class ComplexRequest extends AbstractValidatedRequest
{
    protected function getPopulateStrategy(): string
    {
        return self::SERIALIZER_STRATEGY;
    }
}
```

### PATCH Requests with Undefined Pattern

[](#patch-requests-with-undefined-pattern)

For partial updates, use the `Undefined` sentinel value and `PatchValidation` constraint:

```
use TeamMatePro\Contracts\Dto\Undefined;
use TeamMatePro\UseCaseBundle\Validator\PatchValidation;
use Symfony\Component\Validator\Constraints as Assert;

final class UpdateUserRequest extends AbstractValidatedRequest implements UpdateUserDtoInterface
{
    #[PatchValidation([
        new Assert\NotBlank(),
        new Assert\Email(),
    ])]
    public string|Undefined $email = new Undefined();

    #[PatchValidation([
        new Assert\Length(min: 2, max: 100),
    ])]
    public string|Undefined $name = new Undefined();

    public function getEmail(): string|Undefined
    {
        return $this->getValue('email');
    }

    public function getName(): string|Undefined
    {
        return $this->getValue('name');
    }
}
```

The `PatchValidation` constraint:

- Only validates properties that were explicitly provided in the request
- Skips validation for properties that remain `Undefined`
- Allows you to have required validation on fields that are optional to send

### PartialUpdateService

[](#partialupdateservice)

Map values from DTOs to entities, automatically skipping `Undefined` values:

```
use TeamMatePro\UseCaseBundle\Utils\PartialUpdateService;

final readonly class UpdateUserUseCase
{
    public function __construct(
        private UserRepository $repository,
        private PartialUpdateService $partialUpdate
    ) {}

    public function __invoke(UpdateUserDtoInterface $dto): Result
    {
        $user = $this->repository->getOne($dto->getUserId());

        // Only updates properties that aren't Undefined
        $this->partialUpdate->map($dto, $user);

        $this->repository->save($user);

        return Result::create()->with($user);
    }
}
```

The `PartialUpdateService`:

- Maps getters from source (`getEmail()`) to setters on target (`setEmail()`) or public properties
- Automatically skips values that are instances of `Undefined`
- Supports a `$strict` mode that throws exceptions for unmapped properties
- Supports a `$skips` array to exclude specific properties

### Repository Collections

[](#repository-collections)

```
use TeamMatePro\Contracts\Collection\Pagination;
use TeamMatePro\Contracts\Collection\PaginatedCollection;

// Create pagination
$pagination = new Pagination(page: 1, limit: 20);

// Return paginated collection
$items = $this->repository->findAll($pagination);
$collection = new PaginatedCollection(
    items: $items,
    count: $this->repository->count(),
    pagination: $pagination
);

// Use in Result
return Result::create()->with($collection);
```

For requests with pagination support, use the `PaginationTrait`:

```
use TeamMatePro\UseCaseBundle\Http\PaginationTrait;

final class FindUsersRequest extends AbstractValidatedRequest implements FindUsersDtoInterface
{
    use PaginationTrait;

    // Provides: $page, $perPage properties and getPagination() method
}
```

### Content Negotiation

[](#content-negotiation)

Check Accept headers to determine response format:

```
use TeamMatePro\UseCaseBundle\Http\ContentType\ContentTypeChecker;

final class ExportController extends AbstractRestApiController
{
    #[Route('/api/users', methods: ['GET'])]
    public function findUsersAction(
        FindUsersRequest $request,
        FindUsersUseCase $useCase,
        ContentTypeChecker $contentTypeChecker,
        CsvResponseFactory $csvFactory
    ): Response {
        $result = $useCase($request);

        if ($contentTypeChecker->isCsvRequest($request)) {
            return $csvFactory->createCsvResponse($result, ['user:export']);
        }

        if ($contentTypeChecker->isPdfRequest($request)) {
            return $this->createPdfResponse($result);
        }

        return $this->response($result, ['user:read']);
    }
}
```

The `ContentTypeChecker` supports:

- **CSV detection**: `text/csv`, `application/csv`, `text/comma-separated-values`
- **PDF detection**: `application/pdf`
- Case-insensitive matching

### Response Factories

[](#response-factories)

Generate blob responses from Result objects:

```
use TeamMatePro\UseCaseBundle\Http\ResultResponseFactory;

// CSV response
$response = ResultResponseFactory::createCsvResponse(
    result: $result,
    filename: 'users.csv',
    base64Encode: false
);

// Binary blob response
$response = ResultResponseFactory::createBlobResponse(
    result: $result,
    contentType: 'application/pdf',
    filename: 'report.pdf'
);
```

Architecture Standards
----------------------

[](#architecture-standards)

This bundle is designed to work with the TMP Standards (UCB rules). Key principles:

### UCB-001: UseCase Parameters Must Be Interfaces

[](#ucb-001-usecase-parameters-must-be-interfaces)

```
// Correct: UseCase accepts interface
public function __invoke(CreateUserDtoInterface $dto): Result

// Wrong: UseCase accepts concrete class
public function __invoke(CreateUserRequest $request): Result
```

### UCB-002: UseCase Must Have \_\_invoke Method

[](#ucb-002-usecase-must-have-__invoke-method)

```
// Correct: Single entry point via __invoke
final readonly class CreateUserUseCase
{
    public function __invoke(CreateUserDtoInterface $dto): Result { }
}

// Wrong: Named method
final readonly class CreateUserUseCase
{
    public function execute(CreateUserDtoInterface $dto): Result { }
}
```

### UCB-003: No Authorization in UseCase Layer

[](#ucb-003-no-authorization-in-usecase-layer)

Authorization belongs in the Request's `securityCheck()` method, NOT in the UseCase.

```
// Correct: Authorization in Request
final class CreateUserRequest extends AbstractValidatedRequest
{
    protected function securityCheck(): bool
    {
        return $this->isGranted('ROLE_ADMIN');
    }
}

// Wrong: Security in UseCase
final readonly class CreateUserUseCase
{
    public function __construct(private Security $security) {} // Forbidden!

    public function __invoke(CreateUserDtoInterface $dto): Result
    {
        if (!$this->security->isGranted('ROLE_ADMIN')) { } // Forbidden!
    }
}
```

### UCB-004: Controller Must Use $this-&gt;response()

[](#ucb-004-controller-must-use-this-response)

```
// Correct: Use $this->response()
return $this->response($useCase($request), ['user:read']);

// Wrong: Manual JSON construction
return $this->json(['user' => $user]);
```

### UCB-005: Controller Action Methods Must Have "Action" Suffix

[](#ucb-005-controller-action-methods-must-have-action-suffix)

```
// Correct
public function createUserAction(): JsonResponse { }

// Wrong
public function createUser(): JsonResponse { }
```

Architecture Flow
-----------------

[](#architecture-flow)

```
HTTP Request
    ↓
Controller receives Request object
    ↓
Request auto-validates (constraints)
    ↓
Request checks authorization (securityCheck())
    ↓
Controller invokes UseCase with Request (implements DTO interface)
    ↓
UseCase executes pure business logic
    ↓
UseCase returns Result object
    ↓
Controller converts Result to JsonResponse via $this->response()
    ↓
HTTP Response with proper status code

```

Error Handling
--------------

[](#error-handling)

Event listeners provide automatic exception handling:

- **ValidationExceptionListener**: Catches validation exceptions, returns structured error JSON
- **AuthorizationExceptionListener**: Handles access denied exceptions with 403 responses
- **HttpMalformedRequestException**: Thrown by `getValue()` for null/undefined/unset properties

### Error Codes

[](#error-codes)

Use error codes for client-side handling of specific failure cases:

```
final class ErrorCodes
{
    public const int USER_ALREADY_EXISTS = 100;
    public const int EMAIL_ALREADY_TAKEN = 101;
    public const int INVALID_PASSWORD = 102;
}

// In use case
return Result::create(ResultType::DUPLICATED, 'Email already exists')
    ->withErrorCode(ErrorCodes::EMAIL_ALREADY_TAKEN);
```

Development
-----------

[](#development)

This bundle uses Docker for development. All commands run inside containers.

### Setup

[](#setup)

```
# Clone the repository
git clone
cd use-case-bundle

# Install dependencies (inside Docker)
docker compose run --rm lib composer install
```

### Running Tests

[](#running-tests)

```
# Run all unit tests
docker compose run --rm lib tests:unit

# Run PHPUnit directly
docker compose run --rm lib phpunit

# Run with coverage
docker compose run --rm lib phpunit --coverage-text
```

Test structure:

- Tests located in `tests/Unit/` mirroring `src/` structure
- 165 tests, 297 assertions
- Mother objects in `tests/_Data/MotherObject/` for test data builders

### Static Analysis

[](#static-analysis)

```
# Run PHPStan (max level)
make phpstan
# or
docker compose run --rm lib composer phpstan

# Generate baseline for existing issues
make phpstan_baseline
```

PHPStan configuration:

- Level: max (highest strictness)
- Analyzes both `src/` and `tests/`
- Extensions: PHPUnit, Symfony

### Interactive Development

[](#interactive-development)

```
# Enter bash shell in container
docker compose run --rm lib bash

# Inside container, run commands:
composer tests:unit
composer phpstan
vendor/bin/phpunit
```

### Deployment

[](#deployment)

```
# Tag and publish new version (reads version from composer.json)
make tag

# Publish dev-master
make publish
```

Testing Your Integration
------------------------

[](#testing-your-integration)

```
use TeamMatePro\Contracts\Collection\Result;
use TeamMatePro\Contracts\Collection\ResultType;

class CreateUserUseCaseTest extends TestCase
{
    #[Test]
    public function newUserIsCreatedSuccessfully(): void
    {
        // Given
        $dto = $this->createMock(CreateUserDtoInterface::class);
        $dto->method('getEmail')->willReturn('test@example.com');
        $dto->method('getName')->willReturn('Test User');

        // When
        $result = $this->useCase->__invoke($dto);

        // Then
        $this->assertSame(ResultType::SUCCESS_CREATED, $result->getType());
        $this->assertNotNull($result->getResult());
    }

    #[Test]
    public function duplicateEmailReturnsDuplicatedResult(): void
    {
        // Given: existing user with same email
        $this->givenUserExistsWithEmail('test@example.com');

        $dto = $this->createMock(CreateUserDtoInterface::class);
        $dto->method('getEmail')->willReturn('test@example.com');

        // When
        $result = $this->useCase->__invoke($dto);

        // Then
        $this->assertSame(ResultType::DUPLICATED, $result->getType());
        $this->assertSame('USER_EXISTS', $result->getErrorCode());
    }
}
```

Requirements
------------

[](#requirements)

- PHP &gt;= 8.3
- Symfony &gt;= 7.0
- Docker (for development)

Configuration
-------------

[](#configuration)

No special configuration required. The bundle auto-configures when installed in a Symfony application.

Contributing
------------

[](#contributing)

1. Fork the repository
2. Create a feature branch
3. Write tests for your changes
4. Ensure PHPStan passes at max level
5. Submit a pull request

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

Author
------

[](#author)

Sebastian Twaróg ()

Links
-----

[](#links)

- Documentation:
- Package: team-mate-pro/use-case-bundle

###  Health Score

49

—

FairBetter than 94% of packages

Maintenance92

Actively maintained with recent releases

Popularity23

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity59

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~13 days

Total

16

Last Release

38d ago

Major Versions

2.4.0 → 3.0.02026-05-27

PHP version history (2 changes)2.0.0PHP &gt;=8.2

2.0.3PHP &gt;=8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/d03da0e36972b87363f18ae2773a03c48142b6ebcebf75eea64698cb6cea0146?d=identicon)[teammatepro](/maintainers/teammatepro)

---

Top Contributors

[![serek-dev](https://avatars.githubusercontent.com/u/6751932?v=4)](https://github.com/serek-dev "serek-dev (20 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/team-mate-pro-use-case-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/team-mate-pro-use-case-bundle/health.svg)](https://phpackages.com/packages/team-mate-pro-use-case-bundle)
```

###  Alternatives

[rcsofttech/audit-trail-bundle

Enterprise-grade, high-performance Symfony audit trail bundle. Automatically track Doctrine entity changes with split-phase architecture, multiple transports (HTTP, Queue, Doctrine), and sensitive data masking.

1189.8k](/packages/rcsofttech-audit-trail-bundle)[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.9M388](/packages/easycorp-easyadmin-bundle)[pimcore/pimcore

Content &amp; Product Management Framework (CMS/PIM/E-Commerce)

3.8k3.8M508](/packages/pimcore-pimcore)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M203](/packages/sulu-sulu)[kimai/kimai

Kimai - Time Tracking

4.8k9.0k1](/packages/kimai-kimai)[prestashop/prestashop

PrestaShop is an Open Source e-commerce platform, committed to providing the best shopping cart experience for both merchants and customers.

9.1k17.8k](/packages/prestashop-prestashop)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
