uxf / core
Requires
- php: ^8.4
- ext-json: *
- brick/money: ^0.15
- doctrine/orm: ^2.19 || ^3.0
- nette/utils: ^4.0
- ramsey/uuid: ^4.0
- symfony/http-foundation: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
- symfony/polyfill-php85: ^1.32
- symfony/property-access: ^7.4 || ^8.0
- symfony/property-info: ^7.4 || ^8.0
- symfony/validator: ^7.4 || ^8.0
- symfony/yaml: ^7.4 || ^8.0
- uxf/hydrator: 3.84.0
Requires (Dev)
None
Suggests
- egulias/email-validator: Allow use Email
- giggsey/libphonenumber-for-php: Allow use Phone
- globalcitizen/php-iban: Allow use Iban
Provides
None
Conflicts
None
Replaces
None
- 3.x-dev
- 3.84.0
- 3.83.1
- 3.83.0
- 3.82.0
- 3.81.8
- 3.81.5
- 3.81.4
- 3.81.3
- 3.81.2
- 3.81.0
- 3.80.3
- 3.80.2
- 3.80.0
- 3.79.3
- 3.79.2
- 3.79.0
- 3.78.7
- 3.78.2
- 3.78.0
- 3.77.9
- 3.77.8
- 3.77.7
- 3.77.5
- 3.77.4
- 3.77.3
- 3.77.2
- 3.76.3
- 3.76.2
- 3.76.0
- 3.75.12
- 3.75.10
- 3.75.3
- 3.75.0
- 3.73.6
- 3.73.5
- 3.73.4
- 3.73.3
- 3.73.2
- 3.73.0
- 3.72.2
- 3.72.1
- 3.72.0
- 3.71.0
- 3.70.5
- 3.70.4
- 3.70.3
- 3.70.2
- 3.70.0
- 3.69.12
- 3.69.7
- 3.69.6
- 3.69.5
- 3.69.3
- 3.69.1
- 3.69.0
- 3.68.0
- 3.67.3
- 3.67.0
- 3.66.0
- 3.65.3
- 3.65.2
- 3.65.0
- 3.64.4
- 3.64.3
- 3.64.2
- 3.64.1
- 3.64.0
- 3.63.1
- 3.62.4
- 3.62.3
- 3.62.2
- 3.62.0
- 3.61.7
- 3.61.6
- 3.61.5
- 3.61.1
- 3.61.0
- 3.60.12
- 3.60.11
- 3.60.10
- 3.60.8
- 3.60.7
- 3.60.5
- 3.60.3
- 3.60.0
- 3.59.0
- 3.58.5
- 3.58.2
- 3.58.1
- 3.58.0
- 3.57.12
- 3.57.10
- 3.57.7
- 3.57.6
- 3.57.1
- 3.57.0
- 3.56.3
- 3.56.2
- 3.56.1
- 3.56.0
- 3.55.9
- 3.55.8
- 3.55.7
- 3.55.5
- 3.55.0
- 3.54.5
- 3.54.3
- 3.54.2
- 3.54.0
- 3.53.7
- 3.53.3
- 3.53.2
- 3.53.1
- 3.53.0
- 3.51.2
- 3.51.0
- 3.50.6
- 3.50.5
- 3.50.3
- 3.50.2
- 3.50.1
- 3.50.0
- 3.49.2
- 3.49.1
- 3.49.0
- 3.48.0
- 3.47.2
- 3.47.0
- 3.46.11
- 3.44.6
- 3.44.5
- 3.44.4
- 3.44.3
- 3.44.2
- 3.44.0
- 3.43.2
- 3.43.0
- 3.42.0
- 3.41.2
- 3.41.1
- 3.41.0
- 3.40.4
- 3.40.3
- 3.40.2
- 3.40.1
- 3.40.0
- 3.39.4
- 3.39.3
- 3.39.2
- 3.39.1
- 3.38.0
- 3.37.1
- 3.37.0
- 3.36.3
- 3.36.2
- 3.36.0
- 3.35.5
- 3.35.4
- 3.35.2
- 3.34.3
- 3.34.0
- 3.32.4
- 3.32.3
- 3.30.1
- 3.29.1
- 3.29.0
- 3.27.3
- 3.26.0
- 3.24.2
- 3.24.1
- 3.24.0
- 3.23.3
- 3.23.1
- 3.23.0
- 3.22.0
- 3.21.4
- 3.21.3
- 3.21.0
- 3.20.0
- 3.19.4
- 3.19.2
- 3.18.0
- 3.17.4
- 3.17.3
- 3.17.1
- 3.17.0
- 3.15.6
- 3.15.5
- 3.13.2
- 3.13.0
- 3.11.3
- 3.11.0
- 3.10.1
- 3.10.0
- 3.9.2
- 3.8.2
- 3.8.1
- 3.8.0
- 3.7.3
- 3.7.1
- 3.7.0
- 3.6.0
- 3.5.0
- 3.4.0
- 3.3.0
- 3.2.4
- 3.2.3
- 3.2.2
- 3.2.1
- 3.2.0
- 3.1.4
- 3.1.3
- 3.1.2
- 3.1.1
- 3.1.0
- 3.0.4
- 3.0.3
- 3.0.2
- 3.0.1
- 3.0.0
- 1.9.1
- 1.9.0
- 1.8.1
- 1.8.0
- 1.7.7
- 1.7.6
- 1.7.5
- 1.7.4
- 1.7.3
- 1.7.2
- 1.7.1
- 1.7.0
- 1.6.0
- 1.5.1
- 1.5.0
- 1.4.3
- 1.4.2
- 1.4.1
- 1.4.0
- 1.3.0
- 1.2.1
- 1.2.0
- 1.1.2
- 1.1.1
- 1.1.0
- 1.0.1
- 1.0.0
- v0.3.5
- v0.3.4
- v0.3.3
- v0.3.2
- v0.3.1
- v0.3.0
- v0.2.1
- v0.2.0
- v0.1.4
- v0.1.3
- v0.1.2
- v0.1.1
- v0.1
- dev-legacy
- dev-main
- dev-master
- dev-enum-validation
- dev-psr-12
This package is auto-updated.
Last update: 2026-09-16 18:58:04 UTC
README
Install
$ composer require uxf/core
Config
// config/packages/uxf.php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $config): void {
$config->extension('uxf_core', [
'gen' => [
// everything is optional
'translation' => [
'enabled' => false, // default true
'tolgee_dsn' => env('TOLGEE_DSN'), // tolgee://key@translations.uxf.dev/1?namespaces=a,b,c
'sources' => [__DIR__ . '/../../translations'],
'destination' => __DIR__ . '/../generated',
'namespace' => 'UXF\CoreTests\Project\Translation',
],
],
]);
};
Commands
# generate Translation.php from *.json files
$ bin/console uxf:core-gen
# download data from tolgee - TOLGEE_DSN required!
$ bin/console uxf:core:translation:pull
System providers
use UXF\Core\SystemProvider\Calendar;
use UXF\Core\SystemProvider\Clock;
use UXF\Core\SystemProvider\Uuid;
// uuid 4
$uuid = Uuid::uuid4();
// uuid 7
$uuid = Uuid::uuid7();
// uuid 7 with custom date time
$uuid = Uuid::uuid7($customDate);
// date time
$now = Clock::now();
// date
$today = Calendar::today();
// you can freeze time + date in tests
Clock::$frozenTime = new DateTime('2022-01-13T22:43:51+00:00');
Calendar::$frozenDate = Date::fromDateTime($dateTime);
// time travel
Clock::timeTravel(new DateTime('2030-01-13 22:43:51'), static function () {
// space with custom time
...
});
Doctrine
ManyToManyHelper
use UXF\Core\Utils\ManyToManyHelper;
class Entity
{
/** @var Collection<int, Tag> */
#[ORM\ManyToMany(Tag::class)]
private Collection $tags;
/**
* @param array<Tag>|Collection<int, Tag> $tags
*/
public function setTags(Collection|array $tags): void
{
ManyToManyHelper::replace($this->tags, $tags);
}
}
DATE(created_at)
TIME(created_at)
CAST(column_a AS TEXT)
UNACCENT(column_a)
JSONB_CONTAINS(root.items, :value) = TRUE
JSONB_CONTAINS_SUBSTRING(root.items, :value) = TRUE
JSONB_CONTAINS
$items = $this->entityManager->createQueryBuilder()
->select('t')
->from(Test::class, 't')
->where('JSONB_CONTAINS(t.data, :value) = TRUE')
->setParameter('value', json_encode([1, 2]))
->getQuery()
->getResult();
// value examples:
// json_encode([true])
// json_encode([false])
// json_encode(["A"])
// json_encode(["name" => "UXF"])
JSONB_CONTAINS_SUBSTRING
$items = $this->entityManager->createQueryBuilder()
->select('t')
->from(Test::class, 't')
->where('JSONB_CONTAINS_SUBSTRING(t.data, :value) = TRUE')
->setParameter('value', '%HELLO%')
->getQuery()
->getResult();
Custom types
# time + date
$a = new UXF\Core\Type\Date('2020-10-30');
$b = new UXF\Core\Type\DateTime('2020-10-30 10:00:11');
$c = new UXF\Core\Type\Time('10:00:00');
# money
$x = UXF\Core\Type\Currency::CZK
$y = UXF\Core\Type\Money::of(666, $y);
# decimal
$d = UXF\Core\Type\Decimal::of(666);
# url
$e = UXF\Core\Type\Url::of('https://google.com/wow?hello[]=1#fragment');
# url relative
$e2 = UXF\Core\Type\UrlRelative::of('/wow?hello[]=1#fragment');
# country
$f = UXF\Core\Type\Country::CZE;
# phone
$z = UXF\Core\Type\Phone::of('+420777666777');
# email
$z = UXF\Core\Type\Email::of('info@uxf.cz');
# national identification number for CZ - NinCze
$o = UXF\Core\Type\NationalIdentificationNumberCze::of('930201/3545');
# bank account number for CZ - BanCze
$p = UXF\Core\Type\BankAccountNumberCze::of('123/0300');
# IBAN
$p = UXF\Core\Type\Iban::of('CZ95 0300 0000 1900 0000 1230');
FromBody & FromQuery & FromHeader & FromForm
use UXF\Core\Attribute\FromBody;
use UXF\Core\Attribute\FromQuery;
use UXF\Core\Attribute\FromHeader;
class TestController
{
public function __invoke(
#[FromBody] TestRequestBody $body,
#[FromQuery] TestRequestQuery $query,
#[FromHeader] TestRequestHeader $header,
) {
...
}
}
U FromQuery a FromForm se hodnoty přetypují podle deklarovaného typu. Když se hodnota na daný typ
převést nedá, například když přijde pole (?resource=a&resource=b) na skalární parametr nebo nečíselný
text na int, rozhoduje nullabilita: nullable parametr dostane null, povinný si nechá původní
hodnotu, takže ohlásí validační chybu „invalid value“ a ne „missing value“. DTO, které chce vstup
posoudit samo, má tedy mít všechny parametry nullable.
Vlastní podoba chybové odpovědi
Parametry se převádějí ještě před controllerem, takže chybějící nebo vadná hodnota skončí výjimkou,
kterou controller nemůže zachytit. Pokud endpoint musí odpovídat v jiném tvaru, například podle nějaké
normy nebo HTML stránkou, implementuje UXF\Core\Http\RequestExceptionResolver a odpověď si sestaví sám.
Návrat null nechá výjimku projít obvyklou cestou. Používat střídmě: klient generovaný z tohoto API
balíčkem uxf/gen čeká standardní tvar chyby.
final class TokenController implements RequestExceptionResolver
{
public function __invoke(#[FromForm] TokenRequestForm $form) { ... }
public function resolveRequestException(Request $request, CoreException $exception): ?Response
{
return new JsonResponse(['error' => 'invalid_request'], 400);
}
}
FromForm
FromForm je alternativa k FromBody pro klasické HTTP formuláře
(application/x-www-form-urlencoded a multipart/form-data). Místo JSON těla čte
$request->request a $request->files, takže umí i nahrané soubory
(UploadedFile / UploadedFile[]). Hodnoty jsou stejně jako u FromQuery
automaticky přetypovány podle deklarovaného typu (string -> int/float/bool/enum).
use Symfony\Component\HttpFoundation\File\UploadedFile;
use UXF\Core\Attribute\FromForm;
final readonly class UploadRequestForm
{
/**
* @param UploadedFile[] $attachments
*/
public function __construct(
public string $title,
public int $priority,
public UploadedFile $cover,
public array $attachments = [],
public ?UploadedFile $thumbnail = null,
) {
}
}
class UploadController
{
#[Route('/api/upload', methods: 'POST')]
public function __invoke(#[FromForm] UploadRequestForm $form)
{
...
}
}
Pozor: PHP naplní form data a soubory pouze u metody POST. Pro PUT/PATCH
s multipart tělem zůstane $request->request prázdný - FromForm proto používejte
jen na POST routách.
FromBody array
use UXF\Core\Attribute\FromBody;
class TestController
{
/**
* @param TestRequestBody[] $body
*/
public function __invoke(#[FromBody(TestRequestBody::class)] array $body)
{
...
}
}
PATCH method
class TestPatchRequestBody
{
public function __controller(
public readonly string | null | NotSet $string = new NotSet(),
) {
}
}
use UXF\Core\Attribute\FromBody;
class TestController
{
public function __invoke(#[FromBody] TestPatchRequestBody $body)
{
if (!$body->string instanceof NotSet) {
...
}
}
}
Entity
Controller::__invoke method entity arguments (uri path params) are resolved by entity primary identifier (or by custom property specified by #[Entity('uuid')])
use UXF\Core\Attribute\Entity;
#[ORM\Entity]
class File
{
#[ORM\Column, ORM\Id]
public int $id;
#[ORM\Column(type: 'uuid', unique: true)]
public UuidInterface $uuid;
}
final readonly class TestIdController
{
// eg. GET /1
#[Route('/{file}')]
public function __invoke(#[Entity] File $file)
{
...
}
}
final readonly class TestUuidController
{
// eg. GET /4f94e4b0-e31a-4070-9ae0-59b32006d911
#[Route('/{file}')]
public function __invoke(#[Entity('uuid')] File $file)
{
...
}
}
final readonly class TestUnionController
{
// eg. GET /4f94e4b0-e31a-4070-9ae0-59b32006d911 or GET /1
#[Route('/{file}')]
public function __invoke(#[Entity(['id', 'uuid'])] File $file)
{
...
}
}
Response with HydratorMap (Union)
use UXF\Hydrator\Attribute\HydratorMap;
// interface
#[HydratorMap(property: 'type', matrix: [
'o' => Orienteering::class,
'p' => Paragliding::class,
])]
interface ActivityResponse
{
}
// children
class OrienteeringResponse implements ActivityResponse
{
public function __construct(
public readonly int $card,
public readonly string $type = 'o',
) {
}
}
class ParaglidingResponse implements ActivityResponse
{
public function __construct(
public readonly string $glider,
public readonly string $type = 'p',
) {
}
}
// usage
class ClubController
{
public function __invoke(): ActivityResponse
{
return new ParaglidingResponse('GIN');
}
}
UXF\Core\Http\ResponseModifierInterface - modify symfony response
use Symfony\Component\HttpFoundation\Response;
use UXF\Core\Http\ResponseModifierInterface;
/**
* @implements ResponseModifierInterface<array<string, string>>
*/
final class ModifiedResponseController implements ResponseModifierInterface
{
/**
* @return array<string, string>
*/
public function __invoke(): array
{
return [];
}
public static function modifyResponse(Response $response, mixed $data): void
{
$response->setStatusCode(509);
$response->headers->set('X-Test', 'hello');
}
}
Translations
Example usage
use App\CoreZone\Translation\Translation;
throw BasicException::badRequest(Translation::of(Translation::NOT_FOUND), 'ERROR_CODE');
use App\CoreZone\Translation\Translation as T;
use UXF\Core\Translator\Translator;
final readonly class Example
{
public function __construct(private Translator $translator)
{
}
public function example(): void
{
$a = $this->translator->trans(T::of(T::MESSAGE));
$b = $this->translator->trans(T::of(T::MESSAGE, [
'param1' => 'hello',
'param2' => 'world',
]));
}
}
Utils
PHPStan
# phpstan.neon
includes:
- vendor/uxf/core/config/extension.neon
#[UXF\Core\Attribute\Internal]
use UXF\Core\Attribute\Internal;
final readonly class Funny
{
#[Internal(FunnyService::class)]
public function setState(State $state): void
{
...
}
}
UXF\Core\Type\Money
$a = Money::of(1.50, Currency::CZK);
$b = Money::of('1.40', Currency::CZK);
$z = Money::zero(Currency::CZK);
// +
$c = $a->plus($b);
// -
$c = $a->minus($b);
// *
$c = $a->multipliedBy($b);
// /
$c = $a->dividedBy($b);
// round
$c = $a->toScale(1);
// (int)
$c = $a->amountInt();
// (float)
$c = $a->amountFloat();
// (bool)
$c = $a->equals($b);
// (bool)
$c = $a->isZero();
UXF\Core\Type\Decimal
$a = Decimal::of(1.50);
$b = Decimal::of('1.40');
$z = Decimal::zero();
// +
$c = $a->plus($b);
// -
$c = $a->minus($b);
// *
$c = $a->multipliedBy($b);
// /
$c = $a->dividedBy($b);
// round
$c = $a->toScale(1);
// (int)
$c = $a->toInt();
// (float)
$c = $a->toFloat();
// (bool)
$c = $a->equals($b);
// (bool)
$c = $a->isZero();
UXF\Core\Type\Url
$url = Url::of('https://user:pass@google.com:443/hello?ok&test[]=1#fragment');
/**
# UXF\Core\Type\UrlComponents {
scheme: "https"
host: "google.com"
port: 443
user: "user"
pass: "pass"
path: "/hello"
query: [
"ok" => ""
"test" => [1]
]
fragment: "fragment"
}
*/
$components = $url->getComponents();
UXF\Core\Utils\Lst
// find
Lst::from([$a, $b])->find(fn (X $a): bool => $a->id === 1); // $a
// map
Lst::from([$a, $b])->map(fn (X $x, int $index): Y => $x->y); // Lst[$x, $y]
// filter
Lst::from([$a, $b])->filter(fn (X $a): bool => $a->id > 1); // Lst[$b]
// sort
Lst::from([$a, $b])->sort(fn (X $a, X $b): int => $a <=> $b); // Lst[$b, $a]
// unique
Lst::from([$a, $b, $b])->unique(); // Lst[$a, $b]
// concat
Lst::from([$a, $b])->concat(Lst::from([$c, $d])); // Lst[$a, $b, $c, $d]
// push
Lst::from([$a, $b])->push($c); // Lst[$a, $b, $c]
// unshift
Lst::from([$a, $b])->unshift($c); // Lst[$c, $a, $b]
// slice
Lst::from([$a, $b, $c])->slice(1, 1); // Lst[$b]
// join
Lst::from([1, 2, 3])->join(', '); // '1, 2, 3'
// aggregate
Lst::from([1, 2, 3])->aggregate(0, fn (int $sum, int $item) => $sum + $item)); // 6
// forEach
Lst::from([1, 2, 3])->forEach(fn (int $item, int $index) => $test->counter += $item); // void
// forAll
Lst::from([1, 2, 3])->forAll(fn (int $item, int $index) => $item > 0); // true
// dictionary
Lst::from([$a, $b])->dictionary(fn (X $a) => $a->key, fn (X $a) => $a->name); // ['key1' => 'value1', 'key2' => 'value2']
// groupBy
Lst::from([$a, $b, $c])->groupBy(fn (X $a) => $a->key, fn (X $a) => $a); // ['key1' => [$a], 'key2' => [$b, $c]]
// groupByToMap
$map = Lst::from([$a, $b, $c])->groupByToMap(fn (X $a) => $a->key); // Map<KeyType, list<X>>
// first
Lst::from([$a, $b])->first(); // $a
Lst::from([])->first(); // null
// last
Lst::from([$a, $b])->last(); // $b
Lst::from([])->last(); // null
// count
Lst::from([$a, $b])->count(); // 2
// isEmpty
Lst::from([$a, $b])->isEmpty(); // false
// contains
Lst::from([$a, $b])->contains($a); // true
// exists
Lst::from([$a, $b])->exists(fn (X $a): bool => $a->id === 1); // true
// getValues
Lst::from([$a, $b])->getValues(); // [$a, $b]
// full
$values = Lst::from([
Language::create(10, 'Z'),
Language::create(11, 'A'),
Language::create(12, 'C'),
Language::create(13, 'A'),
Language::create(14, 'D'),
])
->filter(fn (Language $l) => $l->getId() >= 12)
->sort(fn (Language $a, Language $b) => $a->getName() <=> $b->getName())
->map(fn (Language $l) => $l->getName())
->unique()
->getValues();
UXF\Core\Utils\Map
Map is an immutable collection of key-value pairs. It supports objects (especially Enums and objects implementing UXF\Core\Type\Equals) as keys.
use UXF\Core\Utils\Map;
use UXF\Core\Utils\MapItem;
// from array
$map1 = Map::fromArray(['a' => 1, 'b' => 2]);
// from MapItem list
$map2 = Map::from([new MapItem(MyEnum::A, 'value')]);
// get
$map1->get('a'); // 1
$map2[MyEnum::A]; // 'value' (implements ArrayAccess)
// forEach
$map2->forEach(fn (string $value, MyEnum $key) => ...);
// groupByToMap (from Lst)
$map = Lst::from([$a, $b, $c])->groupByToMap(fn (X $x) => $x->type); // Map<Type, list<X>>