Search by

uxf / core

uxf

Package info

gitlab.com/uxf/core

Issues

pkg:composer/uxf/core

Statistics

Installs: 24 151

Dependents: 10

Suggesters: 1

Stars: 0

3.84.0 2026-09-16 20:50 UTC

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>>