letkode / common-bundle
Common exceptions, attributes, value resolvers and utilities for Symfony applications
Package info
github.com/letkode/common-bundle
Type:symfony-bundle
pkg:composer/letkode/common-bundle
Requires
- php: ^8.4
- doctrine/persistence: ^3.0 || ^4.0
- letkode/helpers-bundle: ^1.0
- psr/container: ^1.1 || ^2.0
- symfony/dependency-injection: ^7.0 || ^8.0
- symfony/http-foundation: ^7.0 || ^8.0
- symfony/http-kernel: ^7.0 || ^8.0
- symfony/translation-contracts: ^3.0
- symfony/uid: ^7.0 || ^8.0
- symfony/validator: ^7.0 || ^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- nelmio/api-doc-bundle: ^5.10
- phpstan/phpstan: ^2
- phpunit/phpunit: ^11
- zircote/swagger-php: ^6.0
Suggests
- nelmio/api-doc-bundle: Enables DocAttributeRouteDescriber, which applies the OpenAPI attributes of a Doc class referenced by a controller attribute (^5.10)
Provides
None
Conflicts
None
Replaces
None
README
Common exceptions, attributes, value resolvers and utilities for Symfony applications.
Installation
composer require letkode/common-bundle
Symfony Flex will register the bundle automatically. If not using Flex, add it manually:
// config/bundles.php return [ Letkode\CommonBundle\LetkodeCommonBundle::class => ['all' => true], ];
Contents
Exceptions
ValueObjectException is thrown by value objects that reject invalid input. It carries a translation key and its parameters so the caller can translate the message in the validators domain.
throw new ValueObjectException('Invalid email.', translationKey: 'errors.email_invalid');
The HTTP status exceptions (BadRequestException, NotFoundException, ...) and the JSON ExceptionListener now live in letkode/http-exception-bundle.
Attributes
#[UniqueField] — Constraint
Validates that a field value is unique in the database via Doctrine.
#[UniqueField(entityClass: User::class, field: 'email')] public string $email;
Use scopes to narrow the lookup and/or exclude the record being edited from the check. Each
UniqueFieldScope resolves a value from a route param or a sibling property, then either
Filters the query (via an association) or Excludes a match from the violation:
// Update DTO: same email is fine if it's still the record at /users/{uuid} #[UniqueField( entityClass: User::class, field: 'email', scopes: [new UniqueFieldScope('uuid', UniqueFieldScopeSource::RouteParam, 'uuid', UniqueFieldScopeMode::Exclude)], )] public string $email; // Create DTO: email only needs to be unique within the tenant from /tenants/{tenantUuid}/contacts #[UniqueField( entityClass: TenantContact::class, field: 'email', scopes: [new UniqueFieldScope('tenant', UniqueFieldScopeSource::RouteParam, 'tenantUuid', scopeEntityClass: Tenant::class)], )] public string $email;
Use valueTransformer to compare the value in the same form it is stored in. Implement
UniqueFieldValueTransformerInterface in an autowired/autoconfigured service and reference its
class; the validator transforms the value before the lookup and reports the transformed value in
the violation. A transformer returning null or '' skips the check.
final class LowercaseTransformer implements UniqueFieldValueTransformerInterface { public function transform(mixed $value, object|null $object, string|null $property): mixed { return \is_string($value) ? mb_strtolower(trim($value)) : $value; } } #[UniqueField(entityClass: User::class, field: 'email', valueTransformer: LowercaseTransformer::class)] public string $email;
Transformers must be idempotent (transforming an already-transformed value returns it unchanged). Scopes keep reading the original sibling values.
#[MapUuid] — Mapping
Marks a constructor parameter or property for automatic UUID deserialization.
Value Resolver
UuidValueResolver
Automatically resolves Uuid typed route parameters without manual conversion.
#[Route('/users/{uuid}')] public function show(Uuid $uuid): JsonResponse { ... }
OpenAPI
DocAttributeRouteDescriber
Optional: registered only when nelmio/api-doc-bundle ^5.10 is installed.
NelmioApiDocBundle only reads #[OA\*] attributes declared on the controller class or method itself. To keep the
documentation apart from the controller, declare an attribute class that carries the operations and apply it to the
controller; the describer merges them into the generated specification:
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] #[OA\Get(path: '/api/companies', summary: 'List companies', responses: [new OA\Response(response: 200, description: 'OK')])] #[OA\Post(path: '/api/companies', summary: 'Create company')] final class DocCompaniesEndpoints {} #[Route('/api/companies')] #[DocCompaniesEndpoints] final class CompanyController { /* ... */ }
- Without the describer Nelmio instantiates
#[DocCompaniesEndpoints]and ignores it: none of its operations reach the specification. - An operation applies to a route when its HTTP method matches and, if it declares a
path, the path matches the route's. Place the attribute on a method to document just that route (an operation withoutpaththen applies to it). - The Doc takes precedence over what Nelmio generated:
tagsare united,parametersandresponsesare matched by (in,name) and by status code, and any other property the Doc defines (summary, description, request body...) replaces it. A parameter generated from#[MapQueryString]therefore keeps its own entry unless the Doc declares one with the same name, which is how a Doc can add e.g. anenumtosort. - Only operations (
OA\Get,OA\Post, ...) are applied; a rootOA\Response,OA\ParameterorOA\RequestBodyon a Doc class is ignored.
Utils
BuilderUrlClient
Builds absolute URLs using the configured APP_CLIENT_URL base. Requires letkode/helpers-bundle.
JsonReader
Reads and decodes JSON files from the filesystem, throwing on invalid JSON.
Requirements
- PHP
^8.4 - Symfony
^7.0 || ^8.0 doctrine/persistence^3.0letkode/helpers-bundle^1.0
License
MIT — see LICENSE.