- Installation
- Common
- Doctrine Utils
- Symfony
- Value Objects
composer require cosmologist/gears
ArrayType::get(['fruit' => 'apple', 'color' => 'red'], 'fruit'); // 'apple'
ArrayType::get(['fruit' => 'apple', 'color' => 'red'], 'weight'); // null
ArrayType::get(['fruit' => 'apple', 'color' => 'red'], 'weight', 15); // 15
ArrayType::get(['apple', 'red'], -1); // 'red'
It's more intuitive variant to <code>$array += [$key => $value];
$array = ['fruit' => 'apple'];
ArrayType::touch($array, 'color', 'red']); // ['fruit' => 'apple', 'color' => 'red'];
ArrayType::touch($array, 'fruit', 'banana']); // ['fruit' => 'apple'];
$a = [1,2];
ArrayType::push($a, 3); // [1,2,3]
$a = [2,3];
ArrayType::unshift($a, 1); // [1,2,3]
ArrayType::average([1, 2, 3]); // 3
ArrayType::checkAssoc([1, 2, 3]); // false
ArrayType::checkAssoc(['foo' => 'bar']); // true
ArrayType::contains(array $list, mixed $item);
Unlike array_shift() or reset(), this function safely handles any iterable and allows filtering via a callback.
// Get the first item of any iterable
ArrayType::first([1, 2, 3]); // returns 1
// Find first even number
ArrayType::first([1, 3, 4, 6], fn($x) => $x % 2 === 0); // returns 4
// Use named argument for optional parameter
ArrayType::first([1, 2, 3], condition: fn($x) => $x > 1); // returns 2
// Returns null if no match or empty
ArrayType::first([], condition: fn($x) => $x > 0); // returns null
Unlike end() or array_pop(), this function works with any iterable and supports filtering via a callback.
// Get the last item of any iterable
ArrayType::last([1, 2, 3]); // returns 3
// Find last even number
ArrayType::last([1, 4, 3, 6], fn($x) => $x % 2 === 0); // returns 6
// Use named argument for optional parameter
ArrayType::last([1, 2, 3], condition: fn($x) => $x < 3); // returns 2
// Returns null if no match or empty
ArrayType::last([], condition: fn($x) => $x > 0); // returns null
ArrayType::insertAfter(['a' => 1, 'c' => 3], 'a', ['b' => 2]); // ['a' => 1, 'b' => 2, 'c' => 3]
// If the key doesn't exist
ArrayType::insertAfter(['a' => 1, 'b' => 2], 'c', ['foo' => 'bar']); // ['a' => 1, 'b' => 2, 'foo' => 'bar']
ArrayType::insertBefore(['a' => 1, 'c' => 3], 'c', ['b' => 2]); // ['a' => 1, 'b' => 2, 'c' => 3]
// If the key doesn't exist
ArrayType::insertBefore(['a' => 1, 'b' => 2], 'c', ['foo' => 'bar']); // ['foo' => 'bar', 'a' => 1, 'b' => 2]
ArrayType::ranges([1, 3, 7, 9]); // [[1, 3], [3, 7], [7, 9]]
ArrayType::unsetValue(['a', 'b', 'c'], 'b'); // ['a', 'c']
ArrayType::deviation([1, 2, 1]); // float(0.4714045207910317)
Behavior for different types:
- array - returns as is
- iterable - converts to a native array (
iterator_to_array()
) - another - creates an array with argument ([value])
ArrayType::toArray($value);
If encoded value is false, true or null then returns empty array.
JSON_THROW_ON_ERROR always enabled.
ArrayType::fromJson($json): array;
By serializing into a JSON string.
You can use the cache value calculation function as part of the cache key
(just pass its closure along with the other parameters).
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
$cacheKey = CacheUtils::generateKey('foo', 123, ['foo' => 'bar'], (object) ['bar' => 'baz'], $identifier);
// or
$cacheKey = CacheUtils::generateKey('my-cache-key', $identifier);
// or
$cacheKey = CacheUtils::generateKey(computingFunction(...), $identifier);
// On cache misses, a callback is called that should return the missing value.
$callback = fn() => computingFunction($identifier);
// or
$callback = fn(ItemInterface $item) use ($identifier) {
$item->expiresAfter(3600);
...
computingFunction($identifier);
}
$value = $cache->get($cacheKey, $callback);
CallableType::isClosure(fn($foo) => $foo); // bool(true)
CallableType::isClosure('foo'); // bool(false)
CallableType::isClosure([$foo, 'bar']); // bool(false)
CallableType::isClosure('Foo\Bar::baz'); // bool(false)
CallableType::isFunction(fn($foo) => $foo); // bool(false)
CallableType::isFunction('foo'); // bool(true)
CallableType::isFunction([$foo, 'bar']); // bool(false)
CallableType::isFunction('Foo\Bar::baz'); // bool(false)
CallableType::isMethod(fn($foo) => $foo); // bool(false)
CallableType::isMethod('foo'); // bool(false)
CallableType::isMethod([$foo, 'bar']); // bool(true)
CallableType::isMethod('Foo\Bar::baz'); // bool(true)
CallableType::isStaticMethod(fn($foo) => $foo); // bool(false)
CallableType::isStaticMethod('foo'); // bool(false)
CallableType::isStaticMethod([$foo, 'bar']); // bool(false)
CallableType::isStaticMethod('Foo\Bar::baz'); // bool(true)
CallableType::reflection(fn($foo) => $foo); // object(ReflectionFunction)
CallableType::reflection('foo'); // object(ReflectionFunction)
CallableType::reflection([$foo, 'bar']); // object(ReflectionMethod)
CallableType::reflection('Foo\Bar::baz'); // object(ReflectionMethod)
ClassType::short('Foo\Bar'); // "Bar"
ClassType::short(Foo\Bar::class); // "Bar"
ClassType::short(new Foo\Bar()); // "Bar"
namespace Foo;
class Bar {};
class Baz extends Foo {};
...
ClassType::selfAndParents('Foo\Bar'); // ["Foo\Bar"]
ClassType::selfAndParents(Foo\Bar::class); // ["Foo\Bar"]
ClassType::selfAndParents(new Foo\Bar()); // ["Foo\Bar"]
ClassType::selfAndParents('Foo\Baz'); // ["Foo\Baz", "Foo\Bar"]
ClassType::selfAndParents(Foo\Baz::class); // ["Foo\Baz", "Foo\Bar"]
ClassType::selfAndParents(new Foo\Baz()); // ["Foo\Baz", "Foo\Bar"]
This function extends PHP's built-in class_parents() by optionally including the class itself and implemented interfaces.
namespace Foo;
class Bar {};
class Baz extends Bar implements Stringable {};
ClassType::parents(Baz::class) // [Baz::class, Bar::class]
ClassType::parents(Baz::class, withSelf: false) // [Bar::class]
ClassType::parents('MyClass', withSelf: true, withInterfaces: true) // [Baz::class, Bar::class, Stringable::class]
namespace Foo;
class Bar {};
class Baz extends Foo {};
...
ClassType::selfAndParents('Foo\Bar'); // ["Foo\Bar"]
ClassType::selfAndParents(Foo\Bar::class); // ["Foo\Bar"]
ClassType::selfAndParents(new Foo\Bar()); // ["Foo\Bar"]
ClassType::selfAndParents('Foo\Baz'); // ["Foo\Baz", "Foo\Bar"]
ClassType::selfAndParents(Foo\Baz::class); // ["Foo\Baz", "Foo\Bar"]
ClassType::selfAndParents(new Foo\Baz()); // ["Foo\Baz", "Foo\Bar"]
Basic enumerations does not implement from() or tryFrom() methods, but it is possible to return the corresponding enum case using the constant() function.
ClassType::enumCase(FooEnum::class, 'bar');
FileType::extension('/foo/bar.baz'); // 'baz'
FileType::extension('/foo/bar'); // ''
FileType::put('/foo/bar.txt', 'baz');
FileType::temporary('foo.txt'); // '/tmp/foo.txt'
FileType::isAbsolutePath('C:/foo'); true
FileType::isAbsolutePath('C:\\bar'); true
FileType::isAbsolutePath('foo/bar'); false
FileType::isAbsolutePath('/foo/bar'); true
FileType::isAbsolutePath('\\foo\\bar'); true
FileType::joinPaths('a/', '/b/', '\\c', 'd'); // Return a/b/c/d
Fix the directory separators (remove duplicates and replace with the current system directory separator)
FileType::fixPath('/foo//bar\baz'); '/foo/bar/baz'
FileType::guessExtensions('/foo/bar.txt'); // ['txt']
FileType::guessExtensions('/foo/bar.jpg'); // ['jpeg', 'jpg', 'jpe', 'jfif']
FileType::guessExtension('/foo/bar.txt'); // 'txt'
FileType::guessExtension('/foo/bar.jpg'); // 'jpeg'
FileType::guessMime('/foo/bar.txt'); // 'text/plain'
FileType::guessMime('/foo/bar.jpg'); // 'image/jpeg'
Remove all characters except digits, +-.,eE from the argument and returns result as the float value or NULL if the parser fails.
NumberType::parse(" 123 "); // int(123)
NumberType::parse(" 123.45 "); // float(123.45)
NumberType::parse(" 123.00 "); // int(123)
Remove all characters except digits, +-.,eE from the argument and returns result as the float value or NULL if the parser fails.
NumberType::parseFloat(" 123 "); // float(123)
NumberType::parseFloat(" 123.45 "); // float(123.45)
Remove all characters except digits, plus and minus sign and returns result as the integer value or NULL if the parser fails.
NumberType::parseInteger(" 123 "); // int(123)
NumberType::parseFloat(" 123.45 "); // int(12345)
NumberType::fractions(123.45); // float(0.45)
NumberType::parseFloat(123); // float(0)
NumberType::odd(2); // false
NumberType::odd(3); // true
NumberType::even(2); // true
NumberType::even(3); // false
NumberType::roundStep(50, 5); // 50
NumberType::roundStep(52, 5); // 50
NumberType::roundStep(53, 5); // 55
NumberType::floorStep(50, 5); // 50
NumberType::floorStep(52, 5); // 50
NumberType::floorStep(53, 5); // 50
NumberType::ceilStep(50, 5); // 50
NumberType::ceilStep(52, 5); // 55
NumberType::ceilStep(53, 5); // 55
// Current locale used
NumberType::spellout(123.45); // one hundred twenty-three point four five
// Specific locale used
NumberType::spellout(123.45, 'ru'); // сто двадцать три целых сорок пять сотых
NumberType::divideSafely(1, 0); // null
NumberType::divideSafely(1, null); // null
NumberType::divideSafely(1, 0, 'zero'); // 'zero'
The first argument is a value for calculating the percentage. The second argument is a base value corresponding to 100%.
NumberType::percentage(10, 100); // 10
NumberType::percentage(100, 100); // 100
NumberType::percentage(200, 100); // 200
A negative value will be converted to zero, positive or zero value will be returned unchanged.
NumberType::unsign(-1); // 0
NumberType::unsign(-0.99); // 0
NumberType::unsign(0); // 0
NumberType::unsign(0.99); // 0.99
NumberType::unsign(1); // 1
NumberType::toStringWithSign(-1); // "-1"
NumberType::toStringWithSign(1); // "+1"
NumberType::toStringWithSign(0); // "0"
ObjectType::get($person, 'address.street');
Uses Symfony PropertyAccessor
Read ocramius
ObjectType::getInternal($object, $property);
Read ocramius
$grandfather = new Person(name: 'grandfather');
$dad = new Person(name: 'dad', parent: $grandfather);
$i = new Person(name: 'i', parent: $dad);
ObjectType::getRecursive($i, 'parent'); // [Person(dad), Person(grandfather)]
ObjectType::set($person, 'address.street', 'Abbey Road');
Uses Symfony PropertyAccessor
Read ocramius
ObjectType::setInternal($object, $property, $value);
Read ocramius
ObjectType::callInternal($object, $method, $arg1, $arg2, $arg3, ...);
- Result of __toString method if presents
- String value of case for the BackedEnum
- Name of case for the UnitEnum
- or generated string like "FQCN@spl_object_id"
PHP default behavior: if the method is not defined, an error (Object of class X could not be converted to string
) is triggered.
namespace Foo;
class Bar {
}
class BarMagicMethod {
public function __toString(): string {
return 'Bar';
}
}
enum BazUnitEnum {
case APPLE;
}
enum BazStringBackedEnum: string {
case APPLE = 'apple';
}
enum BazIntBackedEnum: int {
case APPLE = 1;
}
ObjectType::toString(new Foo); // 'Foo/Bar@1069'
ObjectType::toString(new FooMagicMethod); // 'Foo'
ObjectType::toString(BazUnitEnum::APPLE); // 'APPLE'
ObjectType::toString(BazStringBackedEnum::APPLE); // '1'
Returns the result of __toString
or null if the method is not defined.
PHP default behavior: if the method is not defined, an error (Object of class X could not be converted to string
) is triggered.
ObjectType::toClassName($objectOrClass): string;
StringType::contains('Foo', 'Bar'); // false
StringType::contains('FooBar', 'Bar'); // true
StringType::decrypt(StringType::encrypt('The sensitive string', 'qwerty123456'), 'qwerty123456'); // 'The sensitive string'
StringType::encrypt('The sensitive string', 'qwerty123456');
Default behaviour like preg_match_all(..., ..., PREG_SET_ORDER)
StringType::regexp('a1b2', '\S(\d)'); // [0 => [0 => 'a1', 1 => '1'], 1 => [0 => 'b2', 1 => '2']]
Exclude full matches from regular expression matches
StringType::regexp('a1b2', '\S(\d)', true); // [0 => [0 => '1'], 1 => [0 => '2']]
Get only first set from regular expression matches (exclude full matches)
StringType::regexp('a1b2', '(\S)(\d)', true, true); // [0 => 'a', 1 => '1']
Get only first match of each set from regular expression matches (exclude full matches)
StringType::regexp('a1b2', '(\S)(\d)', true, false, true); // [0 => 'a', 1 => 'b']
Get only first match of the first set from regular expression matches as single scalar value
StringType::regexp('a1b2', '(\S)(\d)', true, true, true); // 'a'
StringType::replaceFirst('name name name', 'name', 'title'); // 'title name name'
Find the position of a substring within a string with support for case sensitivity, reverse search, and multibyte encodings
This method improves upon PHP's native string position functions (like strpos, stripos, etc.) by eliminating common pitfalls:
- it returns null instead of false when the substring is not found — preventing type confusion
- supports multibyte and 8-bit encodings
// Basic search in a UTF-8 string
$pos = StringType::position('Hello 世界', '世'); // returns 6
// Case-insensitive search
$pos = StringType::position('Hello World', 'WORLD', searchCaseSensitive: false); // returns 6
// Find last occurrence of substring
$pos = StringType::position('abcbc', 'bc', searchFromEnd: true); // returns 3
// Returns null when substring is not found (not false)
$pos = StringType::position('test', 'x'); // returns null
// Disable multibyte mode for ASCII-only strings
$pos = StringType::position('simple text', 'text', multibyteEncoding: false); // returns 6
This function extracts the part of the haystack string that appears before the specified needle.
It supports case-sensitive and case-insensitive searches, allows searching from the end of the string, and handles multibyte characters correctly by default.
// Returns 'Hello ' (before 'World' in a case-sensitive search)
StringType::strBefore('Hello World', 'World');
// Returns null because 'world' is not found with case-sensitive search
StringType::strBefore('Hello World', 'world');
// Returns 'Hello ' due to case-insensitive search
StringType::strBefore('Hello World', 'world', searchCaseSensitive: false);
// Returns 'Hello Wor' (before last 'l', searching from the end)
StringType::strBefore('Hello World', 'l', searchFromEnd: true);
// Returns 'Привет ' (correctly handles Cyrillic characters)
StringType::strBefore('Привет Мир', 'Мир');
// Returns null when needle is not found
StringType::strBefore('Test', 'xyz');
This function extracts the portion of the haystack string that comes after the specified needle. It supports case-sensitive and case-insensitive searches, allows searching from the end of the string, and properly handles multibyte characters by default.
// Returns 'World' (after 'Hello ' in a case-sensitive search)
StringType::strAfter('Hello World', 'Hello ');
// Returns null because 'hello ' is not found when case-sensitive
StringType::strAfter('Hello World', 'hello ');
// Returns 'World' due to case-insensitive search
StringType::strAfter('Hello World', 'hello ', searchCaseSensitive: false);
// Returns 'd' (after the last occurrence of 'l', searching from the end)
StringType::strAfter('Hello World', 'l', searchFromEnd: true);
// Returns 'Мир' (correctly handles multibyte UTF-8 characters)
StringType::strAfter('Привет Мир', 'Привет ');
// Returns null when needle is at the end and nothing follows
StringType::strAfter('Test', 'st');
StringType::wrap('target', '/'); // '/target/'
StringType::guessMime('foo bar'); // "text/plain"
StringType::guessMime(file_get_content("foo.jpg")); // "image/jpeg"
StringType::guessExtension('foo bar'); // "txt"
StringType::guessExtension(file_get_content("foo.jpg")); // "jpeg"
StringType::isBinary('Foo bar baz'); // false
StringType::toCamelCase('string like this'); // 'StringLikeThis'
StringType::toCamelCase('string_like_this'); // 'StringLikeThis'
StringType::toSnakeCase('StringLikeThis'); // 'string_like_this'
StringType::toSnakeCase('string Like this'); // 'string_like_this'
Use these only if you are supplying the charlist optional arg and it contains UTF-8 characters. Otherwise trim will work normally on a UTF-8 string.
trim('«foo»', '»'); // "�foo"
StringType::trim('«foo»', '»'); // "«foo"
StringType::sentences('Fry me a Beaver. Fry me a Beaver! Fry me a Beaver? Fry me Beaver no. 4?! Fry me many Beavers... End);
returns
[
[0] => 'Fry me a Beaver.',
[1] => 'Fry me a Beaver!',
[2] => 'Fry me a Beaver?',
[3] => 'Fry me Beaver no. 4?!',
[4] => 'Fry me many Beavers...',
[5] => 'End'
]
We assume that words are separated by whitespace.
Words can contain letters, numbers, and other symbols, but a word must begin and end with only a letter or number.
StringType::words('Fry me many Beavers... End'); // ['Fry', 'me', 'many', 'Beavers', 'End']
// Another cases
StringType::words('Roland'); // ['Roland']
StringType::words('Roland TB303'); // ['Roland', 'TB303']
StringType::words('Roland TB-303'); // ['Roland', 'TB-303']
StringType::words('Roland TB-303.'); // ['Roland', 'TB-303']
StringType::words('Roland TB-303â'); // ['Roland', 'TB-303â']
StringType::words('âRoland TB-303'); // ['âRoland', 'TB-303']
StringType::words('Roland - TB303'); // ['Roland', 'TB303']
StringType::words('Roland – TB303'); // ['Roland', 'TB303']
StringType::words("Roland'â - TB303"); // ["Roland'â", 'TB303']
StringType::words('"Roland" - TB303'); // ['Roland', 'TB303']
StringType::words('R.O.L.A.N.D. - TB303'); // ['R.O.L.A.N.D', 'TB303']
StringType::unword('Remove word from text', 'word'); // 'Remove from text'
Manually instance a DoctrineUtils
public function __construct(private Doctrine\Persistence\ManagerRegistry $doctrine)
{
$doctrineUtils = new Cosmologist\Gears\Doctrine\DoctrineUtils($doctrine);
}
Register DoctrineUtils as a service with Symfony DI
# config/services.yaml
services:
_defaults:
autowire: true
Cosmologist\Gears\Doctrine\DoctrineUtils:
$doctrineUtils->getClassMetadata(new App\Entity\User()); // object(ClassMetadata)
$doctrineUtils->getClassMetadata(App\Entity\User::class); // object(ClassMetadata)
$doctrineUtils->getClassMetadata(new App\Controller\FooController())); // null
$doctrineUtils->getClassMetadata(App\Controller\FooController::class); // null
$doctrineUtils->getRealClass(Proxies\__CG__\App\Entity\User::class); // 'App\Entity\User'
$doctrineUtils->getRealClass(new Proxies\__CG__\App\Entity\User()); // 'App\Entity\User'
$doctrineUtils->getRealClass(App\Entity\User::class); // 'App\Entity\User'
$doctrineUtils->getRealClass(new App\Entity\User()); // 'App\Entity\User'
$doctrineUtils->getRealClass(new App\Controller\FooController()); // null
$doctrineUtils->getRealClass(App\Controller\FooController::class); // null
$doctrineUtils->isManaged(new MyEntity()); // true
$doctrineUtils->isManaged(new stdClass()); // false
$doctrineUtils->getSingleIdentifierField(new MyEntityWithSingleIdentifier(id: 1000)); // 'id'
$doctrineUtils->getSingleIdentifierField(new MyEntityWithMultipleIdentifiers()); // \Assert\InvalidArgumentException
$doctrineUtils->getSingleIdentifierField(new stdClass); // \Assert\InvalidArgumentException
$doctrineUtils->getSingleIdentifierValue(new MyEntityWithSingleIdentifier(id: 1000)); // 1000
$doctrineUtils->getSingleIdentifierValue(new MyEntityWithMultipleIdentifiers()); // \Assert\InvalidArgumentException
$doctrineUtils->getSingleIdentifierValue(new stdClass); // \Assert\InvalidArgumentException
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Expr;
DoctrineUtils::mergeCriteria(
new Criteria(new Expr\Comparison('status', Expr\Comparison::EQ, 'new')),
new Criteria(new Expr\Comparison('type', Expr\Comparison::NEQ, 'foo'))
);
$qb = $entityManager->getRepository(Company::class)->createQueryBuilder('company');
DoctrineUtils::join($qb, 'contact.user.type');
// equivalent to
$qb
->join('company.contact', 'contact')
->join('contact.user', 'user')
->join('user.type', 'type');
$qb = $entityManager->getRepository(Company::class)->createQueryBuilder('contact');
// Adds a join and returns an alias of added join
DoctrineUtils::joinOnce($qb, 'contact.user', 'u1'); // "u1"
// If a join with specified parameters exists then only returns an alias of existed join
DoctrineUtils::joinOnce($qb, 'contact.user', 'u2'); // "u1"
$doctrineUtils->getAssociationTargetClassRecursive('AppBundle/Entity/Company', 'contact.user'); // 'AppBundle/Entity/User'
ExpressionFunctionUtils::fromCallable('Foo\Bar::baz'); // object(ExpressionFunction)
For example, this can be useful for injecting simple objects (like ValueObject) into a Symfony service container
class AppExtension extends Extension
{
#[Override]
public function load(array $configs, ContainerBuilder $container)
{
$container->addExpressionLanguageProvider(new class implements ExpressionFunctionProviderInterface {
public function getFunctions(): array {
return [ExpressionFunctionUtils::fromCallable([WalletIdentifier::class, 'create'], 'walletId')];
}
});
$container
->getDefinition(OrderService::class)
->setArgument('$wallet', expr('walletId(13)'));
}
}
It's maybe useful when you validate your model from form on the domain layer and want to map violations to the form.
use Cosmologist\Gears\Symfony\Form\FormUtils;
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
use Symfony\Component\Validator\Exception\ValidationFailedException;
if ($form->isSubmitted()) {
try {
return $this->handler->create($form->getData());
} catch (ValidationFailedException $exception) {
$violationMapper = new ViolationMapper();
foreach ($exception->getViolations() as $domainViolation) {
$violationMapper->mapViolation(FormUtils::convertDomainViolationToFormViolation($domainViolation), $form);
}
}
}
return $form->createView();
This is convenient for mapping of form data to a model via DataMapperInterface::mapFormsToData(), for example, to create a model via a constructor, in this case, the mapping of model data to a form via DataMapperInterface::mapDataToForms() will remain unchanged, and you cannot not define it, since it is required by the DataMapperInterface.
use Cosmologist\Gears\Symfony\Form\DataFormsMapperDefaultTrait;
class TransactionFormType extends AbstractType implements DataMapperInterface
{
use DataFormsMapperDefaultTrait;
#[Override]
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class)
->setDataMapper($this);
}
#[Override]
public function mapFormsToData(Traversable $forms, mixed &$viewData): void
{
$forms = iterator_to_array($forms);
$viewData = new Contact($forms['name']->getData());
}
namespace App;
use App\DependencyInjection\AppExtension;
use Cosmologist\Gears\Symfony\Framework\AppExtension\AppExtensionKernelInterface;
use Cosmologist\Gears\Symfony\Framework\AppExtension\RegisterAppExtensionKernelTrait;
use Override;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel implements AppExtensionKernelInterface
{
use MicroKernelTrait;
use RegisterAppExtensionKernelTrait;
#[Override]
public function getAppExtension(): ExtensionInterface
{
return new AppExtension();
}
}
class FooTest extends KernelTestCase
{
use MessengerTestUtilsTrait;
protected function setUp(): void
{
self::bootKernel();
// A MessengerTestUtilsTrait needs your command bus
$this->commandBus = $this->getContainer()->get('command.bus');;
}
public function testBar() {
$this->assertCommandShouldFail(new FooCommand);
$this->assertCommandShouldFail(new FooCommand, BarException::class);
}
}
It a convenient way to speed up your app response to clients by scheduling hard tasks after the server response, thanks to the kernel.terminate
event.
When run an application from the CLI, the kernel.terminate
event not generated, in this case the events handled on the console.terminate
event.
Firstly, you should enable this transport:
# config/services.yaml
services:
_defaults:
autoconfigure: true
Cosmologist\Gears\Symfony\Messenger\Transport\KernelTerminate\KernelTerminateTransportFactory:
Configure a messenger:
# config/packages/messenger.yaml
framework:
messenger:
transports:
terminate: symfony-kernel-terminate://
routing:
App\Event\FooEvent: terminate
# Use "sync://" transport instead "symfony-kernel-terminate://" for tests
when@test:
framework:
messenger:
transports:
terminate: sync://
and
$this->messenger->dispatch(new App\Event\FooEvent('bar'));
// or
$this->messengerBus->dispatch(new App\Event\FooEvent('bar'));
{% include '@Gears/pagination.html.twig' with { page: current_page, count: items_total, limit: items_per_page } %}
{# Parameters:
* page (int) : The current page you are in
* limit (int): Number of records to display per page
* count (int): Total count of records
* currentFilters (array)(optional) : associative array that contains route-arguments #}
Before use, you should register @Gears
as Twig namespace
# config/packages/twig.yaml
twig:
paths:
'%kernel.project_dir%/vendor/cosmologist/gears/src/Twig/Pagination/Resources/views': Gears
use Cosmologist\Gears\Symfony\PropertyAccessor\RecursivePropertyAccessor;
$grandfather = new Person(name: 'grandfather');
$dad = new Person(name: 'dad', parent: $grandfather);
$i = new Person(name: 'i', parent: $dad);
(new RecursivePropertyAccessor())->getValue($i, 'parent'); // [Person(dad), Person(grandfather)]
A SuperUserRoleVoter brings a ROLE_SUPER_USER, which effectively bypasses any, and all security checks
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
Cosmologist\Gears\Symfony\Security\Voter\SuperUserRoleVoter:
class FooController extends AbstractController
{
public function barAction(): Response
{
$this->denyAccessUnlessGranted(SuperUserRoleVoter::ROLE_SUPER_USER);
...
}
}
use Cosmologist\Gears\Symfony\Test\TestUtils;
class FooTest extends WebTestCase
{
protected function testBar(): void
{
$browser = self::createClient();
TestUtils::addHeader($browser, 'User-Agent', 'Symfony KernelBrowser');
...
}
}
use Cosmologist\Gears\Symfony\Validator\ValidationFailedException;
ValidationFailedException::violate($foo, "Foo with invalid bar");
ValidationFailedException::violate($foo, "Foo with invalid {{ bar }}", compact('bar'));
ValidationFailedException::violate($foo, "Foo with invalid bar", propertyPath: 'bar');
class ProductIdentifier extends IdentifierAbstract {}
$p1 = new ProductIdentifier(123);
$p1->getValue(); // 123
$p2 = new ProductIdentifier('string-id');
$p2->getValue(); // 'string-id'
$p1->equals($p2); // bool(false)
$p1->equals(new ProductIdentifier(123)); // bool(true)
$p1->equals(123); // bool(true)
class ProductIdentifier extends IdentifierUuidAbstract {}
// Create UUID-identifier from value
$product = new ProductIdentifier('70b3738c-dec5-40a1-a992-bdadb3e33f9d'); // object(ProductIdentifier)
// Create UUID-identifier without value validation (default behaviour)
$product = new ProductIdentifier('123'); // object(ProductIdentifier)
// Create UUID-identifier with value validation
$product = new ProductIdentifier('123', validate: true); // InvalidArgumentException
// Create UUID-identifier with auto-generated value (UUID v4)
$product = new ProductIdentifier(); // // object(ProductIdentifier)
// IdentifierUuidAbstract extends IdentifierAbstract so also you can also call
$product->getValue(); // string('2b29a26d-ce2a-41a1-bcb7-41858ae4820f')
// and
$product->equals('2b29a26d-ce2a-41a1-bcb7-41858ae4820f'); // bool(true)
The hybrid UUID-Identifier Value Object allows encoding up to two numeric values in a human-readable format.
This can convenient, for example, when a system works with UUIDs, but certain entities still rely on classic incremental identifiers. This hybrid UUID implementation may be used to hold 1–2 integer values that can be extracted from it.
$userEntity->getId(); // 12345
$uuid = new UserIdentifier($userEntity->getId()); // 00012345-0000-8aaa-bbbb-cccdddeeefff
$uuid->getPrimaryValue(); // 12345
It also supports storing a secondary value to identify nested or aggregated data:
$userEntity->getId(); // 12345
$userPhoto = $userEntity->getRandomPhoto();
$userPhoto->getNumber(); // 25
$uuid = new UserPhotoIdentifier($userEntity->getId(), $userPhoto->getNumber()); // 00012345-0025-8aaa-bbbb-cccdddeeefff
$uuid->getPrimaryValue(); // 12345
$uuid->getSecondaryValue(); // 25
Hybrid identifiers are highly readable for humans, especially within context. From the example above, it’s immediately clear that this refers to photo #25 of user #12345.
The hybrid UUID identifier follows this structure:
01234567-0890-8aaa-bbbb-cccdddeeefff
1234567
— Encodes the primary integer identifier, supporting values from 0 to 99,999,999 (approximately uint26).890
— Encodes the optional secondary identifier, supporting values from 0 to 9,999 (approximately uint13).8
— UUID specification version.aaa-bbbb-cccdddeeefff
— A suffix unique to each identifier implementation, defined and returned by the method IdentifierUuidHybridAbstract::suffix().
This technique is made possible by leveraging the UUID v8 (custom UUID) specification.