cosmologist/gears
Handy helper library for PHP and Symfony with utility functions for arrays, strings, objects, numbers, files, cache, callables, classes, and Guzzle, plus integrations for Doctrine, Symfony components (Forms, Messenger, Security, Twig, Validator) and value objects (identifiers/UUID).
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:
iterator_to_array())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;
use Cosmologist\Gears\Browser\BrowserControl;
$browser = new BrowserControl();
// Wait for browser extension to connect
$browser->listen();
$browser->navigate('http://example.com');
$html = $browser->getHtml();
$titleLength = $browser->execute('return document.title.length;');
$elements = $browser->select('h1');
$browser->disconnect();
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');
File object-oriented implementation
$storage = new File('storage/abc.json');
$storage->serialize($fooObject);
$storage = new File('storage/abc.json');
$storage->serialize($fooObject);
$barObject = $storage->unserialize(MyObject::class); // object(MyObject)
Uses finfo to detect actual MIME type from file content.
$file = new File('image.jpg');
$base64 = $file->toBase64(); // data:image/jpeg;base64,/9j/4AAQ...
The method binds a simple PHP socket server, generates a unique hash in the URL,
passes that URL into the callback, waits for a valid GET request, sends the file as a download, and then stops.
$file = new File('storage/report.pdf');
$file->serve(function (string $url) {
// for example, send the URL to a browser, bot, or another process
var_dump($url);
});
$file = new File('image.jpg');
$mimeType = $file->mime(); // 'image/jpeg'
$file = new File('path/to/dir');
foreach ($file->list(recursive: true) as $child) {
echo $child->basename() . "\n";
}
$file = new File('path/to/file.txt');
$file->delete(); // deletes the file
For directories, use the recursive flag:
$file = new File('path/to/dir');
$file->delete(recursive: true); // deletes directory and all contents
Missing paths are ignored.
If the file does not exist, the provider callable is invoked to get the content, which is then written to the file.
$file = new File('storage/data.json');
$file->putIfNotExists(fn() => $backupService->fetch());
use Cosmologist\Gears\Guzzle\GuzzleBuilder;
$client = GuzzleBuilder()
->configureAsBrowser()
->create();
// Or configure individually
$client = GuzzleBuilder()
->useTimeout(15.0)
->useConnectTimeout(5.0)
->useCookies()
->allowRedirects(max: 5, strict: false, referer: true, track_redirects: true)
->create();
use Cosmologist\Gears\Guzzle\GuzzleBuilder;
// Bind to specific interface
$guzzleBuilder = new GuzzleBuilder();
$guzzleBuilder->bindToInterface('eth0');
$client = $guzzleBuilder->create();
// Auto-detect and bind to first physical interface
$guzzleBuilder = new GuzzleBuilder();
$guzzleBuilder->bindToInterface();
$client = $guzzleBuilder->create();
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
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'
A generic HTTP client wrapper utilizing PSR-18 HTTP Client and PSR-17 HTTP Factories.
An abstract client preconfigured with common browser headers.
use Cosmologist\Gears\DataClient\WebDataClient;
class MyWebClient extends WebDataClient {
// Implement domain specific logic if needed
}
$client = new MyWebClient($psr18Client, $psr17RequestFactory, $psr17StreamFactory);
$html = $client->fetchBy('https://example.com');
Network::isIp('127.0.0.1'); // true
Network::isIp('::1'); // true
Network::isIp('::1', allowIpV4: true, allowIpV6: false); // false
Network::assertIp('127.0.0.1');
Network::assertIp('::1', allowIpV4: false, allowIpV6: true);
$file = new File('storage/report.pdf');
Network::serve($file, function (string $url) {
var_dump($url);
});
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, ...);
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'
This method improves upon PHP's native string position functions (like strpos, stripos, etc.) by eliminating common pitfalls:
// 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'
The criteria specified using Doctrine\Common\Collections\Criteria.
$criteria = new Criteria();
$criteria->where(new Expr\Comparison('uuid', Expr\Comparison::EQ, $order->getValue()));
$criteria->orWhere(new Expr\Comparison('_related->"$[*]"', Expr\Comparison::MEMBER_OF, $order->getValue()));
$this->eventStore->walk(
$criteria,
function (DomainMessage $domainMessage) {
...
}
);
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'
The converter implementation incomplete - a limited number of comparisons currently supported.
$criteria = new Criteria();
$criteria->where(new Expr\Comparison('uuid', Expr\Comparison::EQ, $order->getValue()));
$criteria->orWhere(new Expr\Comparison('_related->"$[*]"', Expr\Comparison::MEMBER_OF, $order->getValue()));
$queryBuilder = $this->connection->createQueryBuilder();
$visitor = new DbalExpressionVisitor($queryBuilder);
$queryBuilder
->select('*')
->from($this->tableName)
->andWhere($criteria->getWhereExpression()->visit($visitor))
->executeQuery();
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();
}
}
Like Symfony\Bundle\FrameworkBundle\Controller\Controller::forward, but for URIs.
use Cosmologist\Gears\Symfony\Framework\Controller\ControllerUtils;
ControllerUtils::forwardByUri('/blog/my-post');
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'));
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 simple yet convenient alternative to the standard acl:set command from AclBundle — no need to remember command semantics, user and object classes, worry about escaping, etc. every time you need to add access rights.
The command allows you to interactively select a user identity and object identity from existing ACLs.
If you want to create an ACL from a new user/object identity, you should pass the appropriate arguments to the command.

Enable the command:
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
Cosmologist\Gears\Symfony\Security\Acl\AclInteractiveCommand:
bind:
$aclConnection: '@doctrine.dbal.security_connection'
$aclProvider: '@security.acl.provider'
$aclTables:
table_classes: '%security.acl.dbal.class_table_name%'
table_sid: '%security.acl.dbal.sid_table_name%'
# 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');
...
}
}
{% set linkAttrs = {
'href': 'https://github.com/Cosmologist/Gears',
'class': ['important', 'disabled'],
'style': {'display': 'block', 'color': 'green'},
'data-attribute': 'bar',
'data-json': {'foo': 'bar'}} %}
<a{{ linkAttrs | htmlAttributes }}>Click me</a>
rendered as:
<a href="https://github.com/Cosmologist/Gears"
class="important disabled"
style="display: block; color: green;"
data-attribute="bar"
data-json="{'foo':'bar'}">Click me</a>
Use the "merge" filter to manage the attribute dictionary.
{% set linkAttrs = linkAttrs | merge({'class': 'disabled'}) %}
The "class" attribute may contain null values - they will be filtered out.
<a{{ {'class': ['important', null]} | htmlAttributes }}>Click me</a> - <a class="important">Click me</a>
Non-unique values of the "class" attribute will be grouped.
<a{{ {'class': ['important', 'important', null]} | htmlAttributes }}>Click me</a> - <a class="important">Click me</a>
You can pass the "class" attribute as a scalar.
<a{{ {'class': 'important'} | htmlAttributes }}>Click me</a> - <a class="important">Click me</a>
You can pass the "style" attribute as a scalar.
<a{{ {'style': 'display: none'} | htmlAttributes }}>Click me</a> - <a style="display: none">Click me</a>
You can pass the "style" attribute as a list.
<a{{ {'style': ['display: none', 'color: green']} | htmlAttributes }}>Click me</a> - <a style="display: none; color: green">Click me</a>
Don't forget to enable this extension
# config/services.yaml
services:
_defaults:
autoconfigure: true
Cosmologist\Gears\Twig\GearsUtilsExtension:
{% 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
and declare GearsTwigExtension
# config/services.yaml
services:
_defaults:
autoconfigure: true
Cosmologist\Gears\Twig\GearsUtilsExtension:
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.
How can I help you explore Laravel packages today?